【问题标题】:Dynamic arrays with an embedded meta-information struct具有嵌入式元信息结构的动态数组
【发布时间】:2021-02-04 01:20:05
【问题描述】:

我正在摆弄一个通用动态数组的实现。数组应该保存有关其大小、使用了多少条目的信息,然后保存实际数据。元信息(大小/使用)是通用的,但数据需要处理不同的类型,所以我用宏来处理。但是,我正在尝试将内存分配代码放入函数中。所以我的想法是:我有一个元信息结构

struct da_meta {
  size_t size;
  size_t used;
};

然后我有一个宏,它使用元信息后面的灵活数组为每种类型创建一个结构:

#define dynarray(TYPE)                 \
struct {                               \
  struct da_meta meta;                 \
  TYPE   data[];                       \
}

我可以声明一个整数数组,例如,as

  dynarray(int) *int_array = 0;

为了分配和重新分配数组,我现在的想法是使用如下代码:

#define size_overflow(meta_size, obj_size, len) \
  ((SIZE_MAX - meta_size) / obj_size < len)

// Always free if we cannot reallocate
void *realloc_dynarray_mem(void *p,
                           size_t meta_size,
                           size_t obj_size,
                           size_t new_len)
{
  if (size_overflow(meta_size, obj_size, new_len))
    goto abort;

  struct da_meta *new_da =
    realloc(p, meta_size + obj_size * new_len);
  if (!new_da) goto abort;

  new_da->size = new_len;
  new_da->used = MIN(new_da->used, new_len);

  return new_da;

abort:
  free(p);
  return 0;
}

该函数获取结构体的大小(不含数据对象)、单个对象的大小以及要为其分配内存的对象数。我不使用struct meta 类型的大小,因为它可能太小,具体取决于数据对象的对齐方式,但我将从sizeof 具体(类型化)结构中获取它。如果我无法分配,该函数将始终释放输入并返回 NULL,因为在我的应用程序中,如果我无法增长数组,我必须放弃,所以我不会尝试保留旧数据以防出现错误。

据我所知,这段代码没有任何问题。我总是可以分配内存,只要我有超过struct meta的大小,我就可以在那里设置变量。但是当我返回结果并将其用作dynarray(T) 类型时,我不太确定。我认为它应该可以工作,因为 C 应该首先将结构的第一个成员的内存放在结构中,这就是我放置 struct meta 的地方,但我在这里吗?

我像这样创建一个新数组:

void *new_dynarray_mem(size_t meta_size,
                       size_t obj_size,
                       size_t len)
{
  struct da_meta *array =
    realloc_dynarray_mem(0, meta_size, obj_size, len);
  if (array) {
    // we do set size in realloc, but
    array->size = len;
    // if used was not initialised in realloc (and it wasn't)
    // then we have to set it here...
    array->used = 0;
  }
  return array;
}

#define new_da(type, init_size)                  \
  new_dynarray_mem(sizeof(dynarray(type)),       \
                   sizeof(type), init_size)

这里,宏 new_da()sizeof(dynarray(type)) 获取标头/元信息的大小,从 sizeof(type) 获取底层类型的大小。第二个值很好,但我也不确定第一个。 C 标准是否保证如果我使用完全相同的代码创建两个不同的结构,例如,调用 dynarray(int) 两次,我得到相同的内存布局?我无法想象一个编译器会为相同的代码提供不同的布局,但是在想象编译器能做什么时,我非常有限。

对于附加到数组,我认为一切都很好。在那里我不生成新类型,而是从现有的动态数组中获取大小,所以如果第一个分配符合标准,那么我认为附加也是如此,但我可能是错的。

#define da_free(da)                              \
  do { free(da); da = 0; } while(0)

#define grow(size)                               \
  (((size) == 0) ? /* special case for zero */   \
    1 :                                          \
    ((size) > SIZE_MAX / 2) ? /* can we grow? */ \
      0 : /* no, then report size zero */        \
      (2 * (size))) /* double the size */

#define da_append(da, ...)                      \
do {                                            \
  if (da->meta.used == da->meta.size) {         \
    size_t new_size = grow(da->meta.size);      \
    if (new_size == 0) { da_free(da); break; }  \
    da = realloc_dynarray_mem(                  \
      da, sizeof *da, *da->data, new_size       \
    );                                          \
    if (!da) break;                             \
  }                                             \
  da->data[da->meta.used++] = __VA_ARGS__;      \
} while (0)

我是否保证如果我在结构顶部布置带有元信息的具体动态数组,那么我可以将分配内存同时视为指向元信息和数组的指针?如果我两次生成相同的结构,假设我得到相同的大小和内存布局是否安全?我觉得它必须是那样的,因为它不应该与我包含两次相同的头文件不同,但是由于我正在生成代码,所以可能会丢失一些东西。

编辑基于 cmets,我已将代码更新为下面的代码,但我保留了原始代码(当然),因此 cmets 在这方面是有意义的。

#define da_at(da,i)  (da->data[(i)])
#define da_len(da)   (da->meta.used)

struct da_meta {
  size_t size;
  size_t used;
};

#define dynarr(TYPE)                   \
struct {                               \
  struct da_meta meta;                 \
  TYPE   data[];                       \
}

// Always free if we cannot reallocate
void *realloc_dynarray_mem(struct da_meta *p,
                           size_t meta_size,
                           size_t obj_size,
                           size_t new_len)
{
  // Size size overflow?
  if (((SIZE_MAX - meta_size) / obj_size < new_len))
    goto fail;

  struct da_meta *new_da =
    realloc(p, meta_size + obj_size * new_len);
  if (!new_da) goto fail;

  new_da->size = new_len;
  new_da->used = MIN(new_da->used, new_len);

  return new_da;

fail:
  free(p);
  return 0;
}

void *new_dynarray_mem(size_t meta_size,
                       size_t obj_size,
                       size_t len)
{
  struct da_meta *array =
    realloc_dynarray_mem(0, meta_size, obj_size, len);
  if (array) array->used = 0;
  return array;
}

void *grow_dynarray_mem(struct da_meta *p,
                        size_t meta_size,
                        size_t obj_size)
{
  // Can we double the length?
  size_t used = meta_size - obj_size * p->size;
  size_t adding = MAX(1, p->size);
  if ((SIZE_MAX - used) / obj_size < adding) {
    free(p);
    return 0;
  }

  return realloc_dynarray_mem(
    p, meta_size, obj_size, p->size + adding
  );
}

#define new_da(da, init_size)                    \
  new_dynarray_mem(sizeof *(da),                 \
                   sizeof *(da)->data,           \
                   (init_size))

#define da_free(da)                              \
  do { free(da); da = 0; } while(0)

#define da_append(da, ...)                       \
do {                                             \
  if (da->meta.used == da->meta.size) {          \
    da = grow_dynarray_mem(                      \
      (struct da_meta *)da,                      \
      sizeof *da, sizeof *da->data               \
    );                                           \
    if (!da) break;                              \
  }                                              \
  da->data[da->meta.used++] = __VA_ARGS__;       \
} while (0)

使用时,代码可以是这样的:

int main(void)
{
  dynarr(int) *int_array = new_da(int_array, 0);
  if (!int_array) goto error;
  printf("%zu out of %zu\n",
         int_array->meta.used,
         int_array->meta.size);

  for (int i = 0; i < 5; i++) {
    da_append(int_array, i);
    if (!int_array) goto error;
  }

  for (int i = 0; i < da_len(int_array); i++) {
    printf("%d ", da_at(int_array, i));
  }
  printf("\n");

  da_free(int_array);

  return 0;

error:
  return 1;
}

【问题讨论】:

  • 按照rkoucha.fr/tech_corner/c_preprocessor.html(c 预处理器最佳实践)中的建议,在 da_append()、size_overflow() 中的参数周围使用括号
  • 在realloc_dynarray_mem()的代码的“abort”部分,“p”可能为NULL。即使 GLIBC 接受 NULL 作为 free() 的参数,我也会在调用 free() 之前检查 p。
  • 括号是正确的,尽管我认为如果 da 无论如何都不是变量,则 append 会中断。对于 free(),标准至少从 C99 开始就允许 NULL。
  • Even if GLIBC accepts NULL as parameter for free() 任何免费的都接受 NULL。
  • 在宏中定义变量通常不是一个好主意,因为变量名称可能与定义的其他变量(本地或全局)冲突。因此,我不会在 da_append() 中使用“new_size”变量,而是在 realloc_dynarray_mem() 函数中检查/计算 new_size。

标签: c standards


【解决方案1】:

只要记住元数据和数组开头之间的填充以及对齐要求就可以了。

因为 C 应该将结构的第一个成员的内存放在结构中的第一个位置,这就是我放置 struct meta 的地方,但我在这里吗?

是的。

我是否可以保证,如果我在结构顶部布置带有元信息的具体动态数组,那么我可以将分配内存视为指向元信息的指针

是的,而且……

还有数组?

没有。数组从元 + 填充后的地址开始。所以在地址(char*)da + sizeof(dynarray(TYPE)) 或只是da-&gt;data

如果我生成两次相同的结构,是否可以假设我得到相同的大小和内存布局?

不,是的。有很多关于该主题的other great stackoverflow questions and answers - 研究它们。务实地说,这将是一个奇怪的编译器,它会为相同的结构生成不同的填充,但从技术上讲,这是允许的。


使用灵活的数组

除非您有特定的目标,否则我建议您不要使用它们。它使您更难编写代码。这使得创建和管理此类数组的数组非常困难

转到中止;

goto 标签的名字多么不幸 - abort() 是一个标准函数。

#define 增长(大小)

请为所有库函数使用前缀,尤其是宏。定义这样的宏将无法在碰巧使用不同grow() 函数的其他代码中使用它。 da_ 似乎是一个不错的前缀。

我猜realloc_dynarray_mem 中的*da-&gt;data 应该是sizeof(*da-&gt;data)

@编辑

【讨论】:

  • 大部分我都同意,但为什么 da 不附加释放?这就是为什么我要重新分配功能。如果 realloc() 返回 NULL,我会显式释放参数。
  • 哦,就是这样!我的错。所以它只是在分配错误时将da 设置为0?好的我明白了。所以用户必须if (array == 0) { handle_error() } ok。
  • 是的,我们的想法是这样报告错误。我有另一种解决方案,可以保留内存,以便用户仍然可以获取它,但错误处理更复杂,我不需要它,所以我把它改成了这个。
【解决方案2】:

我建议在 new_da() 中使用 typeof 关键字。这将避免两次指定类型:在 dynarray(TYPE) 和 new_da(type, init_size) 中。要做到这一点,而不是传递类型,只需传递动态数组上的指针:

#define new_da(da, init_size)            \
  (da) = new_dynarray_mem(sizeof(dynarray(typeof(*(da)))),  \
              sizeof(typeof((da)->data[0])), (init_size))

因此,这将避免定义中使用的类型与分配中使用的类型不同的错误:

dynarray(int) *pInt;
pInt = new_da(char, 1024);

评论中讨论的更新:

那么定义和初始化单个宏呢?

#define new_da(da, type, init_size) \
        dynarray(type) *da = new_dynarray_mem(sizeof(dynarray(type)), sizeof(type), init_size)

【讨论】:

  • 我想将定义和初始化结合起来,所以dynarray(int) *da = new_da(int,10);,这就是为什么我没有在宏中添加da。无论如何,如果我这样做了,我就不需要typeof()(这是一个编译器扩展,不在标准中)。我可以像在附加代码中那样写new_dynarray_mem(sizeof *(da), sizeof *(da)-&gt;data, (init_size))。一旦我有了某种类型的东西,我就不需要获取该类型,除非我想定义另一个该类型的变量。我总能得到sizeof
  • 我明白,但作为一个通用库,用户可以像我的例子一样使用它。因此,出于稳健性目的,您不应提供犯错的机会。
  • 此外,如果你用“dynarray(int) *da = new_da(int,10);”限制你的数组的分配,你如何将“da”定义为全局变量?需要吗?有一些技巧可以做到,但乍一看并不简单......编译器不会在全局范围内接受这个构造。
  • 如果他写这样的代码,他会从一个未初始化的指针开始,这同样是个问题。我不高兴您需要提供两次类型来定义数组,但我更不喜欢其他代码。尤其是因为它使编写诸如new_da(da, 10) ; new_da(da, 10) 之类的泄漏内存的代码变得容易。其他操作使数组保持一致状态,如果它们无法处理内存分配,则将其释放。如果new_da() 分配给指针,它应该首先释放它(这意味着它必须在定义时初始化为NULL),否则很容易导致泄漏。
  • 对不起,但我可以看到另一个弱点:编译器将接受而不会引发任何错误:dynarray(int) *pInt = new_da(char *, 1024);
猜你喜欢
  • 2019-07-19
  • 1970-01-01
  • 2020-08-23
  • 1970-01-01
  • 2021-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多