C语言二维数组详解(进阶实战):VLA、矩阵转置与经典面试题
C语言二维数组详解(进阶实战):VLA、矩阵转置与经典面试题
上篇我们把二维数组的基础打扎实了。这篇直接上强度——C99 变长数组、矩阵转置实战,还有 4 道面试常考题,每一道都带逐行解析和思路拆解。坐稳了!
一、回顾上篇
快速过一下我们已经掌握的内容:
- ✅ 二维数组的定义和行列概念
- ✅ 四种初始化方式(线性、分组、不完全、省略行号)
- ✅ 下标访问和双重循环遍历
- ✅ 从键盘输入数据到二维数组
- ✅ 内存中的行优先连续存储和地址公式
没搞懂的先回去看上篇,我们下篇就默认你已经掌握了这些基础。
二、C99 变长数组(VLA)——让数组大小「活」起来
2.1 C99 之前的问题
写死数组大小很尴尬:
int arr[100]; // 用户只用 10 个,浪费 90 个位置
int arr[10]; // 用户要 100 个,数组根本不够
C99 标准给出的解决方案——变长数组(Variable-Length Array,VLA):
int n;
scanf("%d", &n); // 运行时由用户输入决定
int arr[n]; // 创建长度为 n 的数组
2.2 名称陷阱:「变长」到底什么意思?
| ❌ 你以为的意思 | ✅ 实际的意思 |
|---|---|
| 数组长度可以变长变短 | 长度一旦创建就固定了 |
| 运行时随意调整大小 | 只是用**变量(variable)**指定初始长度 |
📌 「变长」指「用变量指定长度」,「变」的是指定方式,不是数组本身能变大变小。 这个坑,10 个人里 8 个踩过。
2.3 VLA 的限制
第一:VLA 不能初始化。
int n = 5;
int arr[n] = {0}; // ❌ 编译报错!
初始化发生在编译阶段,但 n 的值运行时才知道——编译器压根不知道要初始化多少个元素。
第二:VS2022 不支持 VLA。 MSVC 编译器选择不实现。但以下环境可用:
- GCC(Dev-C++、Code::Blocks、Linux)
- Clang
- 大部分在线 OJ 平台
- Mac 上的 Xcode
2.4 完整示例(GCC 环境)
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n); // 用户输入数组大小
int arr[n]; // 创建变长数组
for (int i = 0; i < n; i++) // 输入
scanf("%d", &arr[i]);
for (int i = 0; i < n; i++) // 输出
printf("%d ", arr[i]);
return 0;
}
⚡ 实践建议:实际工程中更推荐
malloc动态分配(后面的课程会学),VLA 在栈上分配,n 太大可能导致栈溢出。
三、实战一:矩阵转置——把行变成列
3.1 什么是「转置」?
转置就是把矩阵的行和列互换。
转置前: 转置后:
列0 列1 列2 列0 列1 列2
行0 1 2 3 行0 1 4 7
行1 4 5 6 → 行1 2 5 8
行2 7 8 9 行2 3 6 9
观察规律:
- 原来
[0][1]的 2,转置后去了[1][0] - 原来
[1][0]的 4,转置后去了[0][1] - 对角线上(
[0][0]、[1][1]、[2][2])的 1、5、9 位置不变
3.2 算法思路
核心操作:交换 matrix[i][j] 和 matrix[j][i]。
但危险:如果你遍历整个矩阵:
第一次:matrix[0][1] ←→ matrix[1][0] 交换 2 和 4 ✅
...
后来: matrix[1][0] ←→ matrix[0][1] 又把 2 和 4 换回去了 ❌
解决方案:只交换「上三角」区域。
列0 列1 列2
行0 ● △ △ ● = 对角线(不动)
行1 · ● △ △ = 上三角(要交换的)
行2 · · ● · = 下三角(通过交换上三角自动完成)
代码实现——内层 j 从 i + 1 开始:
for (int i = 0; i < 3; i++)
{
for (int j = i + 1; j < 3; j++) // j 从 i+1 开始 = 上三角
{
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
3.3 逐步追踪
i=0 时:
j=1:交换 matrix[0][1]=2 和 matrix[1][0]=4
j=2:交换 matrix[0][2]=3 和 matrix[2][0]=7
矩阵变为:
1 4 7
2 5 6
3 8 9
i=1 时:
j=2:交换 matrix[1][2]=6 和 matrix[2][1]=8
最终:
1 4 7
2 5 8
3 6 9 ✅
i=2 时:j 从 3 开始,j < 3 不成立,跳过(对角线以下不处理)
3.4 完整代码
#include <stdio.h>
int main()
{
int matrix[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// 打印原始矩阵
printf("原始矩阵:\n");
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
printf("%d ", matrix[i][j]);
printf("\n");
}
// 原地转置——只交换上三角
for (int i = 0; i < 3; i++)
for (int j = i + 1; j < 3; j++)
{
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
// 打印转置后
printf("\n转置后的矩阵:\n");
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
printf("%d ", matrix[i][j]);
printf("\n");
}
return 0;
}
🔴 碎碎念
| 错误 | 代码 | 后果 |
|---|---|---|
| 遍历整个矩阵 | for(j=0; j<3; j++) 而非 j=i+1 |
交换两次 = 白干 |
| 缺少 temp | 直接 m[i][j]=m[j][i] |
原值丢失 |
| 非方阵原地转置 | 对 3×4 做原地转置 | 行列数不同不可能原地完成 |
四、经典面试题精讲
下面 4 道题从基础到进阶,都是面试和考试里二维数组的高频考点。
面试题 ① 二维数组作为函数参数——为什么必须指定列数?
题目:以下两个函数声明,哪个是对的?为什么?
void print1(int arr[2][3], int row, int col); // 声明A
void print2(int arr[][], int row, int col); // 声明B
void print3(int arr[][3], int row, int col); // 声明C
答案:A 和 C 可以,B 不行。
详细解析:
二维数组作为参数传递时,编译器必须知道每一行有多少列,才能正确计算地址。
回顾上篇的地址公式:
arr[i][j] 的地址 = 首地址 + (i × 列数 + j) × sizeof(类型)
如果你写 int arr[][],编译器不知道「列数」是多少,没法算 (i × 列数 + j) 这个表达式。所以:
void print(int arr[][3], int row) // ✅ 列数 3 明确了
{
for (int i = 0; i < row; i++)
for (int j = 0; j < 3; j++) // 列数必须已知
printf("%d ", arr[i][j]);
}
void print(int arr[][], int row, int col) // ❌ 编译报错
{
// 编译器:列数不知道,没法算 arr[i][j]!
}
可以省略行,不能省略列——这条规则在函数参数中同样适用。
void print(int arr[][3], int row); // ✅ 行可以省略
void print(int arr[3][3], int row); // ✅ 写全了也行
void print(int arr[3][], int row); // ❌ 列省略了,不行
🎯 面试要点:说出「编译器需要列数来计算元素地址」就算答到点子上了。
面试题 ② 地址计算——给定首地址,求 arr[i][j] 的地址
题目:已知 int a[4][5] 的首地址为 0x1000,求 a[2][3] 的地址(假设 int 占 4 字节)。
解题思路:
直接套用公式:
a[i][j] 的地址 = 首地址 + (i × 总列数 + j) × sizeof(int)
代入:
a[2][3] 的地址 = 0x1000 + (2 × 5 + 3) × 4
= 0x1000 + 13 × 4
= 0x1000 + 52
= 0x1000 + 0x34
= 0x1034
答案:0x1034
变式题(面试官可能会追问):
已知
int a[3][4],&a[1][2]的地址是0x2000,求&a[2][1]的地址。
a[1][2] 地址 = 首地址 + (1×4+2)×4 = 首地址 + 24 = 0x2000
所以首地址 = 0x2000 - 24 = 0x2000 - 0x18 = 0x1FE8
a[2][1] 地址 = 0x1FE8 + (2×4+1)×4 = 0x1FE8 + 36 = 0x1FE8 + 0x24 = 0x200C
🎯 面试要点:记牢公式
首地址 + (i×列数+j)×sizeof(类型),所有地址计算题都从它推导。
面试题 ③ 杨辉三角——二维数组的经典应用
题目:用二维数组生成并打印 10 行杨辉三角。
杨辉三角规律:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
...
每一行的第一个和最后一个都是 1,其余位置 arr[i][j] = arr[i-1][j-1] + arr[i-1][j](等于「左上 + 右上」)。
完整代码:
#include <stdio.h>
#define N 10 // 生成 10 行
int main()
{
int yh[N][N] = {0}; // 先全部清零
// 生成杨辉三角
for (int i = 0; i < N; i++)
{
yh[i][0] = 1; // 每行第一个 = 1
yh[i][i] = 1; // 每行最后一个 = 1
for (int j = 1; j < i; j++) // j 从 1 到 i-1
yh[i][j] = yh[i-1][j-1] + yh[i-1][j];
}
// 打印
for (int i = 0; i < N; i++)
{
// 打印前导空格,让图形居中
for (int k = 0; k < N - i; k++)
printf(" ");
for (int j = 0; j <= i; j++)
printf("%6d", yh[i][j]);
printf("\n");
}
return 0;
}
逐段解析:
- 初始化
int yh[N][N] = {0}:全置 0,未用到的位置自动是 0,方便 - 生成核心:外层
i遍历行,yh[i][0] = 1和yh[i][i] = 1设首尾为 1,中间用yh[i-1][j-1] + yh[i-1][j]计算 - 内层
j从 1 到i-1:因为j=0和j=i已经手动设 1 了,不需要重复计算 - 打印前导空格:每行前面少打印 3 个空格,三角形居中
🎯 面试要点:杨辉三角考察了二维数组的边界处理(首尾为 1)和递推关系(左上+右上),是经典的「用已算出的值推导新值」思维训练。
面试题 ④ 螺旋矩阵——顺时针遍历输出
题目:给定一个 m × n 矩阵,按顺时针螺旋顺序输出所有元素。
输入矩阵: 螺旋输出:
1 2 3 4 1 2 3 4 8 12 11 10 9 5 6 7
5 6 7 8 →
9 10 11 12
思路分析:维护四个边界 top、bottom、left、right,每走完一条边就收缩对应边界。
第一步:从左到右走 top 行 → top++
第二步:从上到下走 right 列 → right--
第三步:从右到左走 bottom 行(如果 top ≤ bottom)→ bottom--
第四步:从下到上走 left 列(如果 left ≤ right)→ left++
重复直到边界交叉
完整代码:
#include <stdio.h>
void spiralPrint(int matrix[][4], int row, int col)
{
int top = 0, bottom = row - 1;
int left = 0, right = col - 1;
printf("螺旋输出:");
while (top <= bottom && left <= right)
{
// 1. 从左到右:遍历 top 行
for (int j = left; j <= right; j++)
printf("%d ", matrix[top][j]);
top++; // 上边界下移
// 2. 从上到下:遍历 right 列
for (int i = top; i <= bottom; i++)
printf("%d ", matrix[i][right]);
right--; // 右边界左移
// 3. 从右到左:遍历 bottom 行(先判边界)
if (top <= bottom)
{
for (int j = right; j >= left; j--)
printf("%d ", matrix[bottom][j]);
bottom--; // 下边界上移
}
// 4. 从下到上:遍历 left 列(先判边界)
if (left <= right)
{
for (int i = bottom; i >= top; i--)
printf("%d ", matrix[i][left]);
left++; // 左边界右移
}
}
printf("\n");
}
int main()
{
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
spiralPrint(matrix, 3, 4);
return 0;
}
执行过程追踪:
初始边界:top=0, bottom=2, left=0, right=3
第1轮:
① 左→右 [0][0..3] → 1 2 3 4 top=1
② 上→下 [1..2][3] → 8 12 right=2
③ 右→左 [2][2..0] → 11 10 9 bottom=1
④ 下→上 [1][0] → 5 left=1
第2轮:
① 左→右 [1][1..2] → 6 7 top=2
② top(2) > bottom(1) → 跳过
结果:1 2 3 4 8 12 11 10 9 5 6 7 ✅
🎯 面试要点:螺旋矩阵考察了边界控制和状态转换。关键技巧——四个指针维护边界,走完一边立即收缩。步3和步4必须先判断边界条件(防止只剩一行或一列时重复输出)。
🔴 面试题避坑总结
| 题号 | 考点 | 易错点 |
|---|---|---|
| ① 函数参数 | 列数必须指定 | 只说「语法规定」不行,要说出「地址计算需要列数」 |
| ② 地址计算 | 公式 首地址+(i×col+j)×sizeof |
忘了乘 sizeof,或把总列数当成总行数 |
| ③ 杨辉三角 | 首尾为 1、递推关系 | 内层 j 的范围弄错(应为 1 到 i-1) |
| ④ 螺旋矩阵 | 四边界收缩 | 步3 步4 不判边界条件,剩一行/一列时重复输出 |
五、全篇总结
| 模块 | 核心知识点 |
|---|---|
| VLA | C99 特性,用变量指定长度,创建后不可变,不能初始化,VS2022 不支持 |
| 矩阵转置 | 交换 m[i][j] ↔ m[j][i],内层 j 从 i+1 开始,只遍历上三角 |
| 函数参数 | 列数必须指定——编译器需要列数算地址;可以省略行 |
| 地址计算 | 万能公式:首地址 + (i × 总列数 + j) × sizeof(类型) |
| 杨辉三角 | 边界处理 + 递推公式 a[i][j] = a[i-1][j-1] + a[i-1][j] |
| 螺旋矩阵 | 四边界维护法,收缩前必须判断防重复 |
这次的二维数组上下两篇加在一起,从概念到面试题全覆盖了。这些内容反复看、手敲代码、自己改参数跑一跑,比看十遍理论都管用。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)