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