欢迎光临
我们一直在努力

11. 表格与矩阵:多维数组

上一篇文章我们学会了一维数组——它像一排连续的抽屉,整齐地排成一条直线。但现实中的数据往往比“一排”更复杂:课程表有行有列,棋盘有纵横坐标,图像是由像素组成的矩形网格。这时候,一维数组就有些力不从心了。

好在 C 语言允许我们把这些“行”再堆叠起来,形成多维数组。二维数组用得最多,三维偶尔露脸,四维以上更多是理论存在。今天就主要攻克二维,再浅探多维,让你以后碰到矩阵运算、图像处理、游戏棋盘之类的问题能心中有数。


一、二维数组的声明与理解

1. 声明方式

类型 数组名[行数][列数];

比如,存储一个班级 3 门课、5 个学生的成绩表:

int scores[3][5]; // 3 行 5 列

它对应这样一张表:

学生0 学生1 学生2 学生3 学生4
课程0: ? ? ? ? ?
课程1: ? ? ? ? ?
课程2: ? ? ? ? ?

逻辑上我们把它理解成“行”和“列”,但在物理内存中,它依然是一维连续排列的。C 语言用的是行优先存储:先完整存第 0 行,然后紧接着存第 1 行,再存第 2 行……像把一张表格一行行拆开,拼成一条长链。

内存顺序: [0][0] [0][1] [0][2] [0][3] [0][4] [1][0] [1][1] … [2][4]

这个知识点非常重要。因为行优先,访问 scores[i][j] 时,编译器实际上计算的是:

地址偏移 = i * 列数 + j

知道这个原理,你以后写遍历循环时,就会把行循环放外层、列循环放内层——尽量让内存访问连续,能大幅提升性能(缓存友好)。

2. 初始化二维数组

完全初始化(嵌套花括号):

int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};

结果为:

第0行: 1, 2, 3
第1行: 4, 5, 6

部分初始化(未指定的自动为 0):

int matrix[2][3] = {
{1, 2} // 第0行:1, 2, 0;第1行:全0
};

省略内层花括号(不推荐,可读性差):

int matrix[2][3] = {1, 2, 3, 4, 5, 6}; // 按行优先依次填充

省略第一维大小(编译器根据初始化列表推断):

int matrix[][3] = {
{1, 2, 3},
{4, 5, 6}
}; // 自动推断为 2 行 3 列

注意:只有最左边的维度可以省略,其他维度必须写清楚,否则编译器算不出地址偏移公式。


二、访问二维数组元素

通过 数组名[行下标][列下标] 访问,下标都从 0 开始。

int matrix[2][3] = {
{10, 20, 30},
{40, 50, 60}
};

printf("%d\\n", matrix[0][1]); // 20
matrix[1][2] = 99; // 修改第1行第2列

遍历二维数组通常用嵌套的 for 循环:

#include <stdio.h>

int main(void) {
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};

for (int i = 0; i < 2; i++) { // 外层遍历行
for (int j = 0; j < 3; j++) { // 内层遍历列
printf("%d ", matrix[i][j]);
}
printf("\\n"); // 每行结束后换行
}
return 0;
}

输出:

1 2 3
4 5 6

遍历顺序很重要:外层是行、内层是列,才能让内存访问是连续的(先 [0][0]、[0][1]、[0][2],然后 [1][0]…)。如果反过来外层列、内层行,CPU 缓存会频繁落空,在大数据量下性能差距可达数倍。记住一个原则:让内层循环对应最右边的下标。


三、二维数组作为函数参数

把二维数组传给函数时,必须告诉编译器除了最左边维度以外的所有维度大小。

void print_matrix(int rows, int cols, int mat[][cols]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", mat[i][j]);
}
printf("\\n");
}
}

注意参数 int mat[][cols]:

  • 最左边的维度(行)可以省略,写成空的 []。
  • 列的大小必须给定,因为编译器需要它来计算 i * cols + j 的偏移。

调用时:

int matrix[2][3] = {{1,2,3}, {4,5,6}};
print_matrix(2, 3, matrix);


四、经典案例:矩阵运算

矩阵加法是最直观的二维数组应用——两个相同大小的矩阵,对应元素相加。

#include <stdio.h>

#define ROWS 2
#define COLS 3

void add_matrices(int a[ROWS][COLS], int b[ROWS][COLS], int result[ROWS][COLS]) {
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
result[i][j] = a[i][j] + b[i][j];
}
}
}

void print_matrix(int rows, int cols, int mat[][cols]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%3d ", mat[i][j]);
}
printf("\\n");
}
}

int main(void) {
int A[ROWS][COLS] = {
{1, 2, 3},
{4, 5, 6}
};
int B[ROWS][COLS] = {
{6, 5, 4},
{3, 2, 1}
};
int C[ROWS][COLS] = {0};

add_matrices(A, B, C);

printf("矩阵 A:\\n");
print_matrix(ROWS, COLS, A);

printf("\\n矩阵 B:\\n");
print_matrix(ROWS, COLS, B);

printf("\\nA + B = \\n");
print_matrix(ROWS, COLS, C);

return 0;
}

输出:

矩阵 A:
1 2 3
4 5 6

矩阵 B:
6 5 4
3 2 1

A + B =
7 7 7
7 7 7

%3d 让每个数字占 3 个字符宽度,排版整齐。二维数组在数值计算、图像处理中无处不在——图像本身就是一个像素值的二维数组,核心操作几乎都是对行列的遍历。


五、多维数组:三维及更高

二维之上,可以扩展出三维、四维乃至更高维度的数组。三维数组可以理解为“多页表格”——每一“页”是一个二维矩阵。

// 2页,每页3行4列
int cube[2][3][4] = {
{ // 第0页
{1, 2, 3, 4},
{5, 6, 7, 8},
{9,10,11,12}
},
{ // 第1页
{13,14,15,16},
{17,18,19,20},
{21,22,23,24}
}
};

遍历三重循环:

for (int p = 0; p < 2; p++) {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%2d ", cube[p][i][j]);
}
printf("\\n");
}
printf("—\\n");
}

更高维度同理,层层嵌套。但维度越高,对内存的消耗呈指数增长,实际开发中很少超过三维。保持对高维数组“层层嵌套”的感知即可。


六、字符串数组与命令行参数预告

二维 char 数组的一个典型用途是存储多个字符串:

char names[3][20] = {"Alice", "Bob", "Charlie"};

这里 names[0] 是一个长度为 20 的字符数组,存放 "Alice"(带 \\0 结尾)。后面我们讲到字符串、指针数组时,会深入这个主题,包括 argv 命令行参数的本质。


七、常见错误与陷阱

  • 下标顺序搞反

    int mat[3][5];
    mat[0][1] = 10; // 正确
    mat[1][0] = 20; // 正确
    // 不要写成 mat[1,0],那是逗号运算符,结果是 0

  • 给二维数组赋值忘记列下标

    int mat[2][3];
    mat[0] = {1,2,3}; // 错误!数组名不能赋值

    只能逐个元素赋值,或者在声明时整体初始化。

  • 传递二维数组时漏写列数

    void func(int mat[][]) // 错误!必须指定列数
    void func(int mat[3][]) // 也错!列数必须给出
    void func(int mat[][5]) // 正确

  • 把 sizeof 用在函数参数里的二维数组

    void func(int mat[][5]) {
    int rows = sizeof(mat) / sizeof(mat[0]); // 错误!mat 是指针
    }

    和一维数组一样,函数参数中的数组名退化为指针,sizeof 失效。

  • 遍历时行列循环写反导致性能骤降
    内层循环应遍历最右边的下标,保证内存访问连续。


  • 八、小结

    今天你把一维的“队伍”扩展成了二维的“矩阵”,还学会了更高维度的基本概念。二维数组是图像处理、游戏开发、科学计算中的核心数据结构,矩阵的加法、乘法、转置都建立在它之上。你学到的“行优先存储”和遍历原则,以后也会直接影响程序的运行速度。

    现在我们已经能管理大量的数据了,但代码依然堆在 main 函数里。当程序逐渐庞大,单靠 main 是远远不够的——你需要把功能拆分成可复用的模块。下一篇文章,我们就进入函数的世界:如何把代码封装成函数,如何传递参数,如何获得返回值。它会让你的代码从“长篇大论”进化成“团队协作”。


    课后小练习

  • 声明一个 3×3 的矩阵,初始化为 1 到 9(按行递增)。打印出它的转置矩阵(行列互换)。
  • 实现两个 3×3 矩阵的乘法并打印结果。矩阵乘法的规则:C[i][j] = sum(A[i][k] * B[k][j]),k 从 0 到 2。
  • 用二维数组打印一个 5×5 的单位矩阵(对角线为 1,其余为 0)。
  • (小挑战)输入一个 3×3 的整数方阵,判断它是否为幻方(即每行、每列、两条对角线的和都相等)。输出判断结果。
  • 我们下期见!

    赞(0)
    未经允许不得转载:171主机测评 » 11. 表格与矩阵:多维数组
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址