vector的常用接口
vector的构造
vector() 无参构造
vector(size_type n, const value_type& val = value_type()) 构造并初始化n个val
vector (const vector& x) 拷贝构造
vector (InputIterator first, InputIterator last) 使用迭代器构造
vector iterator的使用
begin() + end() 获取第一个元素的位置和最后一个元素的下一个位置
rbegin() + rend() 获取最后一个元素的位置和第一个元素的前一个位置
vector容量操作接口
size() 获取元素个数
capacity() 获取容量大小
empty() 判断是否为空
resize(n, val) 改变vector的size
reserve 改变vector的capacity
注意:使用resize接口时,val是一个缺省的,默认给vector元素的初始值。当改变的size小于原本的size,那么直接截断多余的部分;当改变的size大于原本的size,那么多余的部分用val的值填充;当改变的size大于原本的capacity,那么会对原本的vector进行扩容操作。具体文档如下:
reseve开辟空间在vs下capacity是按照1.5倍进行增长的,g++是按照2倍进行增长的。2倍增长时间效率更优,但可能会造成空间上的浪费(相对于1.5倍增长)。不需要纠结具体多少倍比较合适,按照实际的需求去定义增长的倍数。
vector的增删改查
push_back 尾插
pop_back 尾删
find 查找(注意这是algorithm库中的函数模块,并不是vetor的成员函数)
inser 在pos位置插入val,后面的元素后移(注意pos是迭代器)
erase 删除pos位置的元素,后面的元素前移
swap 交换两个vector的元素空间
operator[] 让vector像数组一样访问
vector迭代器失效问题
1、会引起底层空间改变的操作,都可能引起迭代器失效,比如:resize、reserve、insert、assign、push_back等。
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int>v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
vector<int>::iterator it = v1.begin();
v1.push_back(6);
//v1.push_back(7);
while (it != v1.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
return 0;
}
上面这个代码注释尾插7就可以运行。插入操作是需要扩容的,扩容是新开辟一块大的空间,然后把旧的空间复制给新的空间,然后释放旧的空间。当插入7的时候,需要扩容,那么这个it指向释放了的空间。所以使用迭代器需要在可能对容量操作的代码之后。
2、指定位置元素的删除操作(erase)
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int>v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.push_back(6);
vector<int>::iterator it = v1.begin();
while (it != v1.end())
{
if (*it % 2 == 0)
v1.erase(it); // it = v1.erase(it); 对迭代器重新赋值就可以了。
else
it++;
}
for (int e : v1)
cout << e << ' ';
cout << endl;
return 0;
}
对于上面代码是实现vector中偶数的删除。当it指向的元素是偶数,那么就会删除it指向的元素,然后后面的元素往前移。假如没有指向偶数那么it++。并没有改变底层的空间,但是还是会报错。这是因为当it指向最后一个位置是偶数,那么需要删除,后面元素前移,这个时候it就是指向end,那么it就失效了。所以,vector删除任意的位置,vs就认为该位置的迭代器失效了。
3、但是,在Linux下,g++编译器对迭代器失效并没有这么严格
对于第一种迭代器失效的情况,g++还是可以运行,但是结果是错误的。
对于第二种迭代器失效的情况,当最后一个元素没有被删除,那么就可以运行,因为当删除一个位置的元素,后面的元素会前移去填充这个删除的位置,那么这个位置在g++下认为还是可以访问的。如果最后一个元素被删了,那么就不能运行了。
总得来说:对迭代器失效得解决办法就是对迭代器重新赋值
vector的模拟实现
成员变量的定义
template<class T>
class vector
{
typedef T* iterator;
typedef const T* const_iterator;
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
构造函数和析构函数
vector()
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{}
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
拷贝构造
//v2(v1);
// 传统写法
vector(const vector<T>& v) // 拷贝构造, 默认为浅拷贝
{
_start = new T[v.capacity()];
_finish = _start;
_endofstorage = _start + v.capacity();
for (size_t i = 0; i < v.size(); i++)
{
*_finish = v[i];
_finish++;
}
}
//提高复用的写法
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(v.capacity()); // 这不写也行,因为在push_back中会有增容的操作,但是先开辟好相同的空间,提高性能
for (const auto& e : v)
push_back(e);
}
operator=
// 赋值运算符重载,默认的赋值运算符重载为浅拷贝
// 传统写法
vector<T>& operator=(const vector<T>& v)
{
if (this != &v)
{
delete[] _start;
_start = new T[v.capacity()];
memcpy(_start, v._start, sizeof(T) * v.size());
_finish = _start + v.size(); // 注意这里要把_finish _endofstorage更新!!!!
_endofstorage = _start + v.capacity();
}
return *this;
}
//现代写法
//v2 = v1
vector<T>& operator=(vector<T> v) // 这里是传值,会自动调用拷贝构造, 然后v会单独开辟一块空间
{
swap(v);
return *this; // 最后作用域结束,v会自动调用析构函数,释放掉v2的旧空间
}
void swap(vector<T>& v) // 当对于两个vector对象直接使用swap交换,那会产生三次深拷贝,代价极大
{ // 定义一个成员函数swap, 只需要交换三次指针,提升性能
::swap(_start, v._start); // 区分成员函数的swap和库中的swap函数,在函数名之前加std::
::swap(_finish, v._finish); //
::swap(_endofstorage, v._endofstorage);
}
迭代器和 size 、 capacity
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _endofstorage - _start;
}
const size_t size() const
{
return _finish - _start;
}
const size_t capacity() const
{
return _endofstorage - _start;
}
reserve
当 n 大于 capacity,扩容
当 n 小于 capacity,什么也不做
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size(); // 旧的_start的长度要先存起来,因为当扩容时,旧的空间会被释放,那么就找不到旧空间的长度了
T* temp = new T[n];
if (_start)
{
for (size_t i = 0; i < sz; i++)
temp[i] = _start[i];
delete[] _start;
}
_start = temp;
_finish = _start + sz; // 这里不可以 + size(), 因为原来的_start 移动了
_endofstorage = _start + n;
}
}
ps:在扩容的时候,拷贝旧空间时,不可以使用memcpy(temp, _start, sz * sizeof(T));
假如T的类型不是内置类型,比如是string,那么在v中存储的string类型的对象,每个string类型的对象中都有一个指针(char* _str, size_t _size, size_t _capacity)
这个指针指向的是在堆区开辟的空间,这块空间就是这个string类型对象的内容
当需要扩容的时候,temp先开辟空间,然后temp拷贝旧空间的内容
假如是使用memcpy实现拷贝,(按字节拷贝,浅拷贝)那么在temp中拷贝旧空间的string类型对象,该对象中也一样会有_str指针,并且和旧空间的是独立的(也就是说这两个指针的地址不同)
但是两个指针都指向同一块堆区的空间。
然后delete去释放旧vector中的每一个string类型对象,当释放每一个string类型对象的时候,又会调用string类的析构函数
那么就会把堆区的内容也释放掉,这时:temp中的string类型对象的_str指针指向的内容也被释放了,指向空,如果访问的话,就会访问随机值
那如何避免这个问题呢?
使用更深层次的深拷贝,temp[i] = _start[i]; 这是两个string类型对象的赋值操作
那么就会调用string类型的operator=函数,这时就会进行深拷贝, 再次在堆区开辟一块新空间,拷贝原来的堆区内容。
resize
// 这里的T(),是一个匿名对象做缺省值
void resize(size_t n, const T& val = T())
{
if (n > capacity())
reserve(n);
if (n > size())
{
while (_finish < _start + n)
{
*_finish = val;
++_finish;
}
}
else
_finish = _start + n;
}
insert 、push_back
iterator insert(iterator pos, const T& val)
{
assert(pos <= _finish && pos >= _start);
if (_finish == _end_of_storage)
{
size_t n = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + n; // 扩容之后,迭代器失效,需要重新赋值
}
iterator end = _finish;
while (end != pos)
{
*end = *(end - 1);
--end;
}
*end = val;
_finish++;
return pos;
}
void push_back(const T& val)
{
insert(_finish, val);
}
erase、pop_back
iterator erase(iterator pos)
{
assert(pos < _finish);
iterator end = pos;
while (end < _finish - 1)
{
*end = *(end + 1);
++end;
}
_finish--;
return pos;
}
void pop_back()
{
erase(_finish - 1);
}
operator []
T& operator[](size_t pos)
{
return _start(pos);
}
const T& operator[](size_t pos) const
{
return _start(pos);
}