目录
前言
在上一篇文章Vector详解中,我们对vector有了一个较为完整的认识,而要更深入的了解vector,知道他的一些特性和细节,就需要通过模拟实现,让我们的认知体系更加完整。
vector基础定义
首先,我们要知道,vector一个元素中拥有哪些数据,显然,在经过vector的学习后,我们大致能猜出,vector拥有_start(指向元素开始位置),_end(指向元素结束位置),_endofstroage(capacity,表长度),同时,vector因为要满足不同类型可以使用,所以我们要用类模板来进行定义
template<class T>
class vector
{
public:
vector()
:_start(nullptr)
,_end(nullptr)
,_endofstorage(nullptr)
{}
private:
iterator _start;
iterator _end;
iterator _endofstorage;
}
基础定义好后,我们可以开始定义构造函数
Vector构造函数
普通构造函数
在上面的代码中,我们其实已经成功定义了一个普通构造函数,但是,不难看出,vector的构造函数不止一个,如果每个构造函数都要像这样写初始化列表,会让代码很冗余,所以,我们可以直接在私有成员给缺省值,就不用初始化列表了,那不用了上面的构造函数啥也没写要不要删?不能删,构造函数写了系统不会生成,不写才会生成默认的,下面要写的拷贝构造都算构造,所以这个不能删,他也是干了事情的
template<class T>
class vector
{
public:
vector()
{}
private:
iterator _start=nullptr;
iterator _end=nullptr;
iterator _endofstorage=nullptr;
}
n个val初始化构造函数
这个构造函数是通过直接扩容尾插来进行的,比如在末尾插入10个0,要注意的是,参数T& val = T(),这里缺省值要给匿名变量,因为如果给0,就会限定死范围,不能够满足其他类型的使用,当然,也可以用const T& val的写法,因为匿名对象具有常性,且加const后,匿名对象T()的生命周期延长到使用完val。(缺省值调用默认构造函数,但是内置类型比如int没有构造函数,但是模板出现后进行了升级,可以认为内置类型有)
vector(size_t n, T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
迭代器构造
要写迭代器构造,我们需要用模板定义一个InputIterator类型,因为如果直接使用迭代器的话,会导致这个构造函数只能用于初始化vector类型,其他类型不能使用,为了能够满足多种需求,我们需要通过类模板来实现,我们也要知道,在一个类模板里面也可以写模板函数。
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
first++;
}
}
写完后测试运行时我们会发现出了问题:vector<int> v1 = (10, 0)居然报错了?而错误原因追溯到了迭代器构造函数,这是为什么?这种写法的初始化不应该转到n个val的构造函数吗?原来,上面的模板函数可以实例化出int int,而对于vector<int>来说是最匹配的,所以他会优先选择最匹配的,就不会到val初始化构造函数了,这也就导致了报错,且原因追溯到了迭代器构造函数,那有没有什么解决办法?再写一个int类的val的构造函数,让他直接匹配走到这里就可以了
vector(size_t n, T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n, int& val = int())
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
拷贝构造函数
拷贝构造函数有传统和现代写法
传统写法:通过开辟一个新的空间,进行遍历赋值来深拷贝,这种方法可以用reserve和push_back复用达到效果:
vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
}
现代写法:直接用迭代区间初始化去构造临时对象tmp ,然后让tmp和对象进行交换
vector(const vector<T>& v)
{
vector<T> tmp(v.begin(), v.end());
swap(tmp);
}
当然,写现代写法就需要再写一个swap
swap:
void swap(vector<T> tmp)
{
std::swap(_start, tmp._start);
std::swap(_end, tmp._end);
std::swap(_endofstorage, tmp._endofstorage);
}
swap写好后,我们可以顺便将赋值重载运算符写出来,这里要实现深拷贝:
赋值重载运算符:
//v3 = v1
//过程:要先进拷贝构造,因为v1传给tmp是拷贝
vector<T>& operator=(vector<T> tmp)
{
swap(tmp);
return *this;
}
Vector析构函数
关于析构函数,我们可以先释放掉_start,然后让_start直接给值给其他两个变量
~vector()
{
delete[] _start;
_start = _end = _endofstorage;
}
Vector成员函数
迭代器实现
vector类似顺序表,迭代器使用原生指针进行遍历,解引用等操作,同时迭代器拥有普通迭代器和const迭代器两种(注意const迭代器是只读,迭代器可变,指向的内容不变,const迭代器对应的是const T*)
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
//迭代器想给别人用就要放在public
private:
iterator _start=nullptr;
iterator _end=nullptr;
iterator _endofstorage=nullptr;
}
定义好后,我们可以用它把begin和end写出来:
iterator begin()
{
return _start;
}
iterator end()
{
return _end;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _end;
}
这样,一个简单的迭代器就做好了
size,capacity函数
这两个函数很简单,让他们返回相应的私有成员就好了
size:
size_t size() const
{
return _end - _start;
}
capacity:
size_t capacity() const
{
return _endofstorage - _start;
}
对于末尾的const,如果不加的话,const类型调用函数会出现错误
reserve函数
reserve扩容函数,首先我们要检查要扩容的n是否比capacity大,小的话不用扩(注意这里扩的是capacity,所以算endofstorage就用n了),然后new一个新的空间,并判断start是否有空间,有就拷贝销毁
void reserve(size_t n)
{
if (n > capacity())
{
int sz = size();
T* tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;
}
_start = tmp;
_end = _start + sz;
_endofstorage = _start + n;
}
}
写好后,这里的memcpy其实会导致一个比较严重的错误,具体是什么,我们可以通过举例来看看
举例
void test2()
{
vector<string> v1;
v1.push_back("111111");
v1.push_back("111111");
v1.push_back("111111");
v1.push_back("111111");
v1.push_back("111111");
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
我们会发现,如果只有前四个就可以顺利打印,如果大于四个就会打印错误,这是因为浅拷贝的问题,大于四个数据就会进行扩容,在扩容时,我们会创建新的空间,并拷贝删除原来的空间,而这里使用的memcpy,是浅拷贝,通过监视我们可以发现,tmp创建后,memcpy是浅拷贝,会让tmp中的每个元素的地址和数据与_start中的一样,也就是说,进行memcpy后,delete掉_start时,会把浅拷贝的元素全部delete掉,这就导致tmp中的元素出现问题,找不到对象,也就会打印如烫烫烫的错误,怎么解决?
当然在这里不能用memcpy进行操作,因为我们已经创建了_start和tmp两段空间,所以直接进行赋值就可以了,那我们能直接使用拷贝构造吗,不能,库里面的是用的拷贝构造,因为库里的函数并没有用new直接创建对象,他类似于malloc是从内存池来的,并没有初始化,所以用定位new调用拷贝构造进行初始化,而这里已经创建好了两个对象进行了初始化
所以,经过改良后的reserve函数应该是这样子的
void reserve(size_t n)
{
if (n > capacity())
{
int sz = size();
T* tmp = new T[n];
if (_start)
{
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_end = _start + sz;
_endofstorage = _start + n;
}
}
resize函数
resize函数可以改变size值,或者进行扩容改变capacity值,缩容扩容赋值他都做得到。
当n < size()时,进行缩容,改变_end的值即可,n > size()时,调用reserve函数进行扩容,并赋值
void resize(size_t n, T val = T())
{
if (n <= size())
{
_end = _start + n;
}
else
{
reserve(n);
while (_end < _start + n)
{
*_end = val;
_end++;
}
}
}
关于T val = T()的问题在上文n个val初始化构造处已有阐述,这里不做解释
push_back函数
即尾插函数,逻辑上来讲,我们要先检查扩容,然后赋值进去
void push_back(const T& x)
{
if (_end == _endofstorage)
{
size_t capacity_s = capacity() == 0 ? 4 : capacity() * 2;
T* tmp = new T[capacity_s];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;
}
_start = tmp;
_end = _start + size();
_endofstorage = _start + capacity_s;
}
*_end = x;
++_end;
}
现在的代码好像在逻辑和语法上看起来都没有问题,但是一经测试我们发现又出了问题:假如成员都没有值,新建后_end竟没有给值,也就是_start = tmp; _end = _start + size();处出现了错误,原因是什么?因为这里size()是_end - _start,end是旧数据,start是新数据,相减的话减不出来,那是不是换个位置,让start = tmp放后面行不行?也不行,这时start是0,size()减出来的也是0,所以可以给个sz,先把size()存起来使用,就可以避免直接调用出现问题
void push_back(const T& x)
{
if (_end == _endofstorage)
{
size_t sz = size();
size_t capacity_s = capacity() == 0 ? 4 : capacity() * 2;
T* tmp = new T[capacity_s];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;
}
_start = tmp;
_end = _start + sz;
_endofstorage = _start + capacity_s;
}
*_end = x;
++_end;
}
理解了push_back的底层后,我们可以进一步简化,我们已经实现了reserve函数,那是否可以将reserve函数进行复用,节省代码呢?
void push_back(const T& x)
{
if (_end == _endofstorage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_end = x;
++_end;
}
insert,erase函数
插入,删除函数,他们通过pos找到对应的位置,进行操作。在vector的实现当中,我们可以用迭代器帮助实现:
实现insert的逻辑是:insert一般是在pos位置的上一个插入,也就是说,我们需要挪动数据,我们可以创建一个end,让他从尾开始遍历,让从pos位置开始的数据都往后挪动一位,当end移动到pos之前时,说明pos位置的值已经被移动,现在pos位置就没有值了,那么我们就可以填入x,达到inset的目的
insert:
void insert(iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _end);//可以等于,等于了就相当于尾插
//检查是否需要扩容
if (_end == _endofstorage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
iterator end_s = _end - 1;
while (end_s >= pos)
{
*(end_s + 1) = *(end_s);
end_s--;
}
*pos = x;
++_end;
}
这里有一个问题:头插需不需要单独处理?答案是不需要,因为pos的值不可能为0,因为pos是一个迭代器,我们知道,这里的迭代器是用的原生指针重定义的,就算用原生指针,一个原生指针的值不能为0,为0的话就变成空指针了
erase:
void erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _end);
iterator it = pos + 1;
while (it < _end)
{
*(it - 1) = *it;
it++;
}
_end--;
}
到这里,我们的重量级来了:迭代器失效问题。迭代器失效是指:如果失效就不能再用这个迭代器,如果使用了,结果是未定义的。insert暂且不谈,当我们在测试erase时,会发现有些情况下他不能删除数据,比如,现在想要找到一串数据中的偶数并删除
void test2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(2);
v.push_back(4);
v.push_back(5);
v.push_back(6);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
v.erase(it);
}
else
it++;
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
我们会发现,erase的操作方法是移动覆盖,然后pos++,如果有连续的偶数,删除一个后移动,而移动过来的是一个偶数,这个偶数并没有进行判断,pos会直接++,导致删不完的情况。又如果数据的最后一个数是偶数,进行删除需要移动数据,下一个数据是空的,也是_end的位置,移动后pos指向_end,而_end--,会造成越界的问题,这些都是迭代器失效。
那有没有什么解决方法?在库中erase是iterator类型,他会返回被删除数据的下一个位置,也就是说,如果让it去接收返回值就不会造成迭代器失效,当然,这个是库中的erase的用法,所以,我们也要把类型改为iterator,让他返回下一个位置的数据
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _end);
iterator it = pos + 1;
while (it < _end)
{
*(it - 1) = *it;
it++;
}
_end--;
return pos;
}
void test2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(2);
v.push_back(4);
v.push_back(5);
v.push_back(6);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
it = v.erase(it);
}
else
it++;
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
现在erase就避免了迭代器失效的问题,现在我们转回insert,insert也有迭代器失效的问题:pos野指针问题,我们知道,insert可以复用到尾插,而当在尾插或者任何需要扩容的场景时,会出现pos失效的问题,因为pos位置指向扩容前的空间的位置,我们的扩容是创建一个新的空间,然后拷贝删除原来的空间,在这个操作下,pos指针并没有发生变化,可以形象的理解为:搬家了但是没告诉你新家在哪儿,你还是只有原来的家的情报,这时候pos就变成了野指针,所以想要扩容后正常使用pos找到对应位置,可以在扩容操作时保存pos对于start的相对位置,扩容后用新start加上相对位置,就得到了扩容后的pos的位置
void insert(iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _end);//可以等于,等于了就相当于尾插
//检查是否需要扩容
if (_end == _endofstorage)
{
int position = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + position;
}
iterator end_s = _end - 1;
while (end_s >= pos)
{
*(end_s + 1) = *(end_s);
end_s--;
}
*pos = x;
++_end;
}
现在代码经过改良,应该是没有问题了,那我们再来进行测试:
void test2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(1);
v.push_back(1);
v.push_back(1);
v.push_back(1);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
vector<int>::iterator it = v.begin() + 2;
v.insert(it, 10);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
又又又出现错误了:这里使用一次后it就失效了,显然这是迭代器失效的问题,为什么?insert的代码中不是改了吗,为什么还会有这个问题?仔细观察,这里it是传值,pos是it的拷贝,pos的改变当然不影响it,那加个引用可不可以?也不行,这样一来v.insert(v.begin() + 2, 30);就用不了了,因为这里的v.begin() + 2是一个临时变量值,临时变量具有常性,引用必须传变量,加了引用就不行了,那再在前面加一个const?还是不行,加了const虽然v.begin() + 2能进来了,但是pos就改变不了了,所以insert的参数类型选择了iterator pos,且insert使用一次后pos可能会失效,要谨慎使用
其实解决了内部pos空指针的问题后,我们可以将insert复用到push_back中,让他进一步简化:
void push_back(const T& x)
{
insert(end(), x);
}
运算符重载
这里是重载了[]运算符,让[]可以做到解引用
T& operator[](size_t po)
{
assert(po < size());
return _start[po];
}
const T& operator[](size_t po) const
{
assert(po < size());
return _start[po];
}
//这里也需要写一个const的[],因为*it不可以改变
总结
到这里,vector的核心函数和内容都已经模拟实现完了,学习容器的时候,务必要进行模拟实现的步骤,这样可以帮助自己理解容器,更好的学习!