问题描述:
使用纯$C$语言实现一个泛型的$vector$,支持拷贝构造和移动构造。
设计方案:
$vector$是动态的数组,因此我们保存$vector$申请的内存块的指针,此外我们需要两个$size$_$t$类型的数保存当前开辟的空间和当前已经存有的元素个数。故需要一个我们定义以下的$vector$结构体:
struct vector { T* buf; size_t size, capacity; };
由于我们设计的是泛型$vector$,$T$的类型不定,所以一个元素占用的内存不定,所以这个内存块的大小就不能,考虑两种方案:
$1.$在$vector$结构体中增加一个$element$_$size$,创建$vector$时传入元素的大小,然后申请内存块时候就可以自动计算。
$2.$使用$void*$直接指向这个元素,这样子就完全不需要考虑元素的大小,因为指针的大小是固定的。
第一种方案实现简单,但是如果需要复制整个$vector$,且元素是一个结构体且里面含有指针(下图左),就会出现两个$vector$的同一个位置的元素指向同一个数据块(下图右)。
复制前:
复制后:
如果在另一个$vector$中修改了$dat$,然后在另一个$vector$又访问$dat$就可能会出错。同时,这样的数据在$push_back$时,需要手动拷贝或者传参,比较麻烦。同时析构的时候也比较麻烦,需要手动传参。而在每个结构体中保存拷贝构造函数和析构函数又会使得很多空间被浪费。
改进:在$vector$结构体中保存元素的拷贝构造函数和析构函数组,然后再用这种方案。
很棒!盲生你发现了华点。你会惊喜的发现,这样子就不能实现移动构造了。
第二种方案使用$void*$,然后指向这个数据元素。但是这样子对于有指针的结构体,仍然有这种问题,我们参照上面改进方案一的思路,加上元素占用空间,拷贝构造函数和析构函数这个问题就解决了,而且实现移动构造的时候直接把数据元素指针赋值给$vector$的某一个位置即可。
啪!看起来完美。
那我们开始实现吧:
首先数据元素的拷贝构造函数和析构函数散装放到$vector$多难看,我们封装一下(名字我乱取的):
struct data_arg { size_t u_size; void* (*assign)(const void* _src); void* (*destroy)(void* _dat); };
然后我们按照上面的方案二设计出$vector$结构体:
struct vector { void** buf; size_t size, capacity; data_arg dat_arg; };
然后我们开始设计函数:
(注:$catch$_$exec$是我的项目的一个异常处理的库,看官自行忽略就好)
首先是创建:
vector* vec_init(data_arg _dat_arg) { vector* ptr = (vector*)malloc(sizeof(vector)); *ptr = { (void**)malloc(sizeof(void*) * vec_init_size), 0,vec_init_size,_dat_arg }; return ptr; }
比较简单,不说了。
然后实现调用数据的拷贝构造函数和析构函数的函数:
void* vec_new_data(vector* _vec, const void* _dat) { if (!_vec->dat_arg.assign) { void* dst = malloc(_vec->dat_arg.u_size); memcpy(dst, _dat, _vec->dat_arg.u_size); return dst; } return _vec->dat_arg.assign(_dat); } void* vec_delete_data(vector* _vec, void* _dat) { if (!_vec->dat_arg.destroy) free(_dat); else _vec->dat_arg.destroy(_dat); return NULL; }
注:我自己的设计是如果是不含指针的结构体,就直接复制内存,这样子拷贝构造函数和析构函数指针都是$NULL$,效率高一点。
然后是拷贝构造函数:
void* vec_assign(const void* _vec) { if (!_vec) catch_exce(6); vector* vec = (vector*)_vec; vector* newvec = vec_init(vec->dat_arg); vec_resize(newvec, vec->capacity); newvec->size = vec->size; for (int i = 0; i < newvec->size; ++i) newvec->buf[i] = vec->dat_arg.assign(vec->buf[i]); return newvec; }
然后是清空和析构函数二人组:
void vec_clear(vector* _vec) { if (!_vec) catch_exce(6); for (int i = 0; i < _vec->size; ++i) _vec->buf[i] = vec_delete_data(_vec, _vec->buf[i]); } void* vec_destroy(void* _vec) { if (!_vec) catch_exce(6); vector* vec = (vector*)_vec; vec_clear(vec); free(vec->buf); free(vec); return NULL; }
然后剩下的功能自己实现一下就完事了。
检查容量:
void vec_check_capacity(vector* _vec) { if (!_vec) catch_exce(6); if (_vec->size == _vec->capacity) vec_resize(_vec, _vec->capacity << 1); }
重设容量:
size_t vec_resize(vector* _vec, size_t _size) { if (!_vec) catch_exce(6); void** tmp = (void**)realloc(_vec->buf, sizeof(void*) * _size); if (!tmp) catch_exce(6); _vec->buf = tmp, _vec->capacity = _size; return _vec->capacity; }
$push$_$back$(拷贝),$push$_$back$_$no$_$copy$(移动)
size_t vec_push_back(vector* _vec, void* _dat) { if (!_vec) catch_exce(6); if (!_dat) catch_exce(7); vec_check_capacity(_vec); _vec->buf[_vec->size++] = vec_new_data(_vec, _dat); return _vec->size; } size_t vec_push_back_no_copy(vector* _vec, void* _dat) { if (!_vec) catch_exce(6); if (!_dat) catch_exce(7); vec_check_capacity(_vec); _vec->buf[_vec->size++] = _dat; return _vec->size; }
$pop$_$back$(析构),$pop$_$back$_$no$_$delete$(不析构)
size_t vec_pop_back(vector* _vec) { if (!_vec) catch_exce(6); _vec->buf[--_vec->size] = vec_delete_data(_vec, _vec->buf[_vec->size]); return _vec->size; } size_t vec_pop_back_no_delete(vector* _vec) { if (!_vec) catch_exce(6); _vec->buf[--_vec->size] = NULL; return _vec->size; }
注:没有检查边界,看官自己加上。
感谢大家!
全部源代码看这里。这是我自己用纯$C$语言实现的一个$C$语言子集的词法和语法分析器项目,里面需要使用泛型$vector$,故设计。