PAYATHON 2026

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 可能耗尽调用栈,可以改用显式栈或队列。

备注:内容仅供参考。