“我想写个函数处理一个二维数组,为什么我传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 语言代码又快、又安全、还绝不报错!