C++动态数组的简易实现

C++动态数组的简易实现

​ 在啃过 STL源码剖析的vector这一章后,我准备自己写一个动态数组。因为在STL中的vector为了防止频繁的发生,添加元素->配置空间->移动元素->释放原空间,于是采用类似缓冲池的技术,减少空间配置的次数。这个技术就是 size + capacity 。下面举个例子,假如我们正常使用动态数组怎么改变

//使用动态数组的步骤

int* iptr = new int[10];

//插入元素 1
//1、创建新空间
int* tmp = new int[11];
//2、将旧元素移到新空间
for(int i=0;i<10;++i)
    tmp[i] = iptr[i];
//3、插入元素赋值
tmp[10]=1;

//4、释放旧元素
delete[] iptr;

//5、改变指向
iptr = tmp;

​ 可以看到我们繁琐的步骤,导致每插入一个元素就要进行一次内存配置操作,非常的麻烦,且进行一次malloc代价很大。这个效果在数组越大的时候体现的越明显。这个时候STL就引入了容量(预备空间)来作为缓冲区,其实也就是一个预备空间。只有当我们预备空间满了才会进行元素拷贝和移动。

​ ps:STL中 vector 有空间配置器,底层使用的是realloc、memcpy、placement-new ,所以较之于我自己实现的版本效率更高,同时可以减少元素移动的频率,只有realloc调用失败,才开始移动元素

​ 动态数组实现如下:

#define INITSIZE 8


//要求数据元素是遵循严格弱序的
/*
	设计时,把错误处理留给调用者,在设计算法是不考虑外部的调用是否合理(如边界问题,由调用者处理)
*/
template<typename T>
class Vector
{
public:
	Vector();
	Vector(size_t, const T& tmp);
	~Vector();
	Vector<T>& operator=(const Vector& data);
public:
	size_t size() { return _size; };		//返回size
	bool empty() { return !_size; };
	void clear();					//清空

	//增删改查
	T& operator[](size_t index);	//改
	void push_back(T& data);		//增
	void push_back(T&& data);
	size_t earse(size_t indexl,size_t indexr);	//数组范围删除
	size_t insert(size_t index,const T& data);	//元素随机插入
	void pop();						//删除尾部元素
	T& search(T& data);				//查:返回第一个匹配元素,方案二:区间范围查找实现全区间范围查找


private:
	void expand();					//扩张
	size_t _size;
	size_t _capacity;
	T* _array;			//底层数组

};

容量扩张

​ 缓冲如何实现?就是通过容量,默认容量为8,容量就是底层数组实际大小,当添加元素导致容量满,就调用expand进行构造新空间,元素移动操作等

template<typename T>
void Vector<T>::expand()
{
    //1、创建新数组,新数组容量为原来的两倍,即数组下次扩张更加遥远。可以减少移动次数,但同时增加空间上的维护成本
    //解决方法有:实现一个缩容方法,一旦size和capacity相差过多,则进行缩容,如果底层是realloc实现效率更高
	T* tmp = new T[_capacity = _capacity << 1];
	
	for (int i = 0; i < _size; ++i)	//把原数组中所有元素拷贝到新数组
	{
		tmp[i] = _array[i];
	}
	delete[]_array;
	_array = tmp;
}

//提供一种实现,这个接口,只需要放在删除操作前
template<typename T>
void shink()
{
    if(_size-1 <= _capacity>>2)
        return;
    //否则缩容
    T* tmp = new T[_capacity = _capacity >> 1];	
	
	for (int i = 0; i < _size; ++i)	//把原数组中所有元素拷贝到新数组
	{
		tmp[i] = _array[i];
	}
	delete[]_array;
	_array = tmp;
}

插入操作

​ insert接口实现如下:

template<typename T>
size_t Vector<T>::insert(size_t index, const T& data)
{
	if (_size+1 >=_capacity) expand();	//如果容量不足
	for (int i = _size; i < index; --i)
	{	
		_array[i] = _array[i - 1];
	}
	_array[index] = data;	//改变目标元素
	++_size;
	return index;	//成功返回下标
}

删除

​ 删除操作也是一个亮点,这也是size + capacity 结构带来的好处。被删除元素完全不需要管理,直接覆盖掉,然后改变 size 即可。

template<typename T>
size_t Vector<T>::earse(size_t indexl, size_t indexr)
{
	if (indexl > indexr) return -1;	//左下标小于右下标
	if (indexl >= _size && indexr >= _size) return -1;	//左、右下标不能等于数组大小

	//接下来就是不越界的情况
	//删除元素,采用吧后续元素前移的方法:时间复杂度为 o(n)、空间复杂度为 o(1);
	int n = 1 + indexr - indexl;
	while (indexl+n <_size)
	{
		_array[indexl] = _array[indexl + n];
	}
	_size -= n;
	return indexl;	//返回删除元素第一个位置下标
}

​ ps:秉承oop思想,类似这种接口,底层都要有一个基础实现,比如push_bach、pop_back都是insert和earse的一个特化实现。所以想要仔细实现一个这样的接口一定要有一个泛化的接口,在简单转发一下成为多个特化的接口。

重载[ ]运算符

​ 重载下标运算符,是为了匹配使用习惯

template<typename T>
T& Vector<T>::operator[](size_t index)
{
	return _array[index];	//直接返回
}

查找

​ 这里就是应该实现一个 search(int start, int end, const T& data ); 这样一个接口,可以参考C++中的algorithm库,基本上都是基于一个范围操作接口,进行特化成不同接口。其实都是语法糖罢了。

//对于无序数组,只能遍历了。但是有序数组就有很多方法
template<typename T>
T& Vector<T>::search(T& data)
{
	for (int i = 0; i < _size; ++i)
		if (data == _array[i]) 
			return _array[i];
}

//可以提供一个快排的实现:但是我没有添加进入,因为我觉得动态数组不该内置这个接口
//排序也应该基于泛化的思想,实现一个区间排序
template<typename T>
void quicksort(T* array, size_t low, size_t high)
	{
		if (low >= high) return;
		size_t i = low;
		size_t j = high;
		T key = array[i];

		while (i < j)
		{
			while (i < j && array[j] >= key)	--j;

			if (i < j) array[i] = array[j];

			while (i < j && array[i] <= key) ++i;
			if (i < j)	array[j] = array[i];
		}
		array[i] = key;
		quicksort(array, low, i - 1);
		quicksort(array, i + 1, high);
	}

STL中的vector

​ 和我写的区别最大的地方就在于:1、使用了空间配置器;2、使用了迭代器。

​ STL中元素范围是以3个迭代器:

​ 使用迭代器可以支持许多算法,find、find_if、search、sort等。但是最大的作用还是对于边界的检查是便捷的。这就是迭代器使用是我们最常用的的一个条件 != end() ,有助于让我们拜托这该死的边界检查,和下标溢出。

template<class T, class Alloc=alloc>
class vector{
...
protected:
    iterator start;				//使用空间头
    iterator finish;			//使用空间尾
    iterator end_of_storage;	 //未使用空间尾
}

​ STL中许多数据结构都值得我们学习。尤其是基础数据结构中一些理念和思想,可以很好的帮助我们解决一些边界问题,使用一点代价,就可以避免很多边界问题,哦,这也许就是编程的魅力。

猜你喜欢

转载自blog.csdn.net/green_healthy/article/details/121305260
今日推荐