C 内存管理约定:通过在堆栈上分配的对象释放在堆上分配的内存

C memory management conventions: freeing memory allocated on heap by object allocated on stack

我觉得我对 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()

如果您正在使用 _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);
}

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

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);
   }
}

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

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

考虑到这个结构声明

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;
};