C++ vector容器详解
vector
是C++标准模板库(STL)中最常用的序列容器,它实现了动态数组的功能,能够自动管理内存并在运行时动态调整大小。作为C++程序员的必备工具,掌握vector
的使用技巧对提高代码质量和性能至关重要。
基本特性
vector
容器具有以下关键特性:
- 动态数组:底层实现为连续的动态分配数组
- 自动内存管理:自动处理内存分配和释放
- 随机访问:支持常数时间复杂度的随机访问
- 动态扩容:容量不足时自动扩展
- 类型安全:基于模板实现,可存储任意类型
与原生数组相比,vector
提供了更安全、更灵活的接口,同时在性能上几乎没有损失。
头文件与基本用法
头文件引入
使用vector
前必须包含相应头文件:
1#include <vector> // vector的头文件
创建与初始化
vector
提供多种初始化方式,适应不同场景:
1// 默认构造函数
2std::vector<int> vec1; // 空vector
3
4// 指定大小
5std::vector<int> vec2(5); // 包含5个值为0的元素
6
7// 指定大小和初值
8std::vector<std::string> vec3(3, "Hello"); // 包含3个"Hello"字符串
9
10// 使用初始化列表(C++11)
11std::vector<double> vec4{1.1, 2.2, 3.3}; // 包含3个指定的double值
12
13// 从其他容器构造
14int arr[] = {1, 2, 3, 4, 5};
15std::vector<int> vec5(arr, arr + 5); // 从数组构造
16
17// 从其他vector拷贝构造
18std::vector<int> vec6(vec5); // 拷贝vec5的内容
19
20// 使用迭代器范围初始化
21std::vector<int> vec7(vec5.begin() + 1, vec5.end() - 1); // 只包含vec5的部分元素
常用成员函数
容量管理
函数 | 说明 | 复杂度 |
---|---|---|
size() |
返回当前元素数量 | O(1) |
capacity() |
返回当前分配的存储容量 | O(1) |
empty() |
检查容器是否为空 | O(1) |
reserve(n) |
预分配至少能容纳n个元素的内存空间 | O(n) |
shrink_to_fit() |
减少容量以适应当前大小(C++11) | O(n) |
max_size() |
返回可容纳的最大元素数 | O(1) |
resize(n) |
调整容器大小为n个元素 | O(n) |
元素访问
函数 | 说明 | 安全性 |
---|---|---|
operator[] |
通过下标访问元素(不检查边界) | 不安全 |
at(n) |
通过下标访问元素(会检查边界,越界抛出异常) | 安全 |
front() |
访问第一个元素 | 需预先检查非空 |
back() |
访问最后一个元素 | 需预先检查非空 |
data() |
返回指向底层数组的指针(C++11) | 需预先检查非空 |
修改操作
函数 | 说明 | 复杂度 |
---|---|---|
push_back(val) |
在末尾添加元素 | 均摊O(1) |
emplace_back(args) |
在末尾原地构造元素(C++11) | 均摊O(1) |
pop_back() |
移除末尾元素 | O(1) |
insert(pos, val) |
在指定位置插入元素 | O(n) |
emplace(pos, args) |
在指定位置原地构造元素(C++11) | O(n) |
erase(pos) |
移除指定位置的元素 | O(n) |
clear() |
清空所有元素 | O(n) |
assign(n, val) |
替换内容为n个值为val的元素 | O(n) |
swap(other) |
交换两个vector的内容 | O(1) |
实际应用示例
基本操作流程
以下示例展示了vector
的基本操作流程:
1#include <iostream>
2#include <vector>
3#include <string>
4
5int main() {
6 // 创建并初始化vector
7 std::vector<int> numbers = {10, 20, 30, 40, 50};
8
9 // 添加元素
10 numbers.push_back(60); // 在末尾添加元素
11 numbers.insert(numbers.begin() + 2, 25); // 在第3个位置插入25
12
13 // 使用emplace_back直接构造元素(C++11)
14 std::vector<std::pair<std::string, int>> records;
15 records.emplace_back("Alice", 25); // 无需创建临时对象
16
17 // 访问元素
18 std::cout << "第一个元素: " << numbers.front() << std::endl;
19 std::cout << "最后一个元素: " << numbers.back() << std::endl;
20 std::cout << "索引为2的元素: " << numbers[2] << std::endl;
21
22 try {
23 // 安全访问 - 会检查边界
24 std::cout << "安全访问: " << numbers.at(10) << std::endl;
25 } catch (const std::out_of_range& e) {
26 std::cout << "边界检查异常: " << e.what() << std::endl;
27 }
28
29 // 遍历vector (C++11范围for循环)
30 std::cout << "所有元素: ";
31 for (const auto& num : numbers) {
32 std::cout << num << " ";
33 }
34 std::cout << std::endl;
35
36 // 使用迭代器遍历并修改
37 std::cout << "修改后的元素: ";
38 for (auto it = numbers.begin(); it != numbers.end(); ++it) {
39 *it *= 2; // 将每个元素乘以2
40 std::cout << *it << " ";
41 }
42 std::cout << std::endl;
43
44 // 删除元素
45 numbers.pop_back(); // 删除最后一个元素
46 numbers.erase(numbers.begin()); // 删除第一个元素
47
48 // 使用迭代器删除特定元素
49 auto it = std::find(numbers.begin(), numbers.end(), 50);
50 if (it != numbers.end()) {
51 numbers.erase(it); // 删除值为50的元素
52 }
53
54 // 容量信息
55 std::cout << "元素个数: " << numbers.size()
56 << ", 容量: " << numbers.capacity() << std::endl;
57
58 // 释放多余容量
59 numbers.shrink_to_fit();
60 std::cout << "收缩后容量: " << numbers.capacity() << std::endl;
61
62 // 清空vector
63 numbers.clear();
64 std::cout << "清空后大小: " << numbers.size() << std::endl;
65
66 return 0;
67}
自定义类型
vector
可以存储任何类型的元素,包括自定义类型:
1#include <iostream>
2#include <vector>
3#include <string>
4
5class User {
6private:
7 std::string name;
8 int age;
9
10public:
11 User(const std::string& n, int a) : name(n), age(a) {
12 std::cout << "构造: " << name << std::endl;
13 }
14
15 // 移动构造函数(C++11)
16 User(User&& other) noexcept : name(std::move(other.name)), age(other.age) {
17 std::cout << "移动构造: " << name << std::endl;
18 }
19
20 // 拷贝构造函数
21 User(const User& other) : name(other.name), age(other.age) {
22 std::cout << "拷贝构造: " << name << std::endl;
23 }
24
25 void display() const {
26 std::cout << name << ", " << age << "岁" << std::endl;
27 }
28
29 ~User() {
30 std::cout << "析构: " << name << std::endl;
31 }
32};
33
34int main() {
35 std::vector<User> users;
36
37 // 使用push_back添加元素(可能导致拷贝)
38 User alice("Alice", 25);
39 users.push_back(alice); // 调用拷贝构造函数
40
41 // 使用emplace_back直接构造(避免不必要的拷贝)
42 users.emplace_back("Bob", 30); // 直接构造,无需临时对象
43
44 // 使用push_back配合移动语义(C++11)
45 User charlie("Charlie", 35);
46 users.push_back(std::move(charlie)); // 调用移动构造函数
47
48 // 显示所有用户
49 for (const auto& user : users) {
50 user.display();
51 }
52
53 return 0;
54}
内存管理与性能优化
动态增长机制
vector
的内存分配策略是影响性能的关键因素:
1#include <iostream>
2#include <vector>
3
4int main() {
5 std::vector<int> vec;
6 size_t prev_capacity = 0;
7
8 // 观察capacity的变化
9 std::cout << "大小\t容量\t变化倍数" << std::endl;
10 for (int i = 0; i < 100; ++i) {
11 vec.push_back(i);
12
13 if (vec.capacity() != prev_capacity) {
14 double growth = prev_capacity > 0
15 ? static_cast<double>(vec.capacity()) / prev_capacity
16 : 0;
17
18 std::cout << vec.size() << "\t"
19 << vec.capacity() << "\t"
20 << growth << std::endl;
21
22 prev_capacity = vec.capacity();
23 }
24 }
25
26 return 0;
27}
提高性能的技巧
-
预分配空间:避免频繁扩容
1std::vector<int> vec; 2vec.reserve(1000); // 预留1000个元素的空间
-
使用
emplace_back
代替push_back
:减少不必要的拷贝1std::vector<std::pair<int, std::string>> data; 2data.emplace_back(1, "one"); // 直接构造 3data.push_back({2, "two"}); // 创建临时对象再拷贝或移动
-
移动语义与右值引用:减少拷贝开销
1std::string str = "Hello, World!"; 2std::vector<std::string> vec; 3vec.push_back(std::move(str)); // 移动而非拷贝
-
使用
shrink_to_fit
回收多余内存:在内存敏感场景中使用1vec.shrink_to_fit(); // 收缩capacity到size
-
批量操作:减少单元操作次数
1// 一次性插入多个元素 2std::vector<int> source = {1, 2, 3, 4, 5}; 3std::vector<int> dest; 4dest.insert(dest.end(), source.begin(), source.end());
迭代器与陷阱
迭代器失效
迭代器失效是使用vector
时常见的错误源:
1std::vector<int> vec = {1, 2, 3, 4, 5};
2
3// 错误示例:迭代过程中修改容器
4for (auto it = vec.begin(); it != vec.end(); ++it) {
5 if (*it == 3) {
6 vec.push_back(6); // 可能导致迭代器失效
7 }
8}
9
10// 正确示例:先收集需要修改的信息,然后再修改
11std::vector<int> toAdd;
12for (auto it = vec.begin(); it != vec.end(); ++it) {
13 if (*it == 3) {
14 toAdd.push_back(6);
15 }
16}
17for (int val : toAdd) {
18 vec.push_back(val); // 安全地修改
19}
常见陷阱
-
迭代器失效的情况:
- 添加元素导致重新分配内存
- 删除元素后继续使用被删除元素的迭代器
- 对空容器使用front()、back()等函数
-
边界检查:
1// 危险:不检查边界 2int val = vec[100]; // 越界访问不会抛出异常 3 4// 安全:检查边界 5try { 6 int val = vec.at(100); // 越界会抛出std::out_of_range异常 7} catch (const std::out_of_range& e) { 8 std::cerr << "越界访问: " << e.what() << std::endl; 9}
-
引用失效:
1std::vector<int> vec = {1, 2, 3}; 2int& ref = vec[0]; // 获取第一个元素的引用 3vec.push_back(4); // 可能导致重新分配内存 4ref = 10; // 危险:ref可能已经失效
高级应用
二维vector与多维容器
创建和操作多维数据结构:
1// 创建5x4的二维vector,初始值为0
2std::vector<std::vector<int>> matrix(5, std::vector<int>(4, 0));
3
4// 访问和修改元素
5matrix[2][3] = 42;
6
7// 打印二维矩阵
8for (const auto& row : matrix) {
9 for (int val : row) {
10 std::cout << val << " ";
11 }
12 std::cout << std::endl;
13}
14
15// 不规则的二维vector
16std::vector<std::vector<int>> irregular;
17irregular.push_back({1, 2, 3});
18irregular.push_back({4, 5});
19irregular.push_back({6, 7, 8, 9});
配合算法库
1#include <algorithm>
2#include <numeric>
3#include <vector>
4#include <iostream>
5#include <functional> // C++11 函数对象
6
7int main() {
8 std::vector<int> data = {5, 2, 8, 1, 9, 3, 7, 4, 6};
9
10 // 排序
11 std::sort(data.begin(), data.end()); // 升序排序
12
13 // 使用lambda表达式(C++11)降序排序
14 std::sort(data.begin(), data.end(), [](int a, int b) {
15 return a > b;
16 });
17
18 // 查找
19 auto it = std::find(data.begin(), data.end(), 7);
20 if (it != data.end()) {
21 std::cout << "找到7,位置: " << (it - data.begin()) << std::endl;
22 }
23
24 // 计算总和
25 int sum = std::accumulate(data.begin(), data.end(), 0);
26 std::cout << "总和: " << sum << std::endl;
27
28 // 查找最大/最小值
29 auto [min_it, max_it] = std::minmax_element(data.begin(), data.end());
30 std::cout << "最小值: " << *min_it << ", 最大值: " << *max_it << std::endl;
31
32 // 转换元素
33 std::vector<int> squared(data.size());
34 std::transform(data.begin(), data.end(), squared.begin(),
35 [](int x) { return x * x; });
36
37 // 删除满足条件的元素(C++20之前)
38 auto new_end = std::remove_if(data.begin(), data.end(),
39 [](int x) { return x % 2 == 0; });
40 data.erase(new_end, data.end()); // 删除偶数
41
42 return 0;
43}
自定义分配器
高级用例可以使用自定义分配器:
1#include <vector>
2#include <memory>
3#include <iostream>
4
5// 带跟踪功能的简单分配器
6template <typename T>
7class TrackingAllocator {
8public:
9 using value_type = T;
10
11 TrackingAllocator() noexcept = default;
12
13 template <typename U>
14 TrackingAllocator(const TrackingAllocator<U>&) noexcept {}
15
16 T* allocate(std::size_t n) {
17 auto ptr = std::allocator<T>().allocate(n);
18 std::cout << "分配了 " << n << " 个T对象,地址: " << ptr << std::endl;
19 return ptr;
20 }
21
22 void deallocate(T* p, std::size_t n) noexcept {
23 std::cout << "释放了 " << n << " 个T对象,地址: " << p << std::endl;
24 std::allocator<T>().deallocate(p, n);
25 }
26};
27
28int main() {
29 // 使用自定义分配器的vector
30 std::vector<int, TrackingAllocator<int>> vec;
31 vec.reserve(10);
32
33 for (int i = 0; i < 15; ++i) {
34 vec.push_back(i);
35 }
36
37 return 0;
38}
容器比较
vector与其他STL容器
以下表格比较了常见STL容器的特性:
特性 | vector | array | list | deque | forward_list |
---|---|---|---|---|---|
内存布局 | 连续 | 连续 | 不连续 | 分段连续 | 不连续 |
随机访问 | O(1) | O(1) | O(n) | O(1) | 不支持 |
头部插入 | O(n) | 不支持 | O(1) | O(1) | O(1) |
尾部插入 | 均摊O(1) | 不支持 | O(1) | O(1) | 不支持 |
中间插入 | O(n) | 不支持 | O(1) | O(n) | O(1) |
内存开销 | 低 | 极低 | 高 | 中 | 低 |
迭代器稳定性 | 低 | 高 | 高 | 中 | 中 |
适用场景 | 频繁读取、后部修改 | 固定大小集合 | 频繁插入删除 | 两端操作 | 单向链接,内存敏感 |
选择适当的容器
-
选择
vector
的场景:- 需要随机访问元素
- 主要在尾部添加/删除元素
- 需要紧凑的内存布局
- 元素数量变化不频繁
-
考虑其他容器的场景:
- 需要频繁在任意位置插入/删除元素:
list
- 需要在两端频繁添加/删除元素:
deque
- 元素数量固定:
array
或原生数组 - 超大数据集、元素排序很重要:
set
/map
族
- 需要频繁在任意位置插入/删除元素:
最佳实践与总结
有效使用vector的关键点
- 合理预分配容量:对于已知大小的集合,使用
reserve()
避免频繁重新分配 - 利用移动语义:使用C++11的移动语义减少不必要的拷贝
- 使用
emplace
系列函数:减少临时对象的创建和销毁 - 注意迭代器失效:在修改容器后更新迭代器
- 合理选择访问方式:需要安全性时使用
at()
,需要性能时使用operator[]
- 避免频繁的插入/删除操作:尤其是在容器前部和中部
- 使用标准算法:优先使用STL算法而不是自己编写循环
- 在性能关键场景进行基准测试:不同编译器和平台上vector的性能特性可能不同
总结
vector
是C++中最通用、最强大的容器之一,它提供了动态数组的所有功能,同时保持了极高的性能和易用性。掌握vector
的使用技巧和注意事项,可以显著提高C++程序的质量和性能。
关键记忆点:
- 连续内存布局,支持随机访问
- 在尾部添加元素通常很快(均摊O(1))
- 在中间或头部插入/删除元素较慢(O(n))
- 预分配容量可以提高性能
- 注意迭代器失效问题
- 使用C++11/14/17/20的新特性可以进一步提高效率
在大多数需要动态数组功能的场景中,vector
应该是你的首选容器。