C語言實現一個泛型的vector


問題描述:

使用純$C$語言實現一個泛型的$vector$,支持拷貝構造和移動構造。

設計方案:

$vector$是動態的數組,因此我們保存$vector$申請的內存塊的指針,此外我們需要兩個$size$_$t$類型的數保存當前開辟的空間和當前已經存有的元素個數。故需要一個我們定義以下的$vector$結構體:

struct vector
{
	T* buf;
	size_t size, capacity;
};

 由於我們設計的是泛型$vector$,$T$的類型不定,所以一個元素占用的內存不定,所以這個內存塊的大小就不能,考慮兩種方案:

方案一、

在$vector$中加入一個$u$_$size$表示元素的大小。

那我們就可以設計成這樣子:

struct vector
{
	T* buf;
	size_t size, capacity;
	size_t u_size;
};

 這樣子$push$_$back$或者是復制$vector$的時候就可以計算出$malloc$的空間是$n*u$_$size$。

但是有兩個問題:

問題一、

考慮這個結構體:

struct T
{
	void* buf;
	size_t val;
};

 在$push_back$的時候,就會有以下問題:

復制前:

復制后:

問題二:

如果我們需要復制這個$vector$,也會有類似的問題:

復制前:

復制后:

顯然這個$buf$存在危險,它會被幾個指針訪問,可能會造成數據的錯誤。(C里面沒有$unique$_$ptr<T>$和$shared$_$ptr<T>$)。

同時,析構的時候,$buf$這一塊內存極易引發內存泄漏或者是重復釋放。

那我們如何改進呢:

改進方案一:

如果在$push$_$back$的時候傳入構造函數的函數指針,如:

$size$_$t$ $push$_$back(vector*$ _$vec,$ $const$ $T* $ _$dat,$ $T*(*copy$_$assign)(const$ $T*$ _$src))$。然后在$push_back$函數中調用$copy$_$assign$,這樣子就可以成功解決上面的問題一。析構的時候也是一樣的道理,傳入析構函數的函數指針即可。在復制和銷毀$vector$的時候,我們也是同樣的傳入這些函數指針。就解決了問題二。

這個方案已經夠好了,但是,它太麻煩了,每次都要傳函數指針。

改進方案二:

我們為什么不把它保存下來呢,然后$vector$不就可以自動調用了嘛?

有一說一,確實。

這樣子我們設計一下這個結構體:

struct vector
{
	T* buf;
	size_t size, capacity;
	size_t u_size;
	void* (*assign)(const void* _src);
	void* (*destroy)(void* _dat);
};

 這樣子就可以了,實際上是空間換時間的策略。

但是如果函數指針多起來了,這個$vector$結構體在初始化這些函數指針的時候,就會非常麻煩,比如說你要加上移動構造函數,或者其他函數的時候。

所以我們就把它封裝一下好了。

struct data_arg
{
	size_t u_size;
	void* (*assign)(const void* _src);
	void* (*destroy)(void* _dat);
};
struct vector
{
	T* buf;
	size_t size, capacity;
	data_arg dat_arg;
};

 這樣子需要修改函數指針的時候直接修改$dat$_$arg$就可以了。

方案二:

我們使用一個$void*$去指向這些元素,這樣子無論元素是什么是什么類型的我們都可以指向它。

我們設計以下的結構體:

struct vector
{
	void** buf;
	size_t size, capacity;
};

 顯然,方案一上的兩個問題,方案二依然存在。而且無論如何,復制的時候一樣需要知道元素的大小。

所以我們就集思廣益,把方案一的操作搬下來。

struct vector
{
	void** buf;
	size_t size, capacity;
	data_arg dat_arg;
};

 這樣子我們也解決這兩個問題。

???

$Q$:這兩個方案既然都行,那我們隨便選一個是不是就可以了。

$A$:大多數情況是這樣的。但是我們可以注意到:方案一少了一層指針尋址,效率會更高,但是方案一進行移動構造的時候,它實際上相當於是$memcpy$。而方案二雖然需要進行多一層尋址,但是移動的時候可以直接把指向這個元素的指針賦給$vector$。但是其他情況仍需重寫函數。所以根據應用場景靈活選擇方案即可。

接下來的舉例實現使用第二種方案。

!!!

那我們開始實現吧:

結構體定義,不說了。

struct data_arg
{
	size_t u_size;
	void* (*assign)(const void* _src);
	void* (*destroy)(void* _dat);
};
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$,故設計。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM