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  ·    ·    ●      · = 下三角(通过交换上三角自动完成)

代码实现——内层 ji + 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;
}

逐段解析:

  1. 初始化 int yh[N][N] = {0}:全置 0,未用到的位置自动是 0,方便
  2. 生成核心:外层 i 遍历行,yh[i][0] = 1yh[i][i] = 1 设首尾为 1,中间用 yh[i-1][j-1] + yh[i-1][j] 计算
  3. 内层 j 从 1 到 i-1:因为 j=0j=i 已经手动设 1 了,不需要重复计算
  4. 打印前导空格:每行前面少打印 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

思路分析:维护四个边界 topbottomleftright,每走完一条边就收缩对应边界。

第一步:从左到右走 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],内层 ji+1 开始,只遍历上三角
函数参数 列数必须指定——编译器需要列数算地址;可以省略行
地址计算 万能公式:首地址 + (i × 总列数 + j) × sizeof(类型)
杨辉三角 边界处理 + 递推公式 a[i][j] = a[i-1][j-1] + a[i-1][j]
螺旋矩阵 四边界维护法,收缩前必须判断防重复

这次的二维数组上下两篇加在一起,从概念到面试题全覆盖了。这些内容反复看、手敲代码、自己改参数跑一跑,比看十遍理论都管用。


Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐