目录
1.基本结构
在官方的SGI版本的底层代码库中,会发现vector类的会定义三个成员变量
- start--指向数组的第一个数据
- finish--指向数组有效数据的下一个位置
- end_of_storage--指向可放有效数据的下一个位置
这些成员变量的类型会根据vector的类型而改变,所以vector需要定义模板,这些成员变量的类型就是模板指针。
template<class T>
class vector{
private:
iterator _start;
iterator _finish;//有效个数的下一位
iterator _endofstorage;//可放有效个数的下一位
}
拷贝构造
传统写法:将this对象开辟空间,然后拷贝数据
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
_finish = _start + v.size();
_endofstorage = _start + v.capacity();
//memcpy(_start, v._start, sizeof(T)*v.size());//浅拷贝,自定义对象可用
for(size_t i=0;i<v.size();++i)
{
_start[i]=v._start[i];
}
}
现代写法:现代写法会用一个临时变量来拷贝构造形参对象,然后 将临时对象和this对象交换,这样不仅解决了浅拷贝会指向同一块地址的问题,而且tmp对象自动把就空间给释放掉(临时对象出函数作用域销毁)
为什么this的_start不能直接拷贝实参传过来的对象?--因为_start是指针类型,不能直接指向实参传过来的对象,所以要再开辟一块空间。
vector<T> tmp();
这里的临时对象初始化只能被初始化为空,因为只有无参构造。那么怎样才能让临时对象构造出和实参传过来一样的对象呢,也就是里面必须带值呢?
所以这里我们可以用迭代器版本的构造函数,这个迭代器可以放任意类型的迭代器,可以用vector迭代器,string迭代器等等。这里InputIterator是一个迭代器模板。一个类模板的成员函数,又可以是一个模板函数,InputIterator是T里面的一个成员函数。
这就非常的方便了,如果要用string类型来拷贝构造vector<T>的对象,迭代器模板会自动识别类型。这样就可以想传什么类型就传什么类型。
//一个类模板的成员函数,又可以是一个函数模板
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
while (first != last)
{
//因为要调push_back,要初始化
push_back(*first);
++first;
}
}
//拷贝构造
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
vector<T> tmp(v.begin(),v.end());
swap(_start, tmp._start);
swap(_finish, tmp._finish);
swap(_endofstorage, tmp._endofstorage);
}
赋值重载
现代写法(v1=v2)
现代写法只需要开辟一个临时对象,将v2的值赋给这个临时对象,然后交换v1与临时对象的地址,这样v1将临时对象建立好拷贝构造的对象拿过来用,还将自己的对象交给临时对象析构。非常方便。
如果形参不传引用,这样实参传给形参的时候就是一次拷贝构造,因为传参的时候形参会在栈帧上开一个新的空间,存放实参传过来的对象,所以就相当于这个临时对象。形参与this对象交换之后,返回this对象即可。
所以在调试的时候会发现,赋值重载会先调用拷贝构造,然后再调用赋值重载。
//v2=v1
//传统写法是开一个和v1一样的空间,释放v2旧空间,v2指针指向这块新开的空间
//传统写法是比较麻烦的
//赋值重载
//现代写法
vector<T>& operator=(vector<T> v)
{
swap(_start, v._start);
swap(_finish, v._finish);
swap(_endofstorage, v._endofstorage);
return *this;
}
vector中的swap函数
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
迭代器权限问题
上面的迭代器构造函数中发现,迭代器的名称是InputIterator,为什么不直接叫iterator呢?
这是因为迭代器分很多汇总,并且他们是父子类关系。
迭代器分为以下几类
也就是说(随即迭代器)--包括-->(双向迭代器)--包括-->(单向迭代器)-->(只读/写迭代器)
只读/写迭代器可以传任意类型迭代器,而随机迭代器只能传随机类型,因为随机、双向、单向迭代器都支持只读/写迭代器,都是特殊的input_iterator
vector<int> v2;
v2.push_back(1);
v2.push_back(4);
v2.push_back(3);
v2.push_back(2);
sort(v2.begin(), v2.end());
list<int> lt;
lt.push_back(40);
lt.push_back(20);
lt.push_back(30);
sort(lt.begin(), lt.end());
//sort需要传随即迭代器,随即迭代器必须支持“-”操作符(_Last-_First)
//但是链表不支持“-”操作,也就是链表传随即迭代器权限被放大了。不可以用sort
sort因为传的是随机迭代器类型,需要支持加减操作,而链表并不支持。
reverse(lt.begin(), lt.end());//双向迭代器
reverse(v.begin(), v.end());//随机迭代器
forward_list<int> flt;
flt.push_front(1);
flt.push_front(2);
flt.push_front(4);
//reverse(flt.begin(), flt.end());
//reverse是双向迭代器,forward_list是单向迭代器使用双向权限被放大。
2.增删查改
push_back
尾插这个函数需要看已有数据个数是否与可放有效数据个数大小相等,如果相等那就扩容。这里还要实现两个接口,size()和capacity()。
//物理地址连续,直接减就可以
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _endofstorage - _start;
}
push_back函数的实现
void push_back(const T& x)
{
if (_finish == _endofstorage)
{
//扩容
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
T* tmp = new T[newcapacity];
if (_start)//如果空间不是空,将就空间拷贝给新空间,释放旧空间
{
memcpy(tmp, _start, size()*sizeof(T));
delete[] _start;
}
//指向新空间
_finish = tmp + size();
_start = tmp;
_endofstorage = _start + newcapacity;
}
*_finish = x;
_finish++;
}
【ps】:指向新空间三个成员变量的更新小问题
如果代码这样写可以吗
_start = tmp;
_finish = _start + size();
_endofstorage = _start + newcapacity;
这里_start已经更新了为什么_finish的地址还是空呢?
因为size()=旧的_finish减新的_start,旧的_finish地址为空(初始化为空),_start地址已经更新是小于0的,减后size()为正,为+842150451,与负的_start抵消为0.更新失败
方法一:先更新_finish,再更新_start
_finish = tmp + size();
_start = tmp;
_endofstorage = _start + newcapacity;
方法二:保存size(),可以不按顺序更新
size_t sz = size();
_start=tmp;
_finish=_start+sz;
_endofstorage=_start+newcapacity;
测试成功。
上面的测试用到了[],重载一下operator[].
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
operator[]还有一个const版本,只读不写,调用的时候系统会自动匹配,用到const版本就会调用const版本的operator[].
const T& operator[](size_t i) const
{
assert(i < size());
return _start[i];
}
//第二个const指向的vector对象不能被改变,调用const对象返回了const对象的引用。
迭代器begin和end
iterator begin()
{
return _start;
}
iterator end()//end也是有效数据的下一个位置
{
return _finish;
}
这样就可以用迭代器来访问数组
vector<int>::iterator it = v.begin();
while (it != v.end())
{
*it += 1;
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
const迭代器版本
typedef const T* const_iterator;
//const迭代器
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
reserve和resize
reserve仅仅有扩容功能,如果扩容数字比当前数据小,那么该数据不变,也就是reserve无效。所以只要实现扩容功能就可以。
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T)*size());
delete[] _start;
}
size_t sz = size();
_start = tmp;
_finish = _start + sz;
_endofstorage = _start + n;
}
}
还有一个问题,当写这样一个类型,vector<string>,数组里面放的都是string类型,在_start拷贝给tmp的时候,memcpy只做到了浅拷贝,也就是将_start的地址也拷贝过去,释放了_start和_start中的string后,出作用域还要析构tmp中的string ,而tmp指向的string已经被释放掉,程序崩溃
vector<string> v;
v.push_back("11111111");
v.push_back("11111111");
v.push_back("11111");
v.push_back("11111");
v.push_back("11111");
vector<string>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
it++;
}
cout << endl;
所以要改进一下,string类型并不能调用成员变量初始化,所以要一个一个拷贝。
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t sz = size();
if (_start)
{
//memcpy(tmp, _start, sizeof(T)*size());
for (size_t i = 0; i < sz; ++i)
{
tmp[i] = _start[i];
}
//如果tmp是malloc出来的,就用定位new调用拷贝构造
//因为tmp是new出来的,所以调用赋值重载就可以。
//T是int,一个一个拷贝没问题
//T是string,一个一个拷贝调用的是T的深拷贝赋值
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endofstorage = _start + n;
}
}
string如果比较长会放在堆上面,而如果比较短会在对象的本身里面,用memcpy是可以的。
上面的push_back函数就可以复用reserve().
void push_back(const T& x)
{
if (_finish == _endofstorage)
{
//扩容
reserve(capacity() == 0 ? 4 : capacity * 2);
}
*_finish = x;
_finish++;
}
resize不仅扩容还会赋初始值,而这个初始值是模板类型的,就是说可以初始化int,char,vector<int>等等不同的类型,所以初始化的缺省值可以给一个匿名构造函数,这样无论什么类型都可以得到初始值。
resize还是分为三种情况【hello_ _】
- resize(2)小于当前数据
- resize(6)大于数据,小于capacity
- resize(10)大于capacity
如果小于当前数据,将_finish指向n的位置即可;当大于当前数据,将后面的数据都放初始值val,因为此时_finish还指向有效数据的下一个位置,解引用放入val后++即可。如果大于capacity,扩容之后再放初始值val,扩容可以复用reserve(),和reserve()一样直接复用就可以。
//resize(6)大于数据,小于capacity
//resize(10)大于capacity
//resize(2)小于当前数据
//hello123_ _ _
void resize(size_t n, const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
if (n>capacity())
{
reserve(n);
}
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
}
删除pop_back
void pop_back()
{
assert(_finish > _start);
--_finish;
}
插入insert
iterator insert(iterator pos,const T& x)
{
assert(pos>=_start);
assert(pos<=_finish);
if(_finish==_endofstorage)
{
reserve(capacity()==0?4:capacity()*2);
}
iterator end=_finish-1;
while(end>=pos)
{
*(end+1)=*end;
end--;
}
*pos=x;
_finish++;
return pos;
}
这段代码我们来测试一下
void test_vector_wjy::test_vector5()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
/*vector<int> v1(v);
v1 = v;
v1.pop_back();
v1.pop_back();
v1.pop_back();*/
//如果insert中发生了扩容,那么会导致ir指向空间被释放
//posIt本质就是一个野指针,我们称这种现象为迭代器失效
//虽然insert函数内部pos更新完成,但是外面的指针还是指向原来的地址空间,所以insert返回新的迭代器指针
vector<int>::iterator posIt = find(v.begin(), v.end(), 1);
if (posIt != v.end())//说明找到了
{
v.insert(posIt, 20);
}
//v.insert(v.begin()+1, -1);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
发现程序崩溃,这是因为迭代器指针失效,因为初始值capacity最大值给了4个空间,再增加一个空间需要扩容,通过上面的reserve扩容函数,我们直到扩容需要再开辟空间,new一块新的空间,就是新的地址,_start和_finish的地址已经改变,而pos还指向原来的空间,造成野指针问题,所以需要将pos也更新,如果要更新,那么需要记下原来pos和_strat的距离,新的pos位置就是_start+距离。解决迭代器指针失效问题。
size_t len=pos-_start;
reserve(capacity()==0?4:capacity()*2);
pos=_start+len;
删除erase
删除pos位置的值,pos也是个迭代器,当找到pos位置,pos位置的值被后面的值覆盖
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator begin = pos + 1;
while (begin < _finish)
{
*(begin - 1) = *begin;
++begin;
}
--_finish;
return pos;
}
通过代码测试,发现不同情况造成不同结果。
void test_vector_wjy::test_vector7()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//删除所有v的偶数
//1 2 3 4 5 正常
//1 2 3 4 崩溃
//1 2 4 5 没删完
vector<int>::iterator it = v.begin();
while(it != v.end())
{
if (*it % 2 == 0)
{
v.erase(it);
}
++it;
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
为什么会出现错误呢?
it指针指向第一个位置,当遇到偶数将偶数删除,删除后,后面的数将该位置的数字覆盖,也就是说删除之后,it还指向当前数字,但是数字已经变成下一个。然后指针直接++,并没有判断当前指针是否是偶数,这就造成有可能错过,如此循环,直到it指针走到end()的位置。
还有出现崩溃的情况,第二种情况,这是因为指针指向4的时候将它删除,删除后_finish--,_finish和end()走到了和it指针一样的地方,之后it++,it走到了end()下一个位置,再判断,it一直不等于end,it最后越界访问。所以最后i个是偶数,会导致it越界访问,it的意义变了,再++一下,导致it和end的结束判断错过。如果改成it<v.end()虽然解决了当前问题,但是并不能解决其他两种情况,而且也不是标准写法,<只适用于随即迭代器,当遇到list这种链表,并不能使用<
还有一种情况,erase删除有些vector版本的实现,可能会缩容,如果这样,erase后的it也会变成野指针,类似于insert的情况(SGI和PJ版本都没有这么做,capacity没有改变)。
导致上述三种问题,本质都是erase(it)以后意义变了,再去++it是不对的
所以改之后代码
vector<int>::iterator 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;
运行正确
但是不同编译器下的报错行为也是不同的,比如说linux和vs下对比,我们写的情况和linux类似
那么所以迭代器都存在失效吗?是的
string的insert和erase迭代器是否会失效?会
string有没有迭代器失效? 有,string迭代器什么时候会失效跟vector类似,但是string的插入和删除更多的用的是下标,并不经常用迭代器,所以string的迭代器失效问题并不是重点。
结论:只要使用迭代器访问容器,都可能会涉及迭代器失效