别再用二级指针传二维数组了!编译器的报错你真的看懂了吗?

0 阅读7分钟

“我想写个函数处理一个二维数组,为什么我传 int **ptr 进去,编译器会疯狂报错?”

这是无数 C 语言初学者、甚至写了一两年代码的程序员都会踩到的天坑。在绝大多数人的直觉里:

  • 一维数组退化成一级指针:int arr[5]int *p

  • 二维数组自然就该退化成二级指针:int arr[3][4]int **p

听起来逻辑闭环、天衣无缝对不对?

然而,当你在经典的 VS2013 里敲下这段代码,按下编译的那一刻,现实会狠狠地给你一记耳 光 。今天,我们就用最直白的大白话和几张内存分布图,彻底把这个坑给填平。


1. 编译器的疯狂咆哮

我们先来看看这个让无数人百思不得其解的编译报错现场。

#include <stdio.h>
 
// 试图用二级指针来接收一个二维数组
void print_matrix(int **matrix, int rows, int cols) {
    int i = 0;
    int j = 0;
    for (i = 0; i < rows; i++) {
        for (j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }
}
 
int main() {
    int my_array[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    
    // 在这里传入二维数组,在 VS2013 下会直接编译报错
    print_matrix(my_array, 3, 4);
    
    return 0;
}

在 VS2013 中按下编译,你绝对会收到类似下面这条冷酷的报错:
error C2664: 'print_matrix' : cannot convert parameter 1 from 'int [3][4]' to 'int **'

编译器非常明确地告诉你:**int [3][4]** 根本不能转换成 **int ****

为什么?数组名不是可以退化成指针吗?二维数组名为什么就不能退化成二级指针?


2. 扒开内存:二维数组在底层根本不是你想的那样

要搞懂这个问题,我们必须把内存这层衣服扒开,看看二维数组到底是怎么躺在 RAM 里的。

在很多人的脑觉里,二维数组是个“网格表格”,像这样:

[1]  [2]  [3]  [4]
[5]  [6]  [7]  [8]
[9] [10] [11] [12]

但计算机的内存条是线性的、是一维一根筋的!它没有物理上的“行”和“列”。

所以,在 VS2013 底层,二维数组 int my_array[3][4] 在内存中是扁平化连续排列的:

地址:   0x100  0x104  0x108  0x10C  0x110  0x114  0x118  0x11C  ...
数据:   [ 1 ]  [ 2 ]  [ 3 ]  [ 4 ]  [ 5 ]  [ 6 ]  [ 7 ]  [ 8 ]  ...
索引:   my_array[0][0]...[0][3]     my_array[1][0]...[1][3]

看出端倪了吗?

  • my_array 代表的是数组首元素的地址。因为它是二维数组,它的“首元素”并不是数字 1,而是第一行这个一维数组 **{1, 2, 3, 4}**

  • 也就是说,my_array 指向的是一个大小为 4 的整型一维数组

  • 它的类型退化后,实际上是 **int (*)[4]**(指向包含 4 个 int 元素的数组的指针),即数组指针

二级指针的底层又是怎样的?

二级指针 int **ptr 是什么?它是一个存着一级指针地址的变量
如果你写 matrix[i][j],编译器在幕后会把它翻译成:
*(*(matrix + i) + j)

  • matrix + i 会先去找第 i指针变量的值

  • 编译器以为 matrix 指向的地方存着一堆 **int***(4字节/8字节的内存地址),它会先去取这个地址,然后再在这个地址的基础上去找偏移量 j

灾难发生了:
你把 my_array(它里面存的是一堆连续的 int 数值 1, 2, 3, 4)强行塞给了二级指针 matrix
当你访问 matrix[1][0] 时,编译器会把数据 23 当作一个内存地址去进行二次寻址!这会直接导致程序访问非法内存,瞬间崩溃( Windows 提示非法内存访问段错误)。

所以,编译器在编译阶段就直接把这道门死死焊死了,根本不让你过!


3. 破局之道:二维数组传参的 3 种正确姿势

既然不能用二级指针,那么在 VS2013 下,我们应该如何正确地把二维数组传给函数呢?

姿势一:老老实实写明列宽(最推荐,最符合直觉)

既然二维数组退化后是“指向一维数组的指针”,且这个一维数组的大小必须是确定的(在这里列宽是 4),那我们就在形参里写明列宽:

// 方式 A:直接写完整数组大小
void print_matrix_1(int matrix[3][4], int rows) { ... }
 
// 方式 B:省略行数,但列数不能省(退化成数组指针)
void print_matrix_2(int matrix[][4], int rows) { ... }
 
// 方式 C:严格使用数组指针写法
void print_matrix_3(int (*matrix)[4], int rows) { ... }

这三种写法在 VS2013 编译器眼里完全等价。列数 4 必须写死,因为编译器需要知道“往后跳一行,到底需要跨越多少个整型(这里是 4 个,也就是跨越 16 字节)”。

姿势二:降维打击,直接当成一级指针传递(最灵活)

如果你想写一个能处理任意行、任意列的通用函数,由于 C89/C99 混合标准在 VS2013 下对“变长数组(VLA)”的支持有限(VS2013 无法直接写 int matrix[rows][cols] 这种形参),我们可以用一级指针 + 手动计算偏移量的方式来降维处理:

#include <stdio.h>
 
// 将二维数组当做一维数组处理
void print_any_matrix(int *matrix, int rows, int cols) {
    int i = 0;
    int j = 0;
    for (i = 0; i < rows; i++) {
        for (j = 0; j < cols; j++) {
            // 手动计算扁平化后的内存偏移:行索引 * 列数 + 列索引
            printf("%d ", *(matrix + (i * cols + j)));
        }
        printf("\n");
    }
}
 
int main() {
    int my_array[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    
    // 强转成一级指针 int* 传入
    print_any_matrix((int*)my_array, 3, 4);
    
    return 0;
}

这种方式没有任何硬编码,几乎是所有工业级 C 语言库(比如 图像处理 、游戏矩阵运算)最通用的底层做法。

姿势三:真正的“二级指针”应该怎么传?

如果你非要用 int **matrix 作为函数参数,那么你的实参绝对不能是 int my_array[3][4] 这种静态连续二维数组,而是必须用 malloc 动态开辟出一块“指针数组”:

#include <stdio.h>
#include <stdlib.h>
 
void print_real_double_pointer(int **matrix, int rows, int cols) {
    int i, j;
    for (i = 0; i < rows; i++) {
        for (j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]); // 此时 matrix[i] 存储的是个真地址,二次寻址安全!
        }
        printf("\n");
    }
}
 
int main() {
    int rows = 3;
    int cols = 4;
    int i, j;
    
    // 1. 先申请一个指针数组(数组里存的是指针 int*)
    int **matrix = (int**)malloc(sizeof(int*) * rows);
    
    // 2. 每一行单独 malloc,将其地址存入指针数组中
    for (i = 0; i < rows; i++) {
        matrix[i] = (int*)malloc(sizeof(int) * cols);
        // 初始化一些测试数据
        for (j = 0; j < cols; j++) {
            matrix[i][j] = i * cols + j + 1;
        }
    }
    
    // 这时传入二级指针是 100% 安全且合法的!
    print_real_double_pointer(matrix, rows, cols);
    
    // 3. 别忘了依次释放
    for (i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);
    
    return 0;
}

总结

别再把“二维”和“双重”混为一谈:

  • **int arr[3][4]**:在内存中只是一块整整齐齐的连续整型空间,没有一个地址变量存在,退化类型是 int (*)[4](一星数组指针)。

  • **int **ptr**:在内存中代表二次间接寻址,它要求第一层存储的数据必须是实打实的指针变量的值(地址)

在 VS2013 下开发时,牢记这两者的底层内存区别。需要写通用函数时,用一维指针降维法(姿势二),保证你写出来的 C 语言代码又快、又安全、还绝不报错!