文章目录
- 前言
- 一、vector的介绍
- 三个原生指针的图示
- 二、vector的构造函数
- 一个注意事项
- 二、vector的空间大小、调整函数
- size()
- capacity()
- empty()
- resize()
- reserve()
- 三、vector的增删查改
- push_back & pop_back
- insert & erase
- find
- swap
- front & back
- operator[ ] & 正反迭代器
- 四、迭代器失效问题及解决方法
- 问题举例一
- 问题举例二
- 解决方法
- 总结
前言
结束了第一关string后,我们将来学习一下vector
应该说有了string的经验后,vector设计得就不那么冗余,但是仍有很多有趣的东西值得我们学习
来试试吧!
一、vector的介绍
vector文档介绍
关于vector,在正式开始前,你先要有以下认识:
- vector是表示可变大小数组的序列容器,底层是动态开辟顺序表
- 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理
- 当vector需要重新分配大小时,其做法是,分配一个新的数组,然后将全部元素移到这个数组当中,并释放原来的数组空间,并不是真的多开一个两个空间!释放就是全部释放,开辟就是全新开辟!
- vector分配空间策略:vector会分配一些额外的空间以适应可能的增长(如同string),因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的(vector采用_start、_finish、_end_of_storage三个原生指针来完成)
- 由于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); // 尾插元素1
v.push_back(2); // 尾插元素2
v.push_back(3); // 尾插元素3
v.push_back(4); // 尾插元素4
v.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); // 在容器开头插入0
v.insert(v.begin(), 5, -1); // 在容器开头插入5个-1
v.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的位置插入10
pos = find(v.begin(), v.end(), 3); // 获取值为3的元素的迭代器
v.erase(pos); // 删除3
return 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); // err
if (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的实现会更加精彩!