目录
什么是二维数组?
二维数组的声明方式
方式 1:静态二维数组
方式 2:数组指针数组(数组中存放的是指针)
方式 3:双指针 + 二级堆分配
💡 补充建议
如何用“第一性原理”去推导出 C++ 中二维数组的三种声明方式?
第一阶段:内存连续,列固定,行固定 → 推导出方式①
第二阶段:每行独立、列可能不同(不规则矩阵) → 推导出方式②
第三阶段:行数和列数都是运行时才知道的 → 推导出方式③
什么是二维数组?
二维数组本质上是“数组的数组”,你可以将它看作一个矩阵,每个元素有两个索引:
int A[3][4];
表示一个包含 3 行 4 列 的整数矩阵。
访问方式是 A[row][col]
,例如 A[1][2]
表示第 2 行第 3 列。
二维数组的声明方式
我们来逐一讲解,并图示结构 + 内存布局图解:
方式 1:静态二维数组
int A[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10,11,12}
};
结构理解:
这是一个连续内存块,编译器分配的实际大小为 3 * 4 * sizeof(int)
,总共 12 个整型元素。
图示结构:
A: → 3 行 × 4 列的矩阵(行优先排列)[1, 2, 3, 4][5, 6, 7, 8][9,10,11,12]
💾 内存模型(连续存储):
+----+----+----+----+----+----+----+----+----+----+----+----+ ← 低地址
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
+----+----+----+----+----+----+----+----+----+----+----+----+ ← 高地址
按行顺序(row-major)存储:第一行 → 第二行 → 第三行
特点:
属性 | 说明 |
---|---|
内存连续 | 是 |
高效访问 | 是 |
易传给函数 | 是(需行数、列数信息) |
可初始化所有值 | 是 |
灵活性(行数/列数变动) | 否 不可动态变长 |
-
简单高效
-
编译器在编译时就知道每个元素地址的偏移
-
适用于尺寸固定的表格/矩阵处理
优势体现:性能最佳(连续内存 + 快速索引)
📌 示例:加速矩阵乘法(靠缓存命中率)
int A[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
int B[4][2] = {{1,2}, {1,2}, {1,2}, {1,2}};
int C[3][2] = {0};for (int i = 0; i < 3; ++i)for (int j = 0; j < 2; ++j)for (int k = 0; k < 4; ++k)C[i][j] += A[i][k] * B[k][j];
优势体现点:
-
编译器知道
A[i][k]
和B[k][j]
在内存中的确切偏移 -
数据连续,CPU 缓存命中率高
-
运行速度远优于动态结构(如 vector<vector<int>>)
方式 2:数组指针数组(数组中存放的是指针)
int* A[3]; // 数组中存放3个 int* 指针
A[0] = new int[4];
A[1] = new int[4];
A[2] = new int[4];
结构理解:
-
A
是一个大小为 3 的指针数组:A[0]
,A[1]
,A[2]
-
每个
A[i]
是一个指向 长度为 4 的数组 的指针
图示结构:
A: → [指针] [指针] [指针]↓ ↓ ↓[1,2,3,4] [5,6,7,8] [9,10,11,12]
💾 内存模型(不连续):
栈:
+-----+-----+-----+ ← A[0], A[1], A[2] 是在栈区的指针变量
| ptr | ptr | ptr |
+-----+-----+-----+↓ ↓ ↓
堆:[1][2][3][4] // A[0] 指向此处[5][6][7][8] // A[1][9][10][11][12] // A[2]
特点:
属性 | 说明 |
---|---|
内存连续? | 否 每一行在堆中独立分配 |
指针存储在哪里? | 是 指针数组本身在栈 |
灵活性? | 是 可以变行数或列数 |
缺点 | 否 不利于整体拷贝、效率较低、复杂性高 |
-
比方式①更灵活
-
每一行都可以单独分配、单独修改、单独释放
-
行长度可以不同(“不规则二维数组”)
优势体现:行长不一致的表格处理
📌 示例:一个表格,有 3 行,每行元素个数不同
int* A[3];
A[0] = new int[2]{1,2}; // 第一行只有2个元素
A[1] = new int[3]{3,4,5}; // 第二行3个
A[2] = new int[4]{6,7,8,9}; // 第三行4个for (int i = 0; i < 2; ++i) cout << A[0][i] << " ";
for (int i = 0; i < 3; ++i) cout << A[1][i] << " ";
for (int i = 0; i < 4; ++i) cout << A[2][i] << " ";delete[] A[0]; delete[] A[1]; delete[] A[2];
优势体现点:
-
不规则矩阵结构(每行不一样)
-
适合表示数据表格(例如 JSON 表、数据库行)
方式 3:双指针 + 二级堆分配
int** A;
A = new int*[3]; // 分配3个指针
A[0] = new int[4];
A[1] = new int[4];
A[2] = new int[4];
结构理解:
-
A
是一个指向指针的指针:int**
-
它指向一个动态分配的指针数组
-
每个指针再指向各自的行数组
图示结构:
堆1(A):
+-----+-----+-----+
| ptr | ptr | ptr | ← A[0], A[1], A[2]
+-----+-----+-----+↓ ↓ ↓
堆2:[1,2,3,4] [5,6,7,8] [9,10,11,12]
💾 内存模型(全堆 + 不连续):
栈:
+----------------+ ← A(int** 类型,指向堆中指针数组)
| 指向堆的指针地址 |
+----------------+堆(第一次 new):
+-----+-----+-----+
| ptr | ptr | ptr | ← 存放 A[0], A[1], A[2]
+-----+-----+-----+堆(3次 new):
[1,2,3,4] ← A[0]
[5,6,7,8] ← A[1]
[9,10,11,12] ← A[2]
特点:
属性 | 说明 |
---|---|
全部在堆中? | (适合大型数据) |
是否连续? | 不连续,多次分配 |
灵活性? | 最高,可以动态控制行/列数 |
管理复杂度? | 需手动 delete 两层,容易泄漏 |
-
最具通用性
-
完全动态二维数组
-
行数、列数在运行时可变,甚至可以动态增删行列
优势体现:完全运行时控制二维矩阵大小
📌 示例:用户输入决定行列数
int rows, cols;
cin >> rows >> cols;int** A = new int*[rows];
for (int i = 0; i < rows; ++i)A[i] = new int[cols]; // 每行动态开辟// 初始化矩阵
for (int i = 0; i < rows; ++i)for (int j = 0; j < cols; ++j)A[i][j] = i * cols + j;// 打印矩阵
for (int i = 0; i < rows; ++i) {for (int j = 0; j < cols; ++j)cout << A[i][j] << " ";cout << endl;
}// 释放内存
for (int i = 0; i < rows; ++i) delete[] A[i];
delete[] A;
优势体现点:
-
非常适合处理用户输入、数据文件、图结构(邻接表)
-
最接近“二维 vector”的行为
💡 补充建议
-
如果你只需要 固定结构,首选
int A[3][4]
-
如果你需要 运行时决定行列,用
int** A
或vector<vector<int>>
-
如果你想用更现代 C++ 写法:推荐使用
std::vector
来替代动态数组
如何用“第一性原理”去推导出 C++ 中二维数组的三种声明方式?
基础事实(不依赖高层语法):
-
内存是线性地址空间(一条一维线)
-
数组是连续的同类型内存块
-
指针是变量的地址
-
一个数组可以存放指针
-
new / malloc 可以在运行时分配任意大小的内存
-
C++ 编译器可以通过
arr[i][j]
翻译成偏移运算:*( *(arr + i) + j )
🔍 我的问题目标:我想建一个二维表格
输入需求:
-
我希望能访问
A[i][j]
-
这个结构要能容纳多行多列的数据
第一阶段:内存连续,列固定,行固定 → 推导出方式①
设计需求:
-
所有元素个数是 M × N
-
所有行列大小都是固定的
-
适合做 数值型矩阵处理、图像、表格
🤔 思考过程:
❓ 我能不能一次申请一个大数组,把所有数据按行拼在一起?
当然可以,我做个拍扁的线性数组:
int data[M * N];
然后我写一个访问函数:
int get(int i, int j) {return data[i * N + j];
}
👉 很方便,但语法不直观。我希望写 A[i][j]
而不是 data[i * N + j]
于是我想:
int A[M][N];
这不就是把 M*N
连续空间组织成二维格式了吗?
A[i][j]
被编译器翻译为:
*(A + i*N + j) = *( *(A + i) + j )
这就推导出了:
✅方式①:int A[3][4]
第二阶段:每行独立、列可能不同(不规则矩阵) → 推导出方式②
新设计需求:
-
每行的长度不同
-
不想为每一行都分配一样多空间(内存浪费)
-
比如:
Row 0: 3 elements
Row 1: 5 elements
Row 2: 2 elements
思考过程:
❓ 我能否为每一行单独分配一个一维数组?
当然可以,我可以写:
int* row0 = new int[3];
int* row1 = new int[5];
int* row2 = new int[2];
那我怎么管理这些行?
💡 我想到了:把这些指针放进一个数组中!
int* A[3]; // A[0], A[1], A[2] 分别指向三行
A[0] = row0;
A[1] = row1;
A[2] = row2;
现在我就可以访问 A[i][j]
了:
-
A[i]
是一行指针 -
A[i][j]
是访问这一行中的元素
这就推导出了:
✅方式②:int* A[3];
第三阶段:行数和列数都是运行时才知道的 → 推导出方式③
新设计需求:
-
用户在运行时告诉我有多少行和列
-
比如:
cin >> rows >> cols;
❓ 我该怎么在运行时分配二维表格呢?
我不能用 int A[rows][cols]
,因为在 C++ 中数组大小必须是编译时常量(除 VLA 编译器扩展)
🤔 思考过程:
第一步:
我要创建一个 行指针数组:
int** A = new int*[rows];
A
就是一个“二维数组的外壳”:A[i]
将是每一行的指针
第二步:
为每一行动态创建列空间:
for (int i = 0; i < rows; ++i)A[i] = new int[cols];
这样就构成了一个“指针的指针”:二维结构。
得到:
✅方式③:int** A; A = new int*[rows]; A[i] = new int[cols];
总结:需求驱动 + 构造能力 → 推导出三种方式
场景/需求 | 你能用的构造 | 推导出的结构 | 实际语法 |
---|---|---|---|
数据量固定、行列固定 | 数组 | 连续二维数组 | int A[M][N] |
行数固定、列数可变 | 数组 + 指针 | 指针数组 + 行数组 | int* A[M]; |
行列都可变 | 双层指针 + 堆分配 | 二级动态数组 | int** A; |
目标:二维数据结构↓
选择1:一维内存线? → 折叠二维到一维 → A[i][j] ≈ A[i*N + j] → ✅ int A[M][N]↓
选择2:能不能每行分开存? → 每行是 int*,用指针数组存行 → ✅ int* A[M]↓
选择3:连行数都未知? → 指针数组也要动态分配 → ✅ int** A; A = new int*[M]