【发布时间】: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。