C language 将 2D array 传给 function 时为何显示为 pointer
结论
这是 C 语言规定的正常行为,不是内存地址错误。
main() 里的 grid 是一个真正的二维数组,类型是:
int [10][10]
数组作为函数参数时,最外层维度会被调整为指针。因此:
void draw_a_square(int row, int column, int grid[10][10])
在参数类型上等同于:
void draw_a_square(int row, int column, int (*grid)[10])
调试器在 draw_a_square() 中把 grid 显示为指针是正常的。这个指针指向一个包含 10 个 int 的数组,也就是一整行。它与普通的 int * 不同,也不能当作 int ** 使用。
为什么仍然可以使用 grid[r][c]
函数参数写成:
int grid[10][10]
时,只有第一维会退化。编译器仍然知道每行有 10 个 int,所以能够计算每一行的位置。
表达式:
grid[r][c]
等价于:
*(*(grid + r) + c)
grid + r 每次会跨过一整行,也就是 10 个 int,因此第二维的大小必须保留。
下面三种声明表示同一种参数类型:
void draw_a_square(int row, int column, int grid[10][10]);
void draw_a_square(int row, int column, int grid[][10]);
void draw_a_square(int row, int column, int (*grid)[10]);
参数声明里的第一个 10 不属于函数参数类型。即使写成:
void draw_a_square(int row, int column, int grid[100][10]);
参数类型仍然是:
int (*grid)[10]
为什么 main() 和函数中的调试显示不同
在 main() 中,编译器掌握 grid 的完整类型和存储空间:
int grid[10][10];
调试器知道这个数组有 10 行,每行 10 个元素,因此可以直接展开全部内容。
进入函数后,参数中只有指向第一行的指针。函数不会自动得到数组的行数,需要另行传入 row,调试器通常也会按指针显示它。
两处访问的仍是同一块数组内存。在函数中执行:
grid[5][5] = 1;
返回 main() 后也能看到这个修改。
可以通过 sizeof 查看两者的区别:
int grid[10][10];
printf("%zu\n", sizeof grid); // 整个二维数组的大小
在函数内部:
void f(int grid[10][10])
{
printf("%zu\n", sizeof grid); // 指针本身的大小
}
部分编译器还会针对函数参数上的 sizeof grid 发出警告,因为这里测量的是指针,而不是原数组。
原代码中的实际错误
数组在函数中显示为指针不会导致 FloodFill 失败。原代码的问题出在这一行:
grid[r][c]==1;
== 是比较运算符,只会判断两个值是否相等,不会修改数组。这里应该使用赋值运算符:
grid[r][c] = 1;
递归前还要检查当前单元格是否已经填充。否则,不同方向会反复访问同一个位置。例如 A 访问 B 后,B 又立即访问 A,最终可能耗尽调用栈。
边界检查应覆盖整个有效范围:
if (row < 0 || row >= rows ||
column < 0 || column >= columns) {
return;
}
只检查 row == rows 等情况并不稳妥,因为异常参数也可能小于 0 或直接超过边界。
dfs() 也不需要返回整数。原来的 int dfs(...) 有部分执行路径会到达函数末尾,却没有返回值。改成 void 更符合它的用途。
一个可工作的固定尺寸版本
下面的代码使用固定的 10 列布局。grid[][COLUMNS] 表示参数是一个指向数组行的指针:
#include <stdio.h>
#define ROWS 10
#define COLUMNS 10
static void draw_a_square(
int rows,
int columns,
const int grid[][COLUMNS]
) {
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < columns; ++c) {
if (grid[r][c] == 5) {
printf("\\ ");
} else if (grid[r][c] == 0) {
printf(". ");
} else if (grid[r][c] == 1) {
printf("* ");
}
}
putchar('\n');
}
}
static void dfs(
int row,
int column,
int rows,
int columns,
int grid[][COLUMNS]
) {
if (row < 0 || row >= rows ||
column < 0 || column >= columns) {
return;
}
/*
* 只填充值为 0 的单元格。
* 1 表示已经访问,5 表示墙或边界。
*/
if (grid[row][column] != 0) {
return;
}
/* 必须在递归访问相邻位置之前标记。 */
grid[row][column] = 1;
dfs(row - 1, column, rows, columns, grid);
dfs(row + 1, column, rows, columns, grid);
dfs(row, column - 1, rows, columns, grid);
dfs(row, column + 1, rows, columns, grid);
}
int main(void)
{
int grid[ROWS][COLUMNS] = {
{5,5,5,5,5,5,5,5,5,5},
{5,0,0,0,0,0,0,0,0,5},
{5,0,0,0,0,0,0,0,0,5},
{5,0,0,0,0,0,0,0,0,5},
{5,0,0,0,0,0,0,0,0,5},
{5,0,0,0,0,0,5,0,0,5},
{5,0,0,0,0,0,0,0,0,5},
{5,0,0,0,0,0,0,0,0,5},
{5,0,0,0,0,0,0,0,0,5},
{5,5,5,5,5,5,5,5,5,5}
};
dfs(5, 5, ROWS, COLUMNS, grid);
draw_a_square(ROWS, COLUMNS, grid);
return 0;
}
填充从下标 (5, 5) 开始。C 数组的下标从 0 开始,所以这里指的是第 6 行、第 6 列。该位置必须为 0,否则 dfs() 会立即返回。
行列数可变时的写法
如果编译器支持变长数组(VLA),可以把行数和列数一并传给函数:
void draw_a_square(
int rows,
int columns,
const int grid[rows][columns]
) {
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < columns; ++c) {
printf("%d ", grid[r][c]);
}
putchar('\n');
}
}
rows 和 columns 必须写在 grid 参数之前。grid 在函数内部仍然是指针,只是它所指向的每一行宽度由 columns 决定。
并非所有 C 编译器都支持 VLA。为了兼容不支持 VLA 的编译器,可以继续使用固定列数,也可以把数组按一维连续内存传递:
void set_cell(
int *grid,
int columns,
int row,
int column,
int value
) {
grid[row * columns + column] = value;
}
调用方式如下:
set_cell(&grid[0][0], COLUMNS, 5, 5, 1);
注意事项
int grid[][10]是指向一行 10 个int的指针,不能替换为int **grid。两者的内存布局和指针运算方式不同。- 函数签名中的列数必须与实际数组一致,否则
grid[r][c]会按错误的行宽计算地址。 draw_a_square()只读取数组,因此参数可以使用const。dfs()需要修改数组,不能加const。- FloodFill 应先检查边界和单元格状态,然后再访问并标记单元格。
- 标记操作必须放在递归调用之前,避免相邻单元格互相递归。
- 网格很大时,递归 FloodFill 可能耗尽调用栈,可以改用显式栈或队列。
备注:内容仅供参考。
