【问题标题】:C memory management conventions: freeing memory allocated on heap by object allocated on stackC 内存管理约定:通过在堆栈上分配的对象释放在堆上分配的内存
【发布时间】:2021-03-20 05:42:56
【问题描述】:

我想我对 C 中的内存管理约定有点困惑。

假设我们有一个在堆上动态分配数据的结构。 这个结构体提供了_alloc()_free() 函数,用于在堆上分配和释放这个结构体。

这是一个简单的向量结构示例:

struct vec_t {
  int *items;
  size_t size;
  size_t capacity;
};

struct vec_t *vec_alloc(void)
{
  struct vec_t *vec = malloc(sizeof(struct vec_t));

  vec->items = NULL;
  vec->size = 0;
  vec->capacity = 0;

  return vec;
}

void vec_free(struct vec_t *vec)
{
  free(vec->items);
  free(vec);
}

void vec_push(struct vec_t *vec, int item)
{
  if (vec->size == vec->capacity)
  {
    size_t new_capacity = vec->capacity > 0 ? (vec->capacity + 1) * 2 : 5;
    vec->items = realloc(vec->items, new_capacity * sizeof(int));
  }
  vec->items[vec->size++] = item;
}

现在假设我们不使用_alloc()_free(),而是决定在堆栈上分配这个结构。

然后我们通过栈分配结构(my_vec.items)间接在堆上分配数据

void main()
{
  vec_t my_vec;
  vec_push(&my_vec, 8); // allocates memory

  // vec_destroy(&my_vec); // PROBLEM

  return 0;
}

现在我们有一个问题:我们不想释放结构体 (my_vec),因为它在堆栈上,但我们需要释放结构体在堆上分配的数据 (my_vec.items)。

我认为这要么是设计问题,要么是约定问题,或者两者兼而有之。

我看到人们在_alloc()_free() 之外添加了一些额外的功能_init()_deinit()

void vec_init(struct vec_t *vec)
{
  vec->items = NULL;
  vec->size = 0;
  vec->capacity = 0;
}

void vec_deinit(struct vec_t *vec)
{
  free(vec->items);
}

释放_deinit()中的结构分配的内存有意义吗?

如果这种方法是正确的,我是否正确地说分配在这样的堆栈上的结构总是需要_init()_deinit()

【问题讨论】:

  • 通常的解决方案是通过将struct vec_t 设为不完整类型来禁止堆栈分配。如果你想允许堆栈分配,那么是的,你需要某种 _init_deinit 函数来准备和清理。
  • @neeh _init 和 _deinit 是什么?
  • vec->items = realloc(vec, ...) 是错误的。此外,你永远不应该重新分配你传递给realloc的指针。
  • 您需要某种形式的 init 函数,否则您的堆示例使用未初始化的 my_vec 对象,当您尝试使用它时可能会发生任何事情。
  • 旁注:vec_push(my_vec, 8); 对于堆栈分配的情况是错误的;你大概是指vec_push(&my_vec, 8); 那里are ways to make it work either way 使用单元素数组的typedef(这就是GMP 的工作原理),但在许多情况下它是不受欢迎的; openssl 最终将其 BIGNUM API 切换到不透明(强制堆)结构,部分原因是 API 处理堆栈或堆的复杂性。

标签: c memory memory-management


【解决方案1】:

如果您使用_init_deinit 函数,是的,您希望_deinit 释放内存,是的,vec_initvec_deinit 对于堆栈分配的结构是必需的。对于这个用例,堆栈分配的结构 可以 使用 vec_t my_vec = {0}; 初始化并避免 vec_init 调用,但假设归零现在和永远产生一个有效初始化的结构(如果您更改 vec_init稍后要使某些字段不为零,您的库中未使用 vec_init 的用户必须更新),并且当不可避免的 vec_deinit 未与相应的 vec_init 配对时,可能会造成混淆。

请注意,代码不需要如此重复; _alloc_free可以按照_init_deinit来实现,尽量减少代码重复:

struct vec_t *vec_alloc(void)
{
  struct vec_t *vec = malloc(sizeof(struct vec_t));
  if (vec) vec_init(vec);  // Don't try to init if malloc failed
  return vec;
}

void vec_free(struct vec_t *vec)
{
  if (vec) vec_deinit(vec); // Don't try to deinit when passed NULL
  free(vec);
}

【讨论】:

    【解决方案2】:

    我个人的做法是在设计中假设结构可以并且将存在于堆栈中,并编写在已经分配的结构上工作的代码。快速简化示例:

    typedef struct vect_t {
       char *data;
       size_t len;
    } vec_t;
    
    void vec_set(vec_t *v, void *data, size_t len) {
        v->data = data;
        v->len = len;
    }
    
    void vec_clear(vec_t *v) {
        free(v->data);
        vec_set(v, NULL, 0);
    }
    
    int vec_resize(vec_t *v, size_t len) {
        void * data = realloc(v->data, len);
        if (!data) { /* out of memory */
            vec_set(v, NULL, 0);
            return ENOMEM;
        }
        vec_set(v, data, len);
        return 0;
    }
    
    int stack_example(void) {
        vec_t v;
        int err;
        vec_set(&v, NULL, 0);
        if ((err = vec_resize(&v, 64)) !=0) {
            return err;
        }
        strcpy(v.data, "Hello World");
        vec_clear(&v);
        return 0;
    }
    
    void heap_example(void) {
        vec_t *v = malloc(sizeof(vec_t));
        if (v) {
            int err;
            vec_set(v, NULL, 0);
            if ((err = vec_resize(v, 64)) !=0) {
                return err;
            }
            strcpy(v->data, "Hello World");
            vec_clear(v);
            free(v);
       }
    }
    

    将结构放在堆栈上的优点是您可以减少堆分配(有利于性能和碎片),但这当然是以堆栈大小为代价的,这可能是您的限制,具体取决于您所处的环境在。

    【讨论】:

      【解决方案3】:

      您混合了两个概念:动态内存分配和对象初始化。

      考虑到这个结构声明

      struct vec_t {
        int *items;
        size_t size;
        size_t capacity;
      };
      

      没有说这种类型的对象应该在堆中分配。

      但是,与定义位置无关的类型的对象应被初始化。否则你会得到未定义的行为。

      初始化的逆操作是清洗。

      您可以声明具有自动存储持续时间的类型的对象,例如

      struct vec_t v = { .items = NULL, .size = 0, .capacity = 0 };
      

      但是这种方法并不灵活。用户可以直接访问实现/结构定义中的任何更改都可能导致此初始化不正确。

      所以最好为该类型的对象的初始化提供一个通用接口。你可以写例如

      void vec_init( struct vec_t *v )
      {
          v->items = NULL;
          v->size = 0;
          v->capacity = 0;
      }  
      

      void vec_clear( struct vec_t *vec )
      {
          free( v->items );
          v->size = 0;
          v->capacity = 0;
      }
      

      在与例如 C++ 相对的 C 中,如果您动态分配对象,则不会自动调用其初始化(构造)。

      所以如果你想为动态对象分配提供一个接口,你需要再写一个函数,例如

      struct vec_t * vec_create( void )
      {
          struct vec_t *v = malloc( sizeof( *v ) );
      
          if ( v != NULL ) vec_init( v );
      
          return v;
      }
      

      在这种情况下,您可以向用户提供另一个释放动态分配对象的函数,例如

      void vec_destroy( struct vec_t **v )
      {
          free( *v );
          *v = NULL;
      };
      

      【讨论】:

        猜你喜欢
        • 2011-07-25
        • 2013-02-28
        • 2014-10-03
        • 1970-01-01
        • 1970-01-01
        • 2011-05-23
        • 2018-04-03
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多