一、引言
在 C++ 编程中,我们经常需要处理一组数据。比如,你想存储一个班级所有学生的成绩,或者保存用户输入的一组数字。最容易想到的方法是使用数组:
cpp
int scores[100]; // 定义一个能存储100个成绩的数组
但数组有两个明显的缺点:
- 大小固定:一旦定义,无法动态调整。如果实际学生超过 100 人,数组就不够用了。
- 内存管理麻烦:需要手动分配和释放内存(在使用动态数组时)。
vector 就是为解决这些问题而生的! 它是 C++ 标准库提供的动态数组,可以自动管理内存,还支持各种方便的操作
二、vector 基础:快速上手
2.1 如何使用 vector?
要使用 vector,首先需要包含头文件:
cpp
#include <vector>
using namespace std; // 为了简化代码,使用标准命名空间
2.2 创建 vector 对象
vector 的使用非常灵活,可以根据需要创建不同类型和初始值的 vector:
cpp
// 1. 创建空的 vector(最常用)
vector<int> v1; // 存储整数的 vector
// 2. 创建包含 n 个元素的 vector
vector<double> v2(5); // 创建包含 5 个 double 的 vector,初始值为 0.0
// 3. 创建包含 n 个指定值的 vector
vector<string> v3(3, "hello"); // 创建包含 3 个 "hello" 的 vector
// 4. 使用现有数组或其他 vector 初始化
int arr[] = {1, 2, 3, 4};
vector<int> v4(arr, arr + 4); // 使用数组初始化
vector<int> v5(v4); // 使用另一个 vector 初始化
2.3 常用操作:增删查改
下面是 vector 最常用的操作,新手掌握这些就可以应对大部分场景:
cpp
vector<int> v; // 创建空的 vector
// 1. 添加元素(最常用)
v.push_back(10); // 在尾部添加元素 10
v.push_back(20); // 在尾部添加元素 20
v.push_back(30); // 在尾部添加元素 30
// 此时 v 中的元素是:[10, 20, 30]
// 2. 访问元素
cout << v[0] << endl; // 输出第 0 个元素:10
cout << v.at(1) << endl; // 输出第 1 个元素:20(更安全,会检查越界)
// 3. 修改元素
v[0] = 100; // 将第 0 个元素修改为 100
// 此时 v 中的元素是:[100, 20, 30]
// 4. 删除元素
v.pop_back(); // 删除最后一个元素
// 此时 v 中的元素是:[100, 20]
// 5. 获取 vector 大小
cout << v.size() << endl; // 输出 2(当前有 2 个元素)
// 6. 判断 vector 是否为空
if (v.empty()) {
cout << "vector 为空" << endl;
} else {
cout << "vector 不为空" << endl;
}
// 7. 清空 vector
v.clear(); // 删除所有元素
cout << v.size() << endl; // 输出 0(vector 现在为空)
三、如何遍历 vector
遍历 vector 中的元素是常见需求,有多种方法可以实现:
3.1 使用下标遍历(类似数组)
cpp
vector<int> v = {1, 2, 3, 4, 5};
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
// 输出:1 2 3 4 5
3.2 使用迭代器遍历(更通用)
迭代器是一种类似指针的对象,用于访问容器中的元素。所有标准库容器都支持迭代器:
cpp
vector<int> v = {1, 2, 3, 4, 5};
// 正向迭代器
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it << " "; // 使用 *it 访问当前元素
}
// 输出:1 2 3 4 5
// 反向迭代器(从后往前遍历)
for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
cout << *rit << " ";
}
// 输出:5 4 3 2 1
3.3 使用范围 for 循环(C++11 及以后,最简单)
cpp
vector<int> v = {1, 2, 3, 4, 5};
for (int num : v) {
cout << num << " ";
}
// 输出:1 2 3 4 5
// 如果需要修改元素,可以使用引用
for (int& num : v) {
num *= 2; // 将每个元素乘以 2
}
// 此时 v 中的元素是:[2, 4, 6, 8, 10]
四、vector 的高级用法
4.1 存储自定义类型
vector 可以存储任何类型,包括自定义的类或结构体:
cpp
struct Student {
string name;
int age;
};
vector<Student> students;
// 添加元素
students.push_back({"Alice", 20});
students.push_back({"Bob", 21});
// 访问元素
for (const auto& s : students) {
cout << s.name << " " << s.age << endl;
}
// 输出:
// Alice 20
// Bob 21
4.2 二维 vector
可以创建多维 vector,最常见的是二维 vector(类似二维数组):
cpp
// 创建一个 3x4 的矩阵,初始值为 0
vector<vector<int>> matrix(3, vector<int>(4, 0));
// 赋值
matrix[0][0] = 1;
matrix[1][2] = 5;
// 遍历
for (const auto& row : matrix) {
for (int num : row) {
cout << num << " ";
}
cout << endl;
}
// 输出:
// 1 0 0 0
// 0 0 5 0
// 0 0 0 0
4.3 排序和查找
vector没有排序和查找的函数,但是可以结合标准库算法进行排序和查找:
cpp
#include <algorithm> // 需要包含算法库
vector<int> v = {3, 1, 4, 1, 5, 9};
// 排序
sort(v.begin(), v.end()); // 升序排序
// v 现在是:[1, 1, 3, 4, 5, 9]
// 查找
auto it = find(v.begin(), v.end(), 4);
if (it != v.end()) {
cout << "找到元素 4,位置是:" << (it - v.begin()) << endl;
} else {
cout << "未找到元素 4" << endl;
}
五、vector 底层原理与内存管理
5.1 核心数据结构
vector
的底层通过三个指针实现动态数组的管理(不同平台实现不同):
T* _start
:指向数据存储区的起始位置。T* _finish
:指向最后一个有效元素的下一个位置。T* _end_of_storage
:指向已分配内存空间的末尾位置。
这三个指针构成了 vector
的内存模型:
cpp
template<typename T>
class vector {
private:
T* _start;
T* _finish;
T* _end_of_storage;
};
5.2 扩容机制详解
vector 的最大优势之一是可以动态扩容,不需要我们手动管理内存。但它是如何做到的呢?
5.2.1 容量 vs 大小
- 大小(size):当前实际存储的元素个数。
- 容量(capacity):当前分配的内存能够容纳的元素个数。
当 vector
空间不足时,会触发扩容:
-
计算新容量:不同编译器采用不同策略:
- GCC(SGI STL):以 2 倍扩容(如容量为 4 时,扩容后为 8)。
- MSVC(VS):以 1.5 倍扩容(如容量为 4 时,扩容后为 6)。
- CLANG:与 MSVC 类似,采用 1.5 倍扩容。
-
分配新内存 :使用
operator new
分配更大的内存块。 -
数据迁移 :通过
uninitialized_copy
将旧数据迁移到新空间。 -
释放旧内存 :调用
operator delete
释放旧内存块。
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
size_t cap = v.capacity();
cout << "初始容量:" << cap << endl; // 0
for (int i = 0; i < 10; ++i) {
v.push_back(i);
if (cap != v.capacity()) {
cap = v.capacity();
cout << "扩容后容量:" << cap << endl;
// GCC 输出:1, 2, 4, 8, 16...;VS 输出:1, 2, 3, 4, 6...
}
}
return 0;
}
5.2.2 如何避免不必要的扩容?
如果事先知道需要存储多少元素,可以使用 reserve()
方法预分配内存:
cpp
vector<int> v;
v.reserve(100); // 预分配 100 个元素的空间
for (int i = 0; i < 100; i++) {
v.push_back(i); // 不会触发扩容,效率更高
}
扩容策略的数学分析:
- 若扩容因子为
m
,插入n
个元素的均摊时间复杂度为O(n)
,因为每次扩容复制的元素数量呈几何级数增长,总和为O(n)
。
代码示例:
观察扩容行为:
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
size_t cap = v.capacity();
cout << "初始容量:" << cap << endl; // 0
for (int i = 0; i < 10; ++i) {
v.push_back(i);
if (cap != v.capacity()) {
cap = v.capacity();
cout << "扩容后容量:" << cap << endl;
// GCC 输出:1, 2, 4, 8, 16...;VS 输出:1, 2, 3, 4, 6...
}
}
return 0;
}
5.3 模拟实现关键函数
5.3.1 拷贝构造函数
cpp
template<typename InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
first++;
}
}
vector(const vector<T>& v)
{
vector<T> tmp(v.begin(), v.end()); // 利用迭代器区间构造临时对象
swap(tmp); // 交换资源,实现深拷贝
}
vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
}
5.3.2 赋值运算符重载
cpp
vector<T>& operator=(vector<T> tmp) { // 参数为值传递,自动调用拷贝构造
swap(tmp); // 交换资源,实现异常安全
return *this;
}
六、迭代器失效场景与解决方案
6.1 导致迭代器失效的操作
- 空间重新分配 :
resize
、reserve
、insert
、push_back
、assign
等操作可能触发扩容,导致原有迭代器失效。 - 元素删除 :
erase
操作会使被删除位置之后的迭代器失效。 - 容器交换 :
swap
操作会交换两个vector
的内容,导致原迭代器指向其他容器。 - 容器清空 :
clear
操作会删除所有元素,迭代器失效。
6.2 典型错误与修正
6.2.1 错误示例:未处理扩容导致的迭代器失效
cpp
vector<int> v = {1, 2, 3, 4};
auto it = v.begin();
v.insert(it, 0); // 可能触发扩容
cout << *it << endl; // 未定义行为,it 已失效
6.2.2 正确做法:重新获取迭代器
vector<int> v = {1, 2, 3, 4};
auto it = v.begin();
it = v.insert(it, 0); // insert 返回新元素的迭代器
cout << *it << endl; // 正确输出 0
6.3 安全遍历与删除
6.3.1 错误示例:未更新迭代器
cpp
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it % 2 == 0) {
v.erase(it); // erase 后 it 失效
}
}
6.3.2 正确做法:使用 erase 的返回值
cpp
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
it = v.erase(it); // 更新 it 到下一个有效位置
} else {
++it;
}
}
七、常见问题与陷阱
7.1 浅拷贝问题
- 场景 :当
vector
存储包含动态资源的自定义类型时,默认拷贝构造函数会导致浅拷贝。 - 解决方案 :
- 为自定义类型实现深拷贝构造函数和赋值运算符。
- 使用
vector
的默认深拷贝机制(基于元素的拷贝构造函数)。
7.2 迭代器失效与编译器差异
- VS 编译器:对迭代器失效检测严格,访问失效迭代器可能直接崩溃。
- GCC 编译器:检测较宽松,但仍可能导致未定义行为。
- 建议:避免依赖编译器行为,严格处理迭代器失效。
7.3 内存泄漏
-
场景 :使用
reserve
预分配空间后,直接通过下标访问未初始化的元素。 -
示例 :
cppvector<int> v; v.reserve(10); v[5] = 42; // 未定义行为,未初始化的内存
-
修正 :使用
resize
初始化元素:cppvector<int> v; v.resize(10); v[5] = 42; // 合法
八、实战案例:高效算法实现
8.1 杨辉三角
cpp
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for(int i=0;i<numRows;i++)
{
vv[i].resize(i+1,1);
}
for(int i=2;i<numRows;i++)
{
for(int j=1;j<i;j++)
{
vv[i][j]=vv[i-1][j]+vv[i-1][j-1];
}
}
return vv;
}
8.2 只出现一次的数字
cpp
int singleNumber(vector<int>& nums) {
int result = 0;
for (int num : nums) {
result ^= num; // 异或相同数得 0,0 异或任意数得原数
}
return result;
}
九、总结与最佳实践
7.1 核心优势
- 随机访问高效:支持 O (1) 时间复杂度的下标访问。
- 动态扩容:自动管理内存,避免手动分配 / 释放。
- 丰富接口 :提供
push_back
、insert
、erase
等便捷操作。
7.2 最佳实践
- 预分配空间 :使用
reserve
避免频繁扩容。 - 优先使用 emplace_back:减少拷贝 / 移动开销。
- 处理迭代器失效 :在
insert
/erase
后更新迭代器。
通过深入理解 vector
的底层原理和正确使用其接口,开发者可以高效地处理动态数据,避免常见错误,并在实际项目中充分发挥其性能优势。建议结合模拟实现和算法练习(如 LeetCode 题目)进一步巩固知识。
附录
模拟实现(代码)
cpp
//vector.h
#include<iostream>
#include<assert.h>
namespace My_vector
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
vector()
{ }
vector(size_t n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
}
template<typename InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
first++;
}
}
vector(std::initializer_list<T> il)
{
reserve(il.size());
for (auto& e : il)
{
push_back(e);
}
}
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
}
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin()const
{
return _start;
}
const_iterator end()const
{
return _finish;
}
size_t capacity()const
{
return _end_of_storage - _start;
}
size_t size()const
{
return _finish - _start;
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t old_size = size();
iterator tmp = new T[n];
if (_start)
{
for (size_t i = 0; i < old_size ;i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = tmp + old_size;
_end_of_storage = tmp + n;
}
}
void push_back(const T& val)
{
if (_finish == _end_of_storage)
{
size_t newcapacity = 0 == capacity() ? 4 : 2 * capacity();
reserve(newcapacity);
}
*_finish = val;
_finish++;
}
void pop_back()
{
assert(_finish > _start);
--_finish;
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
void resize(size_t n, const T& val=T())
{
if (n > size())
{
if (n > capacity())
{
reserve(n);
}
while (_finish != _end_of_storage)
{
push_back(val);
}
}
else
{
_finish = _start + n;
}
}
iterator insert(iterator pos,const T& val)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + len;
}
iterator it = _finish - 1;
while (it >= pos)
{
*(it + 1) = *it;
it--;
}
*pos = val;
_finish++;
return pos;
}
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos <= _finish);
iterator it = pos;
while (it < _finish)
{
*it = *(it + 1);
it++;
}
_finish--;
return pos;
}
T& operator[](size_t n)
{
assert(n < size());
return _start[n];
}
vector<T>& operator=(vector<T> tmp)
{
swap(tmp);
return *this;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}