1. 理解vector的本质与核心定位
vector是C++标准模板库(STL)中最基础也最重要的容器之一。它的本质是一个动态数组,底层通过连续内存存储元素。这种设计带来了三个关键特性:
-
随机访问能力:由于内存连续,可以通过下标直接访问任意元素,时间复杂度为O(1),与普通数组性能相当。
-
自动扩容机制:当元素数量超过当前容量时,vector会自动分配更大的内存空间并将原有元素迁移过去。这个特性解决了传统数组固定大小的限制。
-
灵活的增删操作:虽然中间插入/删除需要移动元素,但尾插(push_back)和尾删(pop_back)操作非常高效。
实际开发中,vector最适用于元素数量变化不大或主要进行尾部操作的场景。如果频繁在中间位置插入删除,可能需要考虑list等链表结构。
2. vector核心接口详解与实战
2.1 构造与初始化:四种常用方式
vector提供了多种构造方式,适应不同初始化需求:
cpp复制// 1. 默认构造 - 创建空vector
vector<int> v1; // size=0, capacity=0
// 2. 数量+值构造 - 创建包含n个val的vector
vector<int> v2(5, 3); // 包含5个3
// 3. 迭代器范围构造 - 复制另一个容器的区间
int arr[] = {1,2,3,4,5};
vector<int> v3(arr, arr+3); // 复制前3个元素
// 4. 拷贝构造 - 复制另一个vector
vector<int> v4(v3); // v4是v3的副本
// C++11新增的初始化列表方式
vector<int> v5 = {1,2,3,4,5}; // 直接初始化
实际项目中,最常用的是默认构造和初始化列表方式。当需要预分配大量空间时,数量构造也很实用。
2.2 迭代器:统一访问接口
迭代器是STL的核心概念,vector提供了多种迭代器:
cpp复制vector<int> v = {1,2,3,4,5};
// 正向迭代器
for(auto it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
// 反向迭代器
for(auto rit = v.rbegin(); rit != v.rend(); ++rit) {
cout << *rit << " ";
}
// const迭代器(用于const vector)
const vector<int> cv = v;
for(auto it = cv.cbegin(); it != cv.cend(); ++it) {
cout << *it << " ";
}
迭代器的优势在于提供了统一的容器访问接口,使得算法可以独立于具体容器实现。例如,同样的迭代器代码可以用于vector、list等不同容器。
2.3 空间管理与扩容策略
理解vector的size和capacity区别至关重要:
- size:当前存储的元素数量
- capacity:当前分配的内存可以容纳的元素数量
cpp复制vector<int> v;
cout << v.size() << " " << v.capacity() << endl; // 0 0
v.reserve(100); // 预分配空间
cout << v.size() << " " << v.capacity() << endl; // 0 100
for(int i=0; i<50; ++i) v.push_back(i);
cout << v.size() << " " << v.capacity() << endl; // 50 100
不同编译器的扩容策略:
- VS:1.5倍扩容
- GCC:2倍扩容
性能提示:在已知元素数量的情况下,使用reserve()预分配空间可以避免多次扩容带来的性能损耗。
2.4 元素访问与修改
vector提供了多种元素访问方式:
cpp复制vector<int> v = {1,2,3,4,5};
// 1. 下标访问(不检查越界)
cout << v[2] << endl; // 3
// 2. at()方法(会检查越界)
cout << v.at(2) << endl; // 3
// 3. 首尾元素访问
cout << v.front() << endl; // 1
cout << v.back() << endl; // 5
// 4. 数据指针访问
int* p = v.data();
cout << p[2] << endl; // 3
修改操作主要包括:
cpp复制// 尾插元素
v.push_back(6); // 1,2,3,4,5,6
// 尾删元素
v.pop_back(); // 1,2,3,4,5
// 任意位置插入
v.insert(v.begin()+2, 10); // 1,2,10,3,4,5
// 任意位置删除
v.erase(v.begin()+1); // 1,10,3,4,5
3. 高效使用vector的实用技巧
3.1 避免不必要的拷贝
当vector存储大型对象时,拷贝开销可能很大。C++11引入了emplace_back()来避免临时对象的创建:
cpp复制struct Point {
int x, y;
Point(int a, int b) : x(a), y(b) {}
};
vector<Point> v;
v.push_back(Point(1,2)); // 创建临时对象后拷贝
v.emplace_back(1,2); // 直接在容器内构造
3.2 正确使用resize和reserve
- reserve():只改变capacity,不影响size
- resize():改变size,可能增加默认构造的元素
cpp复制vector<int> v;
v.reserve(10); // capacity=10, size=0
v.resize(5); // size=5, 新增的元素被初始化为0
3.3 迭代器失效问题
vector的某些操作会使迭代器失效:
cpp复制vector<int> v = {1,2,3,4,5};
auto it = v.begin() + 2;
v.push_back(6); // 可能导致扩容,所有迭代器失效
// 此时使用it是未定义行为
安全做法是在修改操作后重新获取迭代器。
4. vector性能优化实战
4.1 预分配空间测试
对比有无预分配的性能差异:
cpp复制const int N = 1000000;
// 无预分配
vector<int> v1;
auto start = chrono::high_resolution_clock::now();
for(int i=0; i<N; ++i) v1.push_back(i);
auto end = chrono::high_resolution_clock::now();
// 有预分配
vector<int> v2;
v2.reserve(N);
auto start2 = chrono::high_resolution_clock::now();
for(int i=0; i<N; ++i) v2.push_back(i);
auto end2 = chrono::high_resolution_clock::now();
实测结果显示,预分配可以显著减少运行时间,特别是在大量插入时。
4.2 元素访问性能对比
比较不同访问方式的性能:
cpp复制vector<int> v(1000000, 1);
// 下标访问
auto start = chrono::high_resolution_clock::now();
for(size_t i=0; i<v.size(); ++i) v[i] *= 2;
// 迭代器访问
auto start2 = chrono::high_resolution_clock::now();
for(auto it=v.begin(); it!=v.end(); ++it) *it *= 2;
// 范围for
auto start3 = chrono::high_resolution_clock::now();
for(auto& x : v) x *= 2;
三种方式性能相当,编译器通常会优化为相似的机器代码。
5. 常见问题与解决方案
5.1 迭代器失效场景
cpp复制vector<int> v = {1,2,3,4,5};
// 错误示例1:插入导致扩容
auto it = v.begin() + 2;
v.push_back(6); // 可能扩容
*it = 10; // 危险!it可能失效
// 错误示例2:删除元素
for(auto it=v.begin(); it!=v.end(); ) {
if(*it % 2 == 0) {
it = v.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
5.2 性能陷阱
cpp复制// 低效:频繁在头部插入
vector<int> v;
for(int i=0; i<1000; ++i) {
v.insert(v.begin(), i); // 每次都要移动所有元素
}
// 改进:改用deque或list
deque<int> d;
for(int i=0; i<1000; ++i) {
d.push_front(i); // 高效
}
5.3 内存管理
cpp复制// 清空vector但保留容量
vector<int> v(1000, 1);
v.clear(); // size=0, capacity不变
// 彻底释放内存(C++11)
vector<int>().swap(v); // 与空vector交换
// 或者
v.shrink_to_fit(); // 请求减少capacity到size
6. vector在算法题中的应用
6.1 两数之和问题
cpp复制vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> map;
for(int i=0; i<nums.size(); ++i) {
int complement = target - nums[i];
if(map.count(complement)) {
return {map[complement], i};
}
map[nums[i]] = i;
}
return {};
}
6.2 旋转数组问题
cpp复制void rotate(vector<int>& nums, int k) {
k %= nums.size();
reverse(nums.begin(), nums.end());
reverse(nums.begin(), nums.begin()+k);
reverse(nums.begin()+k, nums.end());
}
6.3 合并有序数组
cpp复制void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int i = m-1, j = n-1, k = m+n-1;
while(j >= 0) {
nums1[k--] = (i >=0 && nums1[i] > nums2[j]) ? nums1[i--] : nums2[j--];
}
}
在实际开发中,vector是最常用的容器之一。掌握它的特性和正确使用方法,可以写出更高效、更安全的C++代码。特别是在算法竞赛和面试中,vector的使用技巧往往是考察的重点。