C++中初始化二维数组的几种常见方法
前言
二维数组的初始化看起来是入门话题,但真正写起来,选择不少:原生数组、std::array 的嵌套、std::vector 的嵌套、用一维数组模拟、以及 new 出来的动态二维数组。每种方式的内存布局、初始化语法、释放方式和传参规则都不一样,混用就会出错。
最常见的误解是「int a[3][4] = {1, 2, 3}; 会把 1、2、3 分别放进三行的第一个位置」。实际上初始化列表是按行优先(row-major)顺序依次填的:1、2、3 落在第一行的前三列,其余元素补零。另一个高频错误是把 std::vector<std::vector<int>> 当成连续内存,直接传给需要 int* 的 C 接口。
还有一个容易被忽略的点:变长数组(VLA)int a[n][m] 在 C99 里合法,但不是标准 C++。GCC 和 Clang 把它作为扩展支持,MSVC 不支持,用了就失去可移植性。
本文按「原生数组 → std::array → std::vector → 一维模拟 → 动态分配」的顺序讲,每个都给出初始化和访问的例子,最后说明传参与释放的注意事项。
一、原生二维数组
1.1 完全初始化
// 适用于 C++17
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};内层花括号可以省略,效果相同:
// 适用于 C++17
int b[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};编译器按行优先顺序依次填充,写成一行的形式可读性差,不推荐。
1.2 部分初始化
// 适用于 C++17
int c[3][4] = {{1}, {2}, {3}}; // 每行首列为 1/2/3,其余为 0
int d[3][4] = {0}; // 全部为 0
int e[3][4] = {}; // 全部为 0(C++11 起)没有被显式初始化的元素会被值初始化为零。这一点对内置类型也成立:只要出现了初始化列表,剩余元素就是 0,而不是不确定值。相反,int f[3][4]; 这样的定义(无初始化器)在块作用域内元素是未初始化的,读取它们是未定义行为。
1.3 省略第一维
定义时第一维可以省略,编译器根据初始化列表推断:
// 适用于 C++17
int g[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}}; // 推断为 int[2][4]第二维及以后不能省略,因为编译器需要知道每行多长才能计算内存布局。
1.4 内存布局
原生二维数组在内存中是连续的,按行优先排列:a[0][0]、a[0][1]、…、a[0][3]、a[1][0]、…。因此 &a[0][0] + i * 4 + j 就是 a[i][j] 的地址。这一点对缓存友好,也是它能直接传给 C 接口的原因。
二、std::array 与 std::vector 的嵌套
std::array 是固定大小的容器,嵌套两层就得到二维数组:
// 适用于 C++17
#include <array>
std::array<std::array<int, 4>, 3> grid{{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}};注意初始化时用了双层花括号:外层是 std::array 的聚合初始化,内层是它的成员(内置数组)的初始化。由于聚合初始化的花括号省略规则,只写一层通常也能成立,但双层写法意图更清楚,很多代码风格指南仍要求保留。
全零初始化:
// 适用于 C++17
std::array<std::array<int, 4>, 3> zeros{}; // 所有元素为 0std::array 的优点是有 size()、可以整体赋值、可以按值传递(虽然通常还是传引用),并且内存布局与原生数组一致,data() 返回指向首元素的指针。
2.1 std::vector 的嵌套
大小在运行期决定时,用 std::vector<std::vector<int>>:
// 适用于 C++17
#include <vector>
// 3 行 4 列,全部为 0
std::vector<std::vector<int>> grid(3, std::vector<int>(4, 0));
// 用初始化列表构造
std::vector<std::vector<int>> init{
{1, 2, 3, 4},
{5, 6, 7, 8}
};这里有一个经典的陷阱:
// 适用于 C++17
std::vector<std::vector<int>> wrong{3, std::vector<int>(4)};这不是「3 行 4 列」,而是「包含两个元素的 vector」:一个是 3(被隐式转换成一个只含 3 的 vector<int>),另一个是长度为 4 的 vector。花括号优先匹配 initializer_list 构造函数。正确写法用圆括号:(3, std::vector<int>(4, 0))。
vector 嵌套的内层 vector 各自独立分配,整体不是连续内存。grid[0].data() 和 grid[1].data() 之间没有保证的相邻关系,不能把 &grid[0][0] 当作 rows * cols 个连续元素使用。
三、一维数组模拟二维
需要连续内存又要运行期尺寸时,用一维 std::vector 加下标换算:
// 适用于 C++17
#include <cstddef>
#include <vector>
const std::size_t rows = 3;
const std::size_t cols = 4;
std::vector<int> grid(rows * cols, 0);
auto at = [&](std::size_t r, std::size_t c) -> int& {
return grid[r * cols + c];
};
at(0, 0) = 1;
at(2, 3) = 12;优点:内存连续、一次分配、缓存友好、可以直接传给需要连续 int 缓冲的接口(如图像库)。缺点:下标要自己算,容易写错行列顺序;访问时没有边界检查。
C++23 提供了 std::mdspan 作为多维视图,可以在不拥有内存的前提下提供多维下标,但它的编译器支持情况差异较大,使用前应确认工具链。C++17 下可以用一个简单的包装类达到类似效果。
| 方式 | 内存连续 | 尺寸确定时机 | 释放方式 |
|---|
原生 int[3][4] | 是 | 编译期 | 自动 |
std::array 嵌套 | 是 | 编译期 | 自动 |
vector<vector<int>> | 否 | 运行期 | 自动 |
一维 vector 模拟 | 是 | 运行期 | 自动 |
new int[3][4] | 是 | 第一维可运行期 | delete[] |
四、动态分配与传参
// 适用于 C++17
int rows = 3;
int cols = 4;
int (*grid)[4] = new int[rows][4]; // 只有第一维可以是变量
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 4; ++j) {
grid[i][j] = 0;
}
}
delete[] grid; // 必须用 delete[]new int[rows][4] 返回的类型是「指向 int[4] 的指针」,即 int (*)[4]。除第一维外,其余维度必须是编译期常量。释放用 delete[],用 delete 是未定义行为。
如果两维都需要运行期决定,标准做法是分配一维再换算,或者分配指针数组再逐行分配(后者不连续,释放要逐行):
// 适用于 C++17
int** rowsPtr = new int*[rows];
for (int i = 0; i < rows; ++i) {
rowsPtr[i] = new int[cols];
}
// 释放
for (int i = 0; i < rows; ++i) {
delete[] rowsPtr[i];
}
delete[] rowsPtr;现代 C++ 更推荐用 std::vector 或一维 vector 模拟,避免手动配对 new[]/delete[]。裸 new 只在需要与 C 接口交互、或实现底层容器时才有必要。
4.1 传参
原生二维数组作为函数参数时,会退化为指向第一行的指针,第二维必须写出来:
// 适用于 C++17
void fill(int arr[][4], int rows) { // 等价于 int (*arr)[4]
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 4; ++j) {
arr[i][j] = i * 4 + j;
}
}
}std::array 和 std::vector 用引用传参,避免拷贝:
// 适用于 C++17
void use(const std::array<std::array<int, 4>, 3>& grid);
void use(const std::vector<std::vector<int>>& grid);完整示例
// 适用于 C++17
#include <array>
#include <iostream>
#include <vector>
int main() {
int native[2][3] = {{1, 2, 3}, {4, 5, 6}};
std::array<std::array<int, 3>, 2> arr{{
{1, 2, 3},
{4, 5, 6}
}};
std::vector<std::vector<int>> vec(2, std::vector<int>(3, 0));
vec[1][2] = 6;
std::vector<int> flat(2 * 3, 0);
flat[1 * 3 + 2] = 6;
int partial[2][3] = {{7}, {8}};
std::cout << native[1][1] << ' ' << arr[0][2] << ' '
<< vec[1][2] << ' ' << flat[5] << '\n';
std::cout << partial[0][1] << ' ' << partial[1][0] << '\n';
return 0;
}按标准语义,第一行输出 5 3 6 6;partial 的第一行是 {7, 0, 0},第二行是 {8, 0, 0},所以第二行输出 0 8。
常见坑点
- ❌ 使用变长数组:
``cpp int n = 10; int a[n][n]; // 不是标准 C++ ` ✅ 用 std::vector<std::vector<int>> 或一维 std::vector`。
- ❌ 以为
{1, 2, 3}是逐行填充:
``cpp int a[2][3] = {1, 2, 3}; // 全在第一行,第二行全 0 ` ✅ 明确写内层花括号:{{1, 2, 3}, {4, 5, 6}}`。
- ❌
new int[3][4]用delete释放:
``cpp delete grid; // UB ` ✅ delete[] grid;`。
- ❌ 用花括号构造
vector<vector<int>>的行列:
``cpp std::vector<std::vector<int>> g{3, std::vector<int>(4)}; // 2 个元素 ` ✅ 用圆括号:g(3, std::vector<int>(4, 0))`。
- ❌ 把
vector<vector<int>>当连续内存传给 C 接口:
``cpp c_function(&grid[0][0]); // 只有第一行是连续的 ` ✅ 改用一维 std::vector<int>,传 grid.data()`。
- ❌ 函数参数写
void f(int a[][]):
``cpp void f(int a[][]); // 编译错误 ` ✅ 写出第二维:void f(int a[][4], int rows)`。
- ❌ 用
memset初始化含非平凡类型的二维vector:
``cpp std::memset(&grid[0][0], 0, sizeof(int) * n); // 对 vector 无效且越界 ` ✅ 用构造函数或 std::fill`。
- ❌ 读取未初始化的局部数组:
``cpp int a[3][3]; int x = a[0][0]; // 未定义行为 ` ✅ 定义时初始化:int a[3][3] = {};`。
总结
| 场景 | 推荐方式 |
|---|
| 尺寸编译期固定、追求性能 | 原生数组或 std::array 嵌套 |
| 尺寸运行期决定、行独立 | std::vector<std::vector<int>> |
| 尺寸运行期决定、要连续内存 | 一维 std::vector + 下标换算 |
| 与 C 接口交互 | 一维连续缓冲或 new int[n][4] |
| 需要避免手动释放 | 一律用容器 |
二维数组的选择本质是三个维度的权衡:尺寸何时确定、内存是否要求连续、由谁负责释放。把这三点的答案定下来,写法自然就确定了。初始化时记住「按行优先、缺省补零、花括号匹配 initializer_list」三条,大部分坑都能提前避开。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)