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