【C++篇】解密 STL 动态之魂:全面掌握 C++ vector 的高效与优雅
CSDN 2024-10-10 15:35:01 阅读 64
文章目录
C++ `vector` 容器详解:从入门到精通前言第一章:C++ `vector` 容器简介1.1 C++ STL 容器概述1.2 为什么使用 `vector`1.3 `vector` 的优缺点
第二章:`vector` 的构造方法2.1 常见构造函数2.1.1 示例:不同构造方法2.1.2 相关文档
第三章:`vector` 容量与大小操作3.1 容量管理接口3.1.1 示例:容量与大小操作3.1.2 相关文档
3.2 动态扩展与性能问题第四章:`vector` 元素访问与修改4.1 元素访问方法4.1.1 示例:元素访问4.1.2 相关文档
4.2 修改元素
第五章:`vector` 的迭代器与遍历5.1 迭代器5.1.1 示例:使用迭代器遍历 `vector`
5.2 使用 `for_each()` 函数遍历5.2.1 示例:使用 `for_each()` 遍历并输出元素5.2.2 相关链接
5.3 `vector` 迭代器失效问题(重点)5.3.1 迭代器失效的原因与后果5.3.2 常见导致迭代器失效的操作5.3.3 扩容操作导致迭代器失效示例:扩容导致迭代器失效
5.3.4 删除操作导致迭代器失效示例:删除导致迭代器失效
5.3.5 删除偶数时的正确和错误写法错误示例:正确示例:
5.3.6 编译器对迭代器失效的处理差异示例:GCC 下的宽松处理示例:MSVC 下严格处理
5.3.7 扩容后的迭代器失效问题5.3.8 总结与建议第六章:`vector` 的插入、删除与修改6.1 插入操作6.1.1 示例:使用 `push_back()` 和 `insert()` 插入元素6.1.2 `emplace_back()` 与 `push_back()` 的区别6.1.3 示例:使用 `emplace_back()` 插入元素
6.2 删除操作6.2.1 示例:使用 `pop_back()` 和 `erase()` 删除元素6.2.2 使用 `clear()` 清空 `vector`6.2.3 相关链接
6.3 修改元素6.3.1 示例:修改 `vector` 元素
写在最后
C++ <code>vector 容器详解:从入门到精通
💬 欢迎讨论:学习过程中有问题吗?随时在评论区与我交流。你们的互动是我创作的动力!
👍 支持我:如果你觉得这篇文章对你有帮助,请点赞、收藏并分享给更多朋友吧!
🚀 一起成长:欢迎分享给更多对 C++ 感兴趣的小伙伴,让我们共同进步!
前言
C++ 标准模板库(STL)是现代 C++ 编程的基石,其中的容器、算法和迭代器为开发者提供了高效、灵活的数据处理工具。vector 作为 STL 中最常用的顺序容器,不仅支持动态数组的功能,还通过自动内存管理和丰富的操作接口,极大简化了数据操作的复杂性。无论是在日常开发还是算法竞赛中,vector 的高效性和灵活性都使其成为开发者的首选。
本篇文章将带你深入理解 C++ vector 的内部工作原理与高级用法,从基本的构造与遍历,到迭代器失效问题的深入剖析,再到在不同场景下的优化策略。通过全面、详细的讲解,我们将一步步揭开 vector 的技术细节,让你在掌握 vector 基本使用的基础上,更好地应对复杂的开发需求。
第一章:C++ vector
容器简介
1.1 C++ STL 容器概述
C++ 提供了丰富的标准模板库 (STL),包括 顺序容器(如 vector
)、关联容器(如 map
、set
)等。vector
是最常用的 STL 顺序容器之一,它的特点是支持 动态数组,可以在运行时自动扩展容量,提供高效的随机访问。
1.2 为什么使用 vector
与传统的 C 风格数组(T array[N]
)相比,vector
具有以下优势:
动态调整大小,无需手动管理内存;提供了丰富的接口,支持插入、删除、查找等操作;内置内存管理机制,防止越界访问。
例如,使用 C 风格数组的代码:
int arr[5] = { 1, 2, 3, 4, 5};
与之相比,使用 vector
的方式更加灵活:
#include <vector>
using namespace std;
vector<int> v = { 1, 2, 3, 4, 5}; // 自动管理内存和大小
1.3 vector
的优缺点
优点:动态扩展、支持随机访问、效率高。缺点:尾部操作快,但头部插入和删除较慢(涉及到元素移动)。
第二章:vector
的构造方法
2.1 常见构造函数
C++ vector
提供了多种构造方法,可以创建不同初始状态的 vector
对象。
构造函数 | 功能 |
---|---|
vector() | 构造一个空的 vector |
vector(size_type n, const T& val) | 构造包含 n 个元素值为 val 的 vector |
vector(const vector& v) | 拷贝构造 vector ,与已有 vector 一致 |
vector(initializer_list<T>) | 使用初始化列表构造 vector |
2.1.1 示例:不同构造方法
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v1; // 空 vector
vector<int> v2(5, 100); // 5个100的元素
vector<int> v3(v2); // 拷贝构造
vector<int> v4 = { 1, 2, 3, 4, 5}; // 使用初始化列表
for (int i : v4) {
cout << i << " "; // 输出: 1 2 3 4 5
}
return 0;
}
输出:
1 2 3 4 5
2.1.2 相关文档
C++ Reference: vector constructor
第三章:vector
容量与大小操作
3.1 容量管理接口
C++ vector
提供了多种方法管理容器的容量与大小。
方法名 | 功能描述 |
---|---|
size() | 返回当前元素个数 |
capacity() | 返回分配的存储空间大小 |
empty() | 判断容器是否为空 |
resize(n) | 将容器大小调整为 n ,多出的部分用默认值填充 |
reserve(n) | 预留存储空间,避免多次扩容 |
shrink_to_fit() | 收缩 capacity 以匹配 size() 的大小 |
3.1.1 示例:容量与大小操作
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = { 1, 2, 3, 4, 5};
cout << "Size: " << v.size() << endl; // 当前元素个数
cout << "Capacity: " << v.capacity() << endl; // 当前容量
v.resize(10, 100); // 调整大小到10
cout << "After resize: " << v.size() << endl;
v.reserve(20); // 预留空间
cout << "Capacity after reserve: " << v.capacity() << endl;
v.shrink_to_fit(); // 收缩容量
cout << "Capacity after shrink_to_fit: " << v.capacity() << endl;
return 0;
}
输出:
Size: 5
Capacity: 5//说明还没扩容
After resize: 10
Capacity after reserve: 20
Capacity after shrink_to_fit: 10
3.1.2 相关文档
C++ Reference: vector capacity
3.2 动态扩展与性能问题
当 vector
超过当前容量时,会自动扩展存储空间,通常是翻倍。虽然扩展是自动的,但涉及到内存重新分配,因此建议提前使用 reserve()
预留空间,减少不必要的性能开销。
补充:
capacity
的代码在vs和g++下分别运行会发现,vs下capacity
是按1.5倍增长的,g++是按2
倍增长的。这个问题经常会考察,不要固化的认为,
vector
增容都是2倍,具体增长多少是
根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
// 测试vector的默认扩容机制
void TestVectorExpand()
{
size_t sz;
vector<int> v;
sz = v.capacity();
cout << "making v grow:\n";
for (int i = 0; i < 100; ++i)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
vs:运行结果:vs下使用的STL基本是按照1.5倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 3
capacity changed: 4
capacity changed: 6
capacity changed: 9
capacity changed: 13
capacity changed: 19
capacity changed: 28
capacity changed: 42
capacity changed: 63
capacity changed: 94
capacity changed: 141
g++运行结果:linux下使用的STL基本是按照2倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 4
capacity changed: 8
capacity changed: 16
capacity changed: 32
capacity changed: 64
capacity changed: 128
reserve
只负责开辟空间,如果确定知道需要用多少空间,reserve
可以缓解vector增容的代
价缺陷问题。
resize
在开空间的同时还会进行初始化,影响size。
第四章:vector
元素访问与修改
4.1 元素访问方法
方法名 | 功能 |
---|---|
operator[] | 通过下标访问元素,不进行边界检查 |
at(n) | 访问指定位置元素,进行越界检查 |
front() | 返回第一个元素 |
back() | 返回最后一个元素 |
data() | 返回指向数组头部的指针 |
4.1.1 示例:元素访问
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = { 1, 2, 3, 4, 5};
cout << "First element: " << v.front() << endl; // 访问第一个元素
cout << "Last element: " << v.back() << endl; // 访问最后一个元素
try {
cout << v.at(10); // 越界访问
} catch (out_of_range& e) {
cout << "Exception: " << e.what() << endl;
}//异常捕获
return 0;
}
输出:
First element: 1
Last element: 5
Exception: vector::_M_range_check: __n (which is 10) >= this->size() (which is 5)
4.1.2 相关文档
C++ Reference: vector element access
4.2 修改元素
通过 operator[]
或 at()
直接修改元素内容:
v[0] = 10;
v.at(2) = 20;
第五章:vector
的迭代器与遍历
5.1 迭代器
vector
提供了多种迭代器类型,便于对元素进行遍历、修改或访问。
迭代器类型 | 功能 |
---|---|
begin() | 返回指向容器第一个元素的迭代器 |
end() | 返回指向容器末尾的迭代器 |
rbegin() | 返回指向容器最后一个元素的反向迭代器 |
rend() | 返回指向容器第一个元素之前位置的迭代器 |
cbegin() | 常量迭代器,无法修改元素 |
cend() | 常量迭代器,返回指向末尾的常量迭代器 |
)
5.1.1 示例:使用迭代器遍历 <code>vector
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = { 1, 2, 3, 4, 5};
// 使用正向迭代器遍历
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
cout << endl;
// 使用反向迭代器遍历
for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
cout << *rit << " ";
}
cout << endl;
return 0;
}
输出:
1 2 3 4 5
5 4 3 2 1
5.2 使用 <code>for_each() 函数遍历
for_each()
是一种 STL 提供的便捷函数,用于对容器中的每个元素执行指定的操作。
5.2.1 示例:使用 for_each()
遍历并输出元素
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void print(int val) {
cout << val << " ";
}
int main() {
vector<int> v = { 1, 2, 3, 4, 5};
for_each(v.begin(), v.end(), print); // 使用for_each输出
return 0;
}
输出:
1 2 3 4 5
5.2.2 相关链接
C++ vector
迭代器文档STL for_each
文档
5.3 vector
迭代器失效问题(重点)
5.3.1 迭代器失效的原因与后果
vector
迭代器失效的根本原因在于底层内存的重新分配或元素的移除,导致迭代器指向的内存不再有效。当发生迭代器失效时,继续使用该迭代器可能会引发未定义行为,如程序崩溃或访问错误数据。
5.3.2 常见导致迭代器失效的操作
扩容相关操作:当 vector
需要扩展容量时,会分配新的内存空间并将原有元素搬移到新的位置。此时,所有的迭代器将会失效。
resize()
reserve()
insert()
push_back()
assign()
删除操作:删除操作会使指向被删除元素及其后续元素的迭代器失效。
5.3.3 扩容操作导致迭代器失效
示例:扩容导致迭代器失效
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v{ 1, 2, 3, 4, 5, 6};
auto it = v.begin();
// 扩容相关操作导致迭代器失效
v.resize(100, 8); // 扩容并填充元素
// v.reserve(100); // 扩容但不增加元素
// v.push_back(7); // 末尾插入可能引发扩容
// v.assign(100, 8); // 重新赋值并扩容
// 扩容后需要重新获取迭代器
it = v.begin();
while (it != v.end()) {
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
说明:在每次扩容操作后,vector
可能会分配新的内存空间,并释放原来的内存区域。这意味着之前的迭代器已指向失效的内存,因此在扩容操作后,必须重新获取迭代器。
5.3.4 删除操作导致迭代器失效
删除 vector
中的某些元素时,指向被删除元素及其后续元素的迭代器会失效。
示例:删除导致迭代器失效
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v{ 1, 2, 3, 4};
// 查找元素3的迭代器
auto pos = find(v.begin(), v.end(), 3);
// 删除元素3
v.erase(pos);
// 迭代器失效,继续使用将导致程序崩溃或未定义行为
cout << *pos << endl; // 非法访问
return 0;
}
说明:删除某个元素后,指向该元素及其后续元素的迭代器会失效。在删除操作后应重新获取有效的迭代器,以避免出现非法访问或程序崩溃。
5.3.5 删除偶数时的正确和错误写法
错误的删除写法在删除元素后没有正确更新迭代器,会导致迭代器失效,引发未定义行为。
错误示例:
int main() {
vector<int> v{ 1, 2, 3, 4, 4};
auto it = v.begin();
// 错误的删除写法,迭代器未更新
while (it != v.end()) {
if (*it % 2 == 0) {
v.erase(it); // 迭代器失效,++it 导致未定义行为
}
++it;
}
return 0;
}
这里去分析一下会发现it
和end
两个刚好错过了,it就“离家出走”了😂
正确示例:
int main() {
vector<int> v{ 1, 2, 3, 4, 4};
auto it = v.begin();
// 正确的删除写法
while (it != v.end()) {
if (*it % 2 == 0) {
it = v.erase(it); // 返回新的有效迭代器,指向被删除元素的下一个元素
} else {
++it;
}
}
for (int num : v) {
cout << num << " ";
}
return 0;
}
输出:
1 3
5.3.6 编译器对迭代器失效的处理差异
不同编译器(如 GCC 和 MSVC)对迭代器失效的处理方式不同。GCC 在某些情况下可能会宽容处理失效迭代器,程序不会立即崩溃,但输出结果不确定;MSVC 则会直接抛出错误并导致程序崩溃。
示例:GCC 下的宽松处理
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v{ 1, 2, 3, 4, 5};
auto it = find(v.begin(), v.end(), 3);
// 删除元素3
v.erase(it);
// 虽然迭代器失效,但在 GCC 下程序可能不会崩溃
cout << *it << endl; // 输出不确定
return 0;
}
示例:MSVC 下严格处理
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v{ 1, 2, 3, 4, 5};
auto it = find(v.begin(), v.end(), 3);
// 删除元素3
v.erase(it);
// 在 MSVC 下,使用失效迭代器会导致程序崩溃
cout << *it << endl; // 程序崩溃
return 0;
}
5.3.7 扩容后的迭代器失效问题
即使扩容后的程序在 Linux 环境下不会立刻崩溃,但输出结果仍然是不可靠的。以下代码展示了 vector
在 reserve()
扩容后的迭代器失效问题。
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v{ 1, 2, 3, 4, 5};
auto it = v.begin();
cout << "扩容之前,vector的容量为: " << v.capacity() << endl;
// 通过 reserve 扩容
v.reserve(100);
cout << "扩容之后,vector的容量为: " << v.capacity() << endl;
// 迭代器失效,输出结果错误
while (it != v.end()) {
cout << *it << " "; // 输出结果可能错误
++it;
}
cout << endl;
return 0;
}
输出:
1 2 3 4 5 // 正常输出
扩容之前,vector的容量为: 5
扩容之后,vector的容量为: 100
0 2 3 4 5 // 错误输出
5.3.8 总结与建议
避免迭代器失效:进行可能引发迭代器失效的操作(如扩容、删除等)后,必须重新获取迭代器,以保证程序的稳定性。最佳实践:对于 erase()
操作,使用函数返回的迭代器继续遍历,以避免出现迭代器失效问题。编译器差异:不同编译器(如 GCC 和 MSVC)对迭代器失效的处理方式不同,在开发跨平台程序时应尤为注意。
第六章:vector
的插入、删除与修改
6.1 插入操作
vector
提供多种方法用于向容器中插入元素。
方法名 | 功能描述 |
---|---|
push_back() | 在末尾插入一个元素 |
insert() | 在指定位置插入元素 |
emplace_back() | 在末尾直接构造元素,避免不必要的复制开销 |
6.1.1 示例:使用 push_back()
和 insert()
插入元素
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = { 1, 2, 3};
// 在末尾插入
v.push_back(4);
// 在第二个位置插入元素5
v.insert(v.begin() + 1, 5);
for (int val : v) {
cout << val << " ";
}
return 0;
}
输出:
1 5 2 3 4
6.1.2 emplace_back()
与 push_back()
的区别
emplace_back()
直接在容器末尾构造元素,减少了不必要的临时对象生成。适用于复杂对象的插入场景。
6.1.3 示例:使用 emplace_back()
插入元素
#include <iostream>
#include <vector>
using namespace std;
struct Point {
int x, y;
Point(int a, int b) : x(a), y(b) { }
};
int main() {
vector<Point> points;
// 直接在末尾构造对象
points.emplace_back(1, 2);
cout << "Point: " << points[0].x << ", " << points[0].y << endl;
return 0;
}
输出:
Point: 1, 2
6.2 删除操作
vector
提供了多种删除元素的方式,包括删除末尾元素和删除指定位置的元素。
方法名 | 功能描述 |
---|---|
pop_back() | 删除末尾元素 |
erase() | 删除指定位置的元素或一段范围内的元素 |
clear() | 清空整个 vector |
6.2.1 示例:使用 pop_back()
和 erase()
删除元素
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = { 1, 2, 3, 4, 5};
// 删除末尾元素
v.pop_back();
// 删除第一个元素
v.erase(v.begin());
for (int val : v) {
cout << val << " ";
}
return 0;
}
输出:
2 3 4
6.2.2 使用 clear()
清空 vector
v.clear();
cout << "Vector size after clear: " << v.size() << endl; // 输出:0
6.2.3 相关链接
C++ vector::erase()
文档
6.3 修改元素
通过迭代器或下标可以直接修改 vector
中的元素。
6.3.1 示例:修改 vector
元素
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = { 1, 2, 3, 4, 5};
// 修改第二个元素
v[1] = 10;
for (int val : v) {
cout << val << " ";
}
return 0;
}
输出:
1 10 3 4 5
写在最后
在C++标准模板库(STL)中,vector 是最常用的顺序容器之一。本文通过详细的代码示例和深入分析,全面介绍了 vector 的构造、容量管理、元素操作、迭代器使用及失效问题等。我们探讨了如何高效处理扩容、删除、迭代等常见操作,避免潜在的迭代器失效问题。同时,结合不同编译器下的行为差异,帮助读者理解和避免 vector 使用中的常见错误。无论你是初学者还是高级开发者,这篇文章都将助你全面掌握 vector 的使用技巧和性能优化策略。
💬 欢迎讨论:如果你有任何问题或建议,请在评论区留言。
👍 支持一下:如果你觉得这篇文章对你有帮助,请点赞、收藏并分享!你们的支持是我持续创作的动力!
以上就是关于【C++篇】解密 STL 动态之魂:全面掌握 C++ vector 的高效与优雅的内容啦,各位大佬有什么问题欢迎在评论区指正,或者私信我也是可以的啦,您的支持是我创作的最大动力!❤️
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。