C++标准库提供的vector容器可以像内置数组一样连续存储元素,而把vector本身作为另一个vector的元素类型,就得到了嵌套结构,也就是俗称的vector二维数组。相比传统的int arr[3][4]这种静态数组,嵌套vector最大的优势是行列维度都可以在运行期确定,不需要提前写死大小,这在处理动态输入、图结构、矩阵运算时非常实用。本文将从定义、初始化、访问、动态调整四个层面把这个问题彻底讲透。

一、vector二维数组的定义与几种初始化方式
定义一个二维vector的核心语法是把元素类型写成vector<int>,外层再套一层vector。最基础的定义如下:
#include <vector> using namespace std; // 只定义,不初始化,此时是空的,行数和列数都是0 vector<vector<int>> matrix;
注意两个尖括号之间要留空格的说法是C++98时代的旧问题,C++11之后编译器已经能正确解析>>,直接连写完全没有问题。空定义虽然安全,但直接访问必然越界,所以更多时候我们会在定义时就指定行列:
// 初始化为3行4列,所有元素默认为0
vector<vector<int>> matrix(3, vector<int>(4, 0));
// 3行4列,全部填充成整数9
vector<vector<int>> grid(3, vector<int>(4, 9));
// C++11列表初始化,直接写出每一行的内容
vector<vector<int>> table = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9, 10} // 每行列数甚至可以不同
};
构造函数vector(3, vector<int>(4, 0))的含义需要理解清楚:第二个参数是一个临时的一维vector对象,外层构造函数会把它复制三份,得到三个独立的行。这三个行互不影响,修改其中一行的数据不会牵连其他行,这一点和某些语言中“引用同一行”的行为完全不同。
列表初始化则更灵活,它允许每一行的长度不一样,这就是所谓的“锯齿状”结构。比如存储图的邻接表时,第0个顶点有2条边,第1个顶点有5条边,用等宽矩阵反而浪费内存,锯齿结构才是正确选择。
二、访问元素:下标、at与范围for循环
访问嵌套vector的元素使用双层下标,写法是matrix[i][j],其中i表示行索引,j表示列索引,都从0开始。先取到第i行这个一维vector,再在它上面取第j个元素:
vector<vector<int>> matrix(3, vector<int>(4, 0));
matrix[1][2] = 88; // 修改第2行第3列的元素
int val = matrix[1][2]; // 读取该元素
// 遍历方式一:传统下标循环
for (size_t i = 0; i < matrix.size(); ++i) {
for (size_t j = 0; j < matrix[i].size(); ++j) {
cout << matrix[i][j] << " ";
}
cout << endl;
}
// 遍历方式二:范围for循环配合引用,避免拷贝
for (const auto& row : matrix) {
for (int x : row) {
cout << x << " ";
}
cout << endl;
}
这里有个容易踩的坑:内层循环的边界必须写成matrix[i].size()而不是matrix[0].size(),因为锯齿结构的每一行长度可能不同。另外范围for循环中,row一定要用引用const auto&接收,如果写成auto row,每一行都会被完整拷贝一份,行越长性能损失越大,在矩阵规模上千时这个差异非常明显。
下标操作符operator[]不做边界检查,越界访问是未定义行为,程序可能崩溃也可能悄悄读出脏数据。如果对安全性要求高,或者索引来自外部输入,建议改用at()成员函数,它会在越界时抛出out_of_range异常,方便捕获处理:
try {
int v = matrix.at(10).at(0); // 只有3行,访问第10行必然越界
} catch (const out_of_range& e) {
cout << "越界了: " << e.what() << endl;
}
三、动态调整:resize、push_back与常见错误
嵌套vector的行和列都可以在运行期动态改变,这是它相比静态数组最核心的价值。常用手段有两个:对已存在的行调用resize,以及用push_back追加新行:
vector<vector<int>> matrix;
// 方法一:先resize出行数,再逐行resize列数
matrix.resize(3); // 现在有3个空行
for (auto& row : matrix) {
row.resize(4, 0); // 每行扩到4列,补0
}
// 方法二:构造好一整行再push_back进去
vector<int> newRow(4, 1);
matrix.push_back(newRow); // 追加一行
matrix.push_back({1, 2, 3}); // C++11可以直接塞初始化列表
// 一次性整体重设为5行6列
matrix.assign(5, vector<int>(6, 0));
新手最常见的错误是定义了空的二维vector就直接下标访问,比如matrix[0][0] = 1,此时外层vector一个元素都没有,matrix[0]本身就是越界,程序大概率直接段错误。正确的做法永远是先resize或者push_back保证行存在,再操作行内元素。排查这类崩溃时,可以用matrix.size()和matrix[i].size()打印实际维度,和自己的预期对一遍。
还有一种隐蔽的错误是以为resize(m, vector<int>(n))里的那个一维vector会被共享,实际上resize的第二个参数同样会被复制m份,每一行都是独立副本,因此对某一行做后续resize不会影响其他行的长度。理解了“复制而非引用”这一点,嵌套vector的大部分行为就都能推出来了。
四、vector二维数组与静态数组、vector动态数组的对比
最后从工程角度对比一下三种方案。静态数组int arr[3][4]内存连续、访问最快,但维度必须编译期常量,无法按需伸缩;嵌套vector灵活性最高,行列皆可动态,还支持锯齿结构,代价是每一行单独分配内存,整体不连续,缓存友好性稍差;此外vector<int>本身也适合一维场景。三者的取舍可以参考下表:
| 方案 | 维度灵活性 | 内存布局 | 适用场景 |
|---|---|---|---|
| int arr[3][4] | 编译期固定 | 完全连续 | 维度已知的小规模矩阵 |
| vector<vector<int>> | 行列均可动态 | 按行分散 | 动态输入、邻接表、锯齿结构 |
| vector<int>配合手动下标换算 | 一维动态 | 完全连续 | 高性能矩阵运算 |
| at()访问的嵌套vector | 行列均可动态 | 按行分散 | 索引不可信、需要异常保护的场合 |
顺带一提,如果追求极致性能,可以把二维数组拍平成一维vector,用data[i * cols + j]计算下标,这样所有元素连续存储,缓存命中率更高,矩阵乘法这类密集计算能获得可观的加速。而对大多数业务代码来说,嵌套vector的可读性和安全性收益更大,不必过度优化。
总结一下核心要点:定义用vector<vector<int>>,初始化优先考虑指定行列的构造函数或列表初始化,访问时内层边界要用matrix[i].size(),动态添加行用push_back,扩展已有行用resize,对不可信索引改用at()并捕获异常。掌握这些,vector二维数组的使用就不会再出问题。
C++ vector二维数组嵌套vectorvector初始化修改时间:2026-09-05 06:48:40