别再用二级指针传二维数组了!编译器的报错你真的看懂了吗?
“我想写个函数处理一个二维数组为什么我传int **ptr进去编译器会疯狂报错”这是无数 C 语言初学者、甚至写了一两年代码的程序员都会踩到的天坑。在绝大多数人的直觉里一维数组退化成一级指针int arr[5]$\rightarrow$int *p二维数组自然就该退化成二级指针int arr[3][4]$\rightarrow$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]时编译器会把数据2或3当作一个内存地址去进行二次寻址这会直接导致程序访问非法内存瞬间崩溃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 语言代码又快、又安全、还绝不报错