vector
1. vector介绍
- vector文档
- vector其实就是一个顺序表,它表示可变大小数组的序列容器。
- 像数组一样,可以使用下标+
[]
来访问vector的元素,和数组一样高效;甚至,它的大小是可以动态改变的,其大小由容器自动处理。- 在底层上,vector使用动态分配空间来储存元素。当新元素插入,原空间不够时,需要重新分配一块连续的空间来增加存储空间,做法是开辟大一点的空间,然后将原内容拷贝过来,再释放原空间,此举的时间成本相对较高。
- vector会额外分配一些空间,以适应可能的增长,实际的存储空间可能比需要的空间更大。不同的STL库的实现采取不同的策略。
- vector的成员变量和string不同,string是两个整型存size和capacity,一个char指针指向动态开辟的空间;而vector是三个迭代器(底层类似于指针),分别是开头的迭代器、最后一个元素下一个位置的迭代器、开辟的空间的最末端的迭代器。
- string是管理字符串的类,那么vector< char>实例化为char是否能替代string呢?
当然不可以,因为string后都有’\0’,可以更好和C的字符串对接,另外string的接口也更加丰富,可以更好的管理字符串。
2. vector的常用接口
vector的许多接口中有很多别名:
相比于string,vector的接口数量就显得很少了,下面我们看看vector常用的接口。
-
构造函数(constructor)
(constructor) 功能 explicit vector (const allocator_type& alloc = allocator_type());
默认构造函数 explicit vector (size_type n, const value_type& val = value_type()
,const allocator_type& alloc = allocator_type());
用n个val值初始化 template <class InputIterator>
vector (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type());
用迭代器初始化(可以允许其他类型作为参数初始化) vector (const vector& x);
拷贝构造函数 vector<int> v1; vector<int> v2(3, 2); int nums[] = { 1,2,3 }; vector<int> v3(arr, arr + 3); vector<int> v4(v3);
-
容量操作
函数 功能 size_type size(); const
返回有效元素个数 size_type capacity(); const
返回实际空间大小 bool empty(); const
判断是否为空 void resize (size_type n, value_type val = value_type());
改变有效元素个数 void reserve (size_type n);
改变空间大小 补充:
- resize:
当n小于有效元素个数时,会将n之后的所有元素删除,只保留从头到n位置的元素;
当n大于有效元素个数,却小于实际空间大小时,会在最后一个有效元素后填充值val,直到n位置;
当n大于实际空间大小时,会开辟空间,然后在最后一个有效元素后填充值val,直到n位置。 - reserve:
当n大于实际空间大小时,开辟空间。其余任何情况不做处理。
- resize:
-
迭代器
函数 功能 iterator begin();
const_iterator begin() const;
返回容器开头的位置的迭代器 iterator end();
const_iterator end() const;
返回容器最后一个有效元素的下一个位置的迭代器 reverse_iterator rbegin();
const_reverse_iterator rbegin() const;
返回容器最后一个有效元素的位置的迭代器 reverse_iterator rend();
const_reverse_iterator rend() const;
返回容器开头的前一个位置的迭代器 -
访问元素
函数 功能 reference operator[] (size_type n);
const_reference operator[] (size_type n) const;
访问下标为n的元素 reference at (size_type n);
const_reference at (size_type n) const;
访问下标为n的元素 vector<int> v(3, 2); cout << v[0] << endl; cout << v.at(1) << endl;
-
修改操作
函数 功能 void push_back (const value_type& val);
尾插一个值 void pop_back();
尾删一个值 insert
在某个位置插入元素 erase
删除某个位置的元素 void swap (vector& x);
交换两个vector对象 void clear();
清空有效元素
3. 模拟实现vector类(部分接口)
#include<iostream>
#include<assert.h>using namespace std;namespace Myspace
{template<class T>class vector{public:typedef T* iterator;typedef const T* const_iterator;vector(){ }vector(size_t n, const T& value = T()){// 开空间_start = new T[n];_finish = _start + n;_endofstorage = _finish;// 放数据for (auto& e : *this){e = value;}}vector(int n, const T& value = T()){// 开空间_start = new T[n];_finish = _start + n;_endofstorage = _finish;// 放数据for (auto& e : *this){e = value;}}vector(long n, const T& value = T()){// 开空间_start = new T[n];_finish = _start + n;_endofstorage = _finish;// 放数据for (auto& e : *this){e = value;}}template<typename InputIterator>vector(InputIterator first, InputIterator last){/*int len = last - first;_start = new T[n];_finish = _start + len;_endofstorage = _finish;for (auto& e : *this){e = *first;first++;}*/_start = new T[last - first];for (size_t i = 0; i < last - first; i++){_start[i] = first[i];}_finish = _start + (last - first);_endofstorage = _start + (last - first);}vector(const vector& v){_start = new T[v.capacity()];for (size_t i = 0; i < v.size(); i++){_start[i] = v._start[i];}_finish = _start + v.size();_endofstorage = _start + capacity();}~vector(){if (_start){delete[] _start;_start = _finish = _endofstorage = nullptr;}}vector<T>& operator= (vector<T> v){swap(v);return *this;}//---------------------------------- 迭代器 ---------------------------------------//iterator begin(){return _start;}const_iterator begin() const{return _start;}iterator end(){return _finish;}const_iterator end() const{return _finish;}//---------------------------------- 容量 ---------------------------------------//size_t size() const{return _finish - _start;}size_t capacity() const{return _endofstorage - _start;}bool empty() const{return _start == _finish;}void reserve(size_t n){if (n > capacity()){int len = size();iterator tmp = new T[n];// 这里拷贝数据不能用memcpy,如果T需要深拷贝,memcpy只是浅拷贝if (_start){for (size_t i = 0; i < n; i++){tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = _start + len;_endofstorage = _start + n;}}void resize(size_t n, const T value = T()){if (n <= size()){_finish = _start + n;}else{reserve(n);while (_finish != _start + n){*_finish = value;_finish++;}}}//---------------------------------- 修改 ---------------------------------------//void push_back(const T value){if (_finish == _endofstorage){reserve(_start == _endofstorage ? 4 : capacity() * 2);}*_finish = value;++_finish;}void pop_back(){assert(_start != _finish);--_finish;}iterator insert(iterator pos, const T& value){assert(pos >= _start && pos <= _finish);if (_finish == _endofstorage){int len = pos - _start; // 防止扩容后迭代器失效reserve(_start == _endofstorage ? 4 : capacity() * 2);pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}_finish++;*pos = value;return pos;}iterator erase(iterator pos){assert(pos >= _start && pos < _finish);iterator it = pos + 1;while (it != _finish){*(it - 1) = *it;++it;}--_finish;return pos;}void swap(vector<T>& v){std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_endofstorage, v._endofstorage);}T& operator[](size_t pos){assert(pos < size());return _start[pos];}const T& operator[](size_t pos) const{assert(pos < size());return _start[pos];}private:iterator _start = nullptr; // 给缺省值,在构造函数的初始化列表中自动初始化iterator _finish = nullptr;iterator _endofstorage = nullptr;};
}
注意:
- 构造函数
vector(int n, const T& value = T())
接口需要重载多个(int/size_t/long),以防止创建对象时(vector<int> v(2,3);
)编译器自动匹配到vector(InputIterator first, InputIterator last)
这个接口。
原因:编译器总会选择最匹配的接口。 - 在扩容时拷贝数据的时候,不要使用memcpy(上述代码的第151行),原因如下:
如果模板实例化为string,那么此时就相当于memcpy(string1, string2, size)
,将两个string类memcpy,一定是浅拷贝,因为string类本身有个char*的指针,需要动态开辟空间,memcpy仅仅是将两个指针指向了同一块空间,并没有开辟新的空间。
直接赋值即可,赋值会调用string自己的赋值运算符重载,它自己会实现深拷贝。
不止是string,其他任何动态管理空间的类都是如此。
4. 迭代器失效
-
迭代器的作用:
迭代器就是为了不管各个容器的底层如何实现,都能够使用算法。其底层实际是个指针,或是对指针的封装,比如string和vector的迭代器就是char* 和 T*。 -
迭代器失效:
当迭代器底层所指向的空间被销毁了,还依旧使用该迭代器,那么就会造成野指针的问题,后果是程序崩溃。在VS2022下直接报错崩溃,在Linux下可能不会报错,因此对于程序员来说,避免迭代器失效是必须的。 -
可能引起迭代器失效的场景:
- 扩容操作
#include <iostream> using namespace std; #include <vector> int main() {vector<int> v{1,2,3,4,5,6};auto it = v.begin();v.resize(100, 8);v.reserve(100);v.insert(v.begin(), 0);v.push_back(8);v.assign(100, 8);while(it != v.end()){cout<< *it << " " ;++it;}cout<<endl;return 0; }
以上五个接口可能会导致迭代器it失效,原因:
使用接口改变底层空间时,可能会扩容,而vector的扩容逻辑是:开辟一块新空间,将原数据拷贝至新空间,然后释放旧空间。扩完容之后底层地址空间就变了,而外部的迭代器it
依旧指向原来已经被释放的空间,对迭代器再次操作时,就是对已经释放的空间进行操作,会引起代码奔溃。- 删除指定位置元素 — erase
#include <iostream> using namespace std; #include <vector> int main() {int a[] = { 1, 2, 3, 4 };vector<int> v(a, a + sizeof(a) / sizeof(int));// 使用find查找4所在位置的iteratorvector<int>::iterator pos = find(v.begin(), v.end(), 3);// 删除pos位置的数据,导致pos迭代器失效。v.erase(pos);cout << *pos << endl; // 此处会导致非法访问return 0; }
上述代码导致迭代器失效的原因:
erase删除pos位置的元素,后面的元素会往前挪动,理论上没有产生扩容,底层地址空间就不会改变,迭代器不应该失效。但是如果pos位置是最后一个元素,删除之后,pos位置就成了end(),是最后一个有效元素的下一个位置,此位置不属于有效数据的区间,此迭代器就失效了。在VS中再对pos迭代器进行操作,程序就会奔溃。 -
解决办法:
完成扩容或删除操作之后,给迭代器重新赋值即可。 -
Linux下,g++编译器对迭代器失效的检测并不是很严格,处理也没有VS下那么极端。
-
vs下迭代器失效
-
g++下迭代器失效
由此可见,SGI版本的STL(Linux下的g++编译),迭代器失效后代码不一定会奔溃(如果迭代器不在begin和end的范围内也会奔溃),但是它的结果一定不正确。
-
-
不仅vector存在迭代器失效的问题,string也有迭代器失效的问题,因此我们在使用STL时,一定要小心迭代器失效!