目录
1.insert导致的迭代器失效问题(扩容引起的迭代器失效问题)
一、vector的使用
1.1vector的定义
vector() :无参构造
vector(size_type n, const value_type& val = value_type()):构造并初始化n个val
vector (const vector& x) : 拷贝构造
vector (InputIterator first, InputIterator last) :使用迭代器进行初始化构造
1.2vector的iterator的使用
begin + end :获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下 一个位置的iterator/const_iterator
rbegin + rend : 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置 的reverse_iterator
1.3vector的空间增长问题
size :获取数据个数
capacity :获取容量大小
empty :判断是否为空
resize : 改变vector的size
reserve :改变vector的capacity
- capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2 倍增长的。不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。
- vs是PJ版本STL,g++是SGI版本STL。
- reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。
- resize在开空间的同时还会进行初始化,影响size。
vs下vector的扩容机制
如果已知容量,直接用reserve开好空间,可以避免一边插入一边扩容导致的效率低下的问题
resize用来改变size的值,当n<size时会缩容但不改变元素,当n>size时会扩容并补充新元素
reverse和resize在插入元素时要谨慎使用。因为[ ]内部访问元素是通过下标来访问的,需要考虑到size的大小,reverse只是开好空间并没有增加元素的个数,因此不能用[ ]去访问,而resize改变的是元素的个数,因此可以用[ ]来访问
1.4vector的增删查改
push_back :尾插
pop_back :尾删
find : 查找(注意这个是算法模块实现,不是vector的成员接口)
insert :在position之前插入val
erase :删除position位置的数据
swap :交换两个vector的数据空间
operator[] :像数组一样访问
1.5迭代器的失效问题
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对 指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器 底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即 如果继续使用已经失效的迭代器,程序可能会崩溃)。
会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、 assign、push_back等。(需要扩容)
erase删除某一个位置也会导致指向该位置的迭代器失效
1.insert导致的迭代器失效问题(扩容引起的迭代器失效问题)
出错原因:以上操作可能会导致vector扩容,插入位置pos在insert实现内部已经不指向原来的那块空间了,但外部的pos并没有改变。也就是说vector底层原理旧空间被释放掉,而在打印时,it还使用的是释放之间的旧空间,在对it迭代器操作时,实际操作的是一块已经被释放的空间,而引起代码运行时崩溃。
解决方式:在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新赋值即可。
2.erase导致的迭代器失效问题
1.删除数据为3的元素
erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end 的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素 时,vs就认为该位置迭代器失效了。
解决方式:重新给迭代器赋值
2.删除所有的偶数
3.. Linux下g++编译器对迭代器失效的处理
Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。SGI STL中,迭代器失效后,代码并不一定会崩溃,但是运行结果肯定不对,如果it不在begin和end范围内,肯定会崩溃的。
4.string的迭代器失效
与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效。
迭代器失效解决办法:在使用前,对迭代器重新赋值即可。
二、vector的模拟实现
2.1vector的定义
1.成员变量
vector的成员变量使用三个指针,通过三个指针的位置来控制vector的大小和容量
2.vector的三种构造函数
C++11支持在成员变量声明的位置给缺省值,因此vecto构造函数r的初始化列表可以不显示写
这里有一个需要注意的细节,在用n个int类型的val去构造时要使用int n而不是size_t n。因为使用size_t 时编译器会走参数为iterator的那个构造函数,但如果使用int n ,第二个构造函数的形参列表完美符合所有实参的类型,就不用调用模板去推导类型了(有现成的编译器会走现成的)
3.拷贝构造函数
2.2深浅拷贝问题
拷贝构造函数中有一个核心点:使用memcpy会导致什么问题?以及如何解决?
- 1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
- 2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型 元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。
接下来我们以string类为例
结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为 memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。
像string本质上就是在vector的拷贝构造函数里去调用它的赋值重载来实现深拷贝
2.3vector的赋值运算符重载函数和析构函数
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
//赋值
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
2.4vector的iterator
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin()const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
2.5vector的空间增长
//修改容量
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* temp = new T[n];
if (_start)
{
//memcpy(temp, _start, sizeof(T) * sz);
for (size_t i = 0; i < size(); i++)
{
temp[i] = _start[i];
}
delete[] _start;
}
_start = temp;
_finish = _start + sz;
_endofstorage = _start + n;
}
}
//获取容量
int capacity() const
{
return _endofstorage - _start;
}
//获取大小
int size() const
{
return _finish - _start;
}
//判空
bool empty()
{
return _start == _finish;
}
//改变size大小
void resize(size_t n, const T& val = T())
{
//比size小
if (n < size())
{
_finish = _start + n;
}
else
{
reserve(n);
//填数据
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
}
2.6vector的增删查改
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _endofstorage)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity()*2;
reserve(newcapacity);
//解决pos引起的迭代器失效问题(扩容会使pos变成野指针)
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = x;
_finish++;
return pos;;
}
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
iterator temp = pos;
while (pos < _finish)
{
*pos = *(pos + 1);
pos++;
}
_finish--;
return temp;
}
void push_back(const T& x)
{
/*if (_finish == _endofstorage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity()*2;
reverse(newcapacity);
}
*_finish = x;
_finish++;*/
insert(end(), x);
}
void pop_back()
{
if (!empty())
--_finish;
}
这边要注意在插入元素时,可能会进行扩容,扩容后pos指向的空间已经被释放了(迭代器失效),所以要先保存pos的位置,再到新空间里找到pos的位置。由于这里传的是外部pos的一份拷贝,内部pos的改变不影响外部pos的改变,在外部就不能对pos进行访问了(迭代器失效),因为外部pos仍然指向旧空间。如果要访问,就必须有返回值并且在外部接收返回值。返回的是插入位置的迭代器。但是不能传const+引用,否则内部就没办法修改pos的位置了。
erase也存在迭代器失效的问题,解决方法一样是返回删除位置的迭代器并在外部接收返回值就可以对迭代器进行访问操作了。