C语言中实现内存分配无关的array_slice函数的方案问询
我们需要实现一个array_slice函数,它能返回数组片段,同时兼容Boehm垃圾回收、引用计数、内存池、标准alloc/free等多种内存分配策略,且实现方式要符合行业惯例、不刻意生硬。现有代码里malloc的部分需要替换成和输入数组old一致的分配方式,目前已经考虑过让数组结构体携带函数指针及相关信息、使用_Generic两种思路,下面给出更多可行方案及细节:
上下文代码
typedef struct array array; struct array { size_t length; uintptr_t* thing; }; array array_slice(array old, int offset) { size_t new_length = old.length - offset; array new = { .length = new_length, // TODO: 这里需要使用和参数"old"相同的分配策略 .thing = malloc(sizeof(uintptr_t) * new_length) }; size_t j = 0; for (size_t i = offset; i < old.length; i++) { new.thing[j] = old.thing[i]; j++; } return new; }
可行方案建议
1. 为数组结构体绑定分配元数据(最常用的常规方案)
这是C语言里处理多态资源管理的标准做法,扩展数组结构体,把分配策略相关的信息封装进去:
// 定义分配器结构体,包含分配、释放函数和上下文 typedef struct array_allocator { void* (*alloc)(size_t size, void* context); void (*free)(void* ptr, void* context); void* context; // 可以是内存池指针、引用计数控制块等 } array_allocator; // 扩展后的数组结构体 typedef struct array array; struct array { size_t length; uintptr_t* thing; array_allocator allocator; // 携带当前数组的分配策略信息 };
这样array_slice就能直接复用原数组的分配器来创建新数组:
array array_slice(array old, int offset) { size_t new_length = old.length - offset; array new = { .length = new_length, .allocator = old.allocator, .thing = old.allocator.alloc(sizeof(uintptr_t) * new_length, old.allocator.context) }; // 元素拷贝逻辑不变 size_t j = 0; for (size_t i = offset; i < old.length; i++) { new.thing[j] = old.thing[i]; j++; } // 如果是引用计数策略,这里可以按需增加新数组的引用计数 // 比如判断allocator.context的类型后执行对应操作 return new; }
这种方式把分配策略和数组本身绑定,逻辑清晰,维护性强。
2. 编译期多态(_Generic)
如果分配策略在编译阶段就能确定,可以用_Generic实现编译期的分支选择,避免运行时函数指针开销:
首先为数组添加类型标记和对应上下文的联合体:
typedef enum { ARRAY_MALLOC, ARRAY_REFCOUNT, ARRAY_MEMPOOL, ARRAY_BOEHM } array_type; typedef struct array array; struct array { size_t length; uintptr_t* thing; array_type type; union { void* pool; // 内存池指针 int* refcount; // 引用计数指针 } u; };
然后用_Generic封装分配逻辑的选择:
#define array_alloc(arr, size) _Generic((arr), \ array: _array_alloc((arr), (size)) \ ) void* _array_alloc(array old, size_t size) { switch (old.type) { case ARRAY_MALLOC: return malloc(size); case ARRAY_REFCOUNT: { void* ptr = malloc(size); // 假设引用计数和内存块关联,这里增加计数 *(old.u.refcount) += 1; return ptr; } case ARRAY_MEMPOOL: return mempool_alloc(old.u.pool, size); case ARRAY_BOEHM: return GC_MALLOC(size); default: return NULL; } }
在array_slice里直接调用array_alloc(old, sizeof(uintptr_t)*new_length)即可,这种方式性能略高,但仅适用于编译期就能确定分配策略的场景。
3. 分配器作为函数参数传入
如果不想修改原数组结构体,可以把分配函数和上下文作为额外参数传给array_slice:
// 定义分配函数的类型 typedef void* (*array_alloc_func)(size_t size, void* context); array array_slice(array old, int offset, array_alloc_func alloc, void* context) { size_t new_length = old.length - offset; array new = { .length = new_length, .thing = alloc(sizeof(uintptr_t) * new_length, context) }; // 元素拷贝逻辑不变 size_t j = 0; for (size_t i = offset; i < old.length; i++) { new.thing[j] = old.thing[i]; j++; } return new; }
调用时根据原数组的分配策略传入对应的函数和上下文:
// 标准malloc方式调用 array sliced = array_slice(old_arr, 2, malloc, NULL); // 内存池方式调用 array sliced_pool = array_slice(old_arr, 2, mempool_alloc, my_pool);
这种方式灵活性最高,但调用时需要额外传递参数,略显繁琐。
4. 引用计数策略的特殊优化
如果数组元素是引用类型,引用计数场景下可以采用**写时复制(Copy-On-Write)**的思路,避免不必要的内存拷贝:
// 针对引用计数的数组结构体 typedef struct refcount_array { size_t length; uintptr_t* thing; int* refcount; size_t start; // 记录切片的起始偏移 } refcount_array;
切片时不需要拷贝底层内存,只需要创建新的数组结构体,共享原内存并增加引用计数:
refcount_array refcount_array_slice(refcount_array old, int offset) { refcount_array new = { .length = old.length - offset, .thing = old.thing, .refcount = old.refcount, .start = old.start + offset }; // 增加引用计数 *(new.refcount) += 1; return new; }
只有当需要修改切片数组的元素时,才真正拷贝底层内存并创建独立的引用计数。这种方式能大幅减少内存开销和拷贝时间,但仅适用于引用计数策略,需要和其他策略分开处理。
内容的提问来源于stack exchange,提问作者Olle Härstedt

