文章目录
前言一、vector的介绍三个原生指针的图示 二、vector的构造函数一个注意事项 二、vector的空间大小、调整函数size()capacity()empty()resize()reserve() 三、vector的增删查改push_back & pop_backinsert & erasefindswapfront & backoperator[ ] & 正反迭代器 四、迭代器失效问题及解决方法问题举例一问题举例二解决方法 总结
前言
结束了第一关string后,我们将来学习一下vector
应该说有了string的经验后,vector设计得就不那么冗余,但是仍有很多有趣的东西值得我们学习
来试试吧!
一、vector的介绍
vector文档介绍
关于vector,在正式开始前,你先要有以下认识:
三个原生指针的图示
_start指向vector的开头
_finish指向最后一个有效元素的后一位
_end_of_storage指向存储空间的最后一个的后一位
我们就用这三个原生指针去模拟迭代器(尽管它不是真正的迭代器),代码如下:
template<class T> class vector { public: typedef T* iterator; typedef const T* const_iterator; private: iterator _start = nullptr; iterator _finish = nullptr; iterator _end_of_storage = nullptr; };
二、vector的构造函数
allocator_type内存配置器(内存池)和explicit这个阶段都不用去理会它,首先我们发现真的跟string相比简要了不少,所以我讲解的也就方便一些,哈哈!
vector();
vector类的默认构造函数,构造一个没有元素的空容器
vector(size_type n, const value_type& val = value_type());
构造一个vector类对象并用n个val初始化,value_type()是模板参数列表实例化转化的T类型,其实就是为了来个缺省,可是缺省不能为0,这样自定义类型就无法默认初始化,于是,Cpp给了内置类型和默认类型一种相同方法的初始化赋值方式:
具体实例如下:
vector(const vector& v);
vector类的拷贝构造函数
template < class InputIterator >
vector(InputIterator first, InputIterator last);
使用迭代器进行初始化构造
请注意!这里begin()和end()函数是传值返回,返回临时对象具有常性,不能通过 ++ 或 - - 修改临时对象
一个注意事项
初始化构造逻辑是没有问题的,但是在调用过程可能会出现问题
vector<int> v1(10, 1); print_vector(v1);
我们看这段代码,很显然我们的想法是调用 vector(size_type n, const value_type& val = value_type()); 这个初始化构造,可问题是,我们发生了非法的间接寻址报错,这很奇怪,那我在这里也不卖关子,直接告诉大家这是编译器调用错了构造函数,调用了利用模板迭代器构造的那个
其实,编译器有时候也没那么聪明,它只会匹配它认为最适合的那个
在这个例子中,我们看两个参数10、1都是int类型,而我们想调用的在这里是size_t、const int类型,相比之下,编译器把InputIterator模板实例化为int,可这哪里对呢,在这里我们应该传入的是迭代器,所以在内部访问的时候必然会发生错误
解决方法也很简单,就是像上方一样,满足编译器选择最合适的这一特性,传入元素个数n的时候数字后面跟个u表示无符号整数 -> v2(10u,1) 或者你干脆直接重载,其他不怎么变,size_t 改为 int 就行
二、vector的空间大小、调整函数
size()
获取有效元素个数
capacity()
获取容量大小
empty()
判断容器是否为空
resize()
将有效元素的个数修改为n,并且如果n大于原来的size,多出来的地方用val填充,如果没有给出val,就用默认初始化的值填充(内置类型一般就是0,自定义类型就是默认初始化构造)
reserve()
改变容器的最大容量
resize 与 reserve 的对比
resize:当所给值大于容器当前的size时,将size扩大到该值,扩大的元素为第二个所给值,若未给出,则默认为0;当所给值小于容器当前的size时,将size缩小到该值
reserve:当所给值大于容器当前的capacity时,将capacity扩大到该值;当所给值小于容器当前的capacity时,什么也不做
三、vector的增删查改
push_back & pop_back
对容器进行尾插尾删
#include <iostream>#include <vector>using namespace std;int main(){vector<int> v;v.push_back(1); // 尾插元素1v.push_back(2); // 尾插元素2v.push_back(3); // 尾插元素3v.push_back(4); // 尾插元素4v.pop_back(); // 尾删元素v.pop_back(); // 尾删元素v.pop_back(); // 尾删元素v.pop_back(); // 尾删元素return 0;}
insert & erase
通过insert函数可以在所给迭代器位置插入一个或多个元素,通过erase函数可以删除所给迭代器位置的元素,或删除所给迭代器区间内的所有元素(左闭右开)
#include <iostream>#include <vector>using namespace std;int main(){vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.insert(v.begin(), 0); // 在容器开头插入0v.insert(v.begin(), 5, -1); // 在容器开头插入5个-1v.erase(v.begin()); // 删除容器中的第一个元素v.erase(v.begin(), v.begin() + 5); // 删除在该迭代器区间内的元素(左闭右开)return 0;}
find
请注意!!!vector并没有单独实现find,但这并不代表“查”不了,我们可以调用算法模块(algorithm)来实现
find函数在所给迭代器区间寻找第一个匹配的元素,并返回它的迭代器,若未找到,则返回所给的第二个参数,即v1.end()
#include <iostream>#include <vector>#include <algorithm>using namespace std;int main(){vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);vector<int>::iterator pos = find(v.begin(), v.end(), 2); // 获取值为2的元素的迭代器v.insert(pos, 10); // 在2的位置插入10pos = find(v.begin(), v.end(), 3); // 获取值为3的元素的迭代器v.erase(pos); // 删除3return 0;}
swap
可以交换两个容器的数据空间,实现两个容器的交换
#include <iostream>#include <vector>using namespace std;int main(){vector<int> v1(10, 1);vector<int> v2(10, 2);v1.swap(v2); //交换v1,v2的数据空间return 0;}
front & back
分别返回容器中第一个元素和最后一个元素的引用
operator[ ] & 正反迭代器
vector当中实现了 [ ] 操作符的重载,因此我们也可以通过 “下标+[ ]” 的方式对容器当中的元素进行访问,且vector是支持迭代器的,所以我们还可以用范围for对vector容器进行遍历。(支持迭代器就支持范围for,因为在编译时编译器会自动将范围for替换为迭代器的形式
#include <iostream>#include <vector>using namespace std;int main(){vector<int> v(10, 1);// 使用“下标+[]”的方式遍历容器for (size_t i = 0; i < v.size(); i++){cout << v[i] << " ";}cout << endl;// 使用“范围for”(本质上是迭代器)的方式遍历容器for (const auto& e : v){cout << e << " ";}cout << endl;return 0;}
四、迭代器失效问题及解决方法
问题举例一
迭代器的主要作用就是让我们在使用各个容器时不用关心其底层的数据结构,而vector的迭代器在底层实际上就是一个指针。
在这方面上,VS可以说是非常严格,进行了强制检查,迭代器用了之后就视为失效
void test_vector(){ 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); v1.push_back(7); v1.push_back(8); print_vector(v1); vector<int>::iterator it = v1.begin() + 3; v1.insert(it, 40); print_vector(v1); cout << *it << endl; // err}
这里报错其实很好理解,我一开始便说vector的扩容不是在原先的基础上多开空间,而是重新开辟一块符合长度要求的空间,再释放旧空间,这就导致了it传参后,尽管就算形参在内部有更新,可形参的改变不影响实参,这是我们从C语言的共识,此时it就是错误的,再去访问它的数据就更是错误了
你可能会想,那我insert采用引用接收不就行了吗,可是你要知道v2.begin()返回的是临时常性变量,不能通过引用来接收,否则就是权限的放大,怎么解决呢?请保留这份疑问来继续往下看~
问题举例二
#include <iostream>#include <vector>using namespace std;int main(){vector<int> v;for (size_t i = 1; i <= 6; i++){v.push_back(i);}vector<int>::iterator it = v.begin();while (it != v.end()){if (*it % 2 == 0) // 删除容器当中的全部偶数{v.erase(it);}it++;}return 0;}
该代码看上去实际上并没有什么错误,但如果你画图仔细分析,你就会发现该代码的问题所在,迭代器访问到了不属于容器的内存空间,导致程序崩溃
或者pos刚好对应最后一个元素,删除后迭代器pos就超出了有效元素范围,可能导致非法访问,这也属于迭代器失效
解决方法
如上,我们会发现,扩容缩容等都有可能导致迭代器失效,因此,VS考虑到这点,迭代器it使用过一次后就立马失效,若再次使用或者访问则报错,因此,每次使用前,对迭代器进行重新赋值是不二的好方法!
std::vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);v1.push_back(5);int x;cin >> x;std::vector<int>::iterator it = find(v1.begin(), v1.end(), x);if (it != v1.end()) {it = v1.erase(it); // right// v1.erase(it); // errif (it != v1.end()) {cout << *it << endl; }}
vector<int> v;for (size_t i = 1; i <= 6; i++){v.push_back(i);}vector<int>::iterator it = v.begin();while (it != v.end()){if (*it % 2 == 0) // 删除容器当中的全部偶数{it = v.erase(it); // 删除后获取下一个元素的迭代器}else{it++; // 是奇数则it++}}
总结
可以还是发现有新内容的,请加油,下篇vector的实现会更加精彩!