目录
前言
默认成员函数
构造函数
拷贝构造
赋值重载
迭代器
正向迭代器
反向迭代器
容量管理
查看容量和大小
扩容
判空
访问数据
下标访问
边界访问
数据修改
尾插尾删
指定位置插入删除
迭代器失效
清空
编辑
交换
查找数据
vector可以代替string吗
前言
讲完string和string的模拟实现,今天讲讲vector的使用。虽然说它叫vector,使用时还是我们平常使用的数组,只不过会自动地调节分配的空间。由于在空间中使用的一块连续的空间,因此支持下标访问,使用起来相当地便利,与我们之前学习的string的区别就在于,string只能存储字符,而vector可以存储任意类型的数据。
默认成员函数
构造函数
在 C++98 中有三种构造函数可以对 vector 进行初始化,分别是:
- 无参进行构造
- 放入n个相同数据
- 根据迭代器区间进行构造
其中的 allocator 是空间配置器,只是用于分配空间,目的为增加申请释放空间的效率。
因为 vector 是一个模板类,因此实例化的时候要声明内置类型。例如 vector 中要存 int 类型的数据便写作 vector<int> ,这样才是其完整的类型名。现在我们就可以试试以不同的构造函数来初始化 vector 了。
[注意]: 使用vector时要包含头文件<vector>!!
int main()
{
string s("abcd");
vector<int> v1;
vector<int> v2(3, 2);
vector<int> v3(s.begin(), s.end());
return 0;
}
v3 中之所以是 97 98 99 100 是因为模板参数选择的是 int 因此 v3 中存的是字符的 ASCII 码。
也可以这样子定义,本质上是发生了一次拷贝构造,这样使用更加与数据接近更加形象和简便。
int main()
{
vector<int> v4 = { 1,2,3,4,5 };
return 0;
}
拷贝构造
vector 的拷贝构造便是支持对同类型 vector 的拷贝。
int main()
{
vector<int> v1({ 1,2,3,4,5 });
vector<int> v2 = v1;
return 0;
}
若是模板参数不同的 vector 便无法进行拷贝构造。
赋值重载
赋值重载用于对已存在的两个 vector 之间进行赋值,就是值之间的拷贝。
迭代器
迭代器作为 STL 六大组件,必然是绕不开的话题。库中也准备了多种的迭代器供使用者选择。
正向迭代器
正向迭代器由 begin 开始 end 结束,直接使用起来与 string 的迭代器并无区别。
借助迭代器我们既可以直接使用迭代器,也可以使用范围 for 完成对 vector 的遍历了。
int main()
{
vector<int> v1 = { 1,2,3,4,5,6,7,8,9 };
vector<int>::iterator it = v1.begin();
//auto it = v1.begin(); 也可以直接使用auto
while (it != v1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
for (auto it : v1)
{
cout << it << " ";
}
cout << endl;
return 0;
}
反向迭代器
反向的迭代器名称为 reverse_iterator,其接口为 rbegin 和 rend ,使用起来与正向迭代器类似。
但值得注意的一点是:范围 for 只能进行正向的遍历,无法反向。
int main()
{
vector<int> v1 = { 1,2,3,4,5,6,7,8,9 };
vector<int>::reverse_iterator it = v1.rbegin();
//auto it = v1.rbegin();
while (it != v1.rend())
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}
容量管理
查看容量和大小
跟 string 一样,我们可以通过 size 和 capacity 两个接口分别访问 vector 的和容量,通过这两个接口也有效帮助我们确定访问时的边界值。
扩容
通过 reserve 我们能够对 capacity 进行修改,根据传入值的大小会有两种处理方式。
- n 大于 capacity 时会重新分配一块更大的空间给 vector ,将 capacity 增长到 n 或更大。
- 其他情况下便不进行处理
同时,reserve 不会更改 vector 的大小和其中的元素。
resize 用于调整 vector 的大小,使其包含 n 个元素,能够对其中的值进行初始化,若未传参数则缺省为 0。
- 如果n小于当前size,则内容将减少到其前n个元素,删除超出的元素,不更改capacity。
- 如果n大于当前size小于capacity,则通过在末尾插入所需数量的元素来扩展内容,以达到n的大小。
- 如果n大于当前capacity,将自动重新分配存储空间,再将元素填充至n个。
使用这段代码便可以直接地观察到上述三种不同的情况。
int main()
{
vector<int> v = { 1,2,3,4,5 };
cout << v.size() << " " << v.capacity() << endl;
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
v.resize(20);
cout << v.size() << " " << v.capacity() << endl;
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
v.resize(10);
cout << v.size() << " " << v.capacity() << endl;
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
return 0;
}
判空
通过 empty 这个接口可以得知当前这个 vector 是否为空,换言之就是 size 是否等于 0。
访问数据
下标访问
库中对 [ ] 运算符进行了重载,因此可以使用下标对 vector 的元素直接进行访问。
结合前面的 size 接口便可以简单实现下标遍历 vector。
边界访问
虽然说用下标直接访问就十分的方便了,但库中还有两个接口用于边界值的访问。
通过这两个接口可以直接访问 vector 中的第一个与最后一个元素。
数据修改
讲完数据访问,之后便是对 vector 之中的元素进行修改的操作了。
尾插尾删
库中有两个接口分别是 push_back 和 pop_back 分别对应尾插和尾删的功能。
可以使用如下代码看看接口的效果。
int main()
{
vector<int> v = { 1,2,3,4,5 }; //原数组
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
v.pop_back(); //尾删
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
v.push_back(8); //尾插
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
return 0;
}
指定位置插入删除
insert 支持让我们在指定的位置插入元素,同时有三种插入方式供使用者选择。
- 指定位置插入一个val
- 指定位置插入n个val
- 指定位置插入迭代器区间
可以通过下面的实例代码看看实际使用的效果。
int main()
{
vector<int> v = { 1,2,3,4,5 }; //原数组
vector<int> v1 = { 8,9,10 };
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
v.insert(v.begin() + 3, 6); //插入单个值
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
v.insert(v.begin() + 4, 3, 7); //插入n个值
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
v.insert(v.begin() + 6, v1.begin(), v1.end()); //插入一段区间
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
return 0;
}
之后我们可以使用 erase 删除任意位置的元素。可以给定一个迭代器位置,或是一个迭代器区间,删除该区间内的元素。
值得注意的时,传入的迭代器区间其实是左闭右开的,因此迭代器 last 指向的那个元素不会被删除。
int main()
{
vector<int> v = { 1,2,3,4,5 }; //原数组
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
v.erase(v.begin()); //删除第一个元素
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
v.erase(v.begin() + 1, v.begin() + 3); //删除[3,5)
for (auto it : v)
{
cout << it << " ";
}
cout << endl;
return 0;
}
迭代器失效
若我们尝试连续插入多个数据的时候,可能会出现这样子的错误。
这是由于当我们不断插入数据的时候,如果出现容量不足的情况就会进行扩容,而扩容大概率不会直接在原地扩容。因此在异地开辟了一块新空间,并将原来的元素拷贝过去,从而达到扩容的效果。
此时,我们原先拿到的迭代器中存的仍是原来空间的地址,而原来那块空间已被释放,因此该迭代器中的地址便成了野指针。
换言之,这个迭代器就失效了。那我们如何解决这个问题呢?其实 insert 是有一个返回值的,只不过我们之前忽略了它。
即,会返回指向插入的第一个元素的迭代器,因此我们只需要每次将 insert 的返回值再传回给我们的迭代器,就能够避免迭代器失效的问题。
int main()
{
vector<int> v = { 1,2,3 };
auto it = v.begin() + 2;
for (int i = 0; i <= 20; i++)
{
it = v.insert(it, i);
}
return 0;
}
同样的现象也会出现在 erase 上,其返回的迭代器是指向删除区间的下一个位置。
清空
我们还可以使用 clear 清空 vector 中的所有元素,本质上就是将 vector 的 size 大小改成 0 即可。
交换
之前在 string 我们也讲过,容器之间的交换并不需要完全的拷贝,而是直接交换类中的指针即可。
因此 vector 中的这个交换函数,自然比原生的交换函数效率要来得高。
查找数据
虽然 vector 的库中并没有查找的这个函数,但是在算法库中为我们准备了一个 find 函数可以进行查找。
传入一个迭代器区间,再输入要查找的值即可,找到了则返回指向该元素的迭代器,反之则返回该容器的 end。
int main()
{
vector<int> v1 = { 1,2,3,4,5 };
cout << *find(v1.begin(), v1.end(), 4);
return 0;
}
这个函数通过使用迭代器因而同时支持了多种容器,使用起来相当方便。
vector<char>可以代替string吗
vector 是一种泛型编程,同时支持 int 和 double 等不同类型的存储,而这些存储形式一般用不到 string 之中的某些接口,比如 += 、逻辑运算等。
可以这么说,string 这个容器是专门针对存储字符类型的,因此其种的接口也是专门适用于处理字符串的,而 vector 需要考虑的是如何支持各种类型都能够存储其中而舍弃了 string 之中的部分操作。
好了,今天使用 vector 的讲解到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注