如果你学C语言学到数组传参这一段,感觉有点绕,甚至被一维、二维搞到怀疑人生,那这篇文章就是写给你的。数组传参是C语言里一个看似简单、实际上暗藏不少坑的点,尤其是在单片机、嵌入式、算法题这些场景里,几乎天天都要跟它打交道。这篇文章我会把一维数组传参和二维数组传参从头到尾拆开讲清楚,包括底层原理、三种常见写法、参数退化问题、为什么二维数组传参必须带列数、动态二维数组怎么做,以及我这么多年实际调试中踩过的一些坑。不管你是刚学完指针的初学者,还是正在准备计算机二级、考研复试,或者是在vscode里折腾C语言环境的学生党,这篇文章都值得你花十分钟看完,能帮你省下不少排查bug的时间。
数组传参这件事,很多人一开始想的是“把数组传进函数”,但C语言压根没有“传数组”这个操作。你把数组写到函数参数里的那一刻,编译器默默把它处理成了指针。这个机制叫数组参数退化,也是后面一系列困惑的总根源。理解了它,一维二维的传参问题就解决了一大半。
1. 数组传参前必须先想明白的几件事
1.1 数组名到底是个什么东西
先说最常见的操作,比如你写了int a[10];,这里的a到底是什么?很多人会背“数组名是首地址”,但这个说法不够准确,容易误导。更准确的说法是:a是一个数组类型的表达式,它代表一块连续内存空间,里面装了10个int。绝大多数情况下——比如把它传给函数、拿去赋值给指针、参与运算——它才会“退化”成指向第一个元素的指针,也就是int *。
这个退化规则在C语言标准里叫array-to-pointer decay。什么时候不退化成指针?只有三个地方除外:一是sizeof(a),二是取地址&a,三是用字符串字面量初始化字符数组。换句话说,sizeof(a)是10个int的实际字节数,但你把a传进函数之后,如果在函数里写sizeof(arr),得到的是指针的大小,通常是8字节(64位系统)或4字节(32位系统)。
这个坑我见得太多了:有人写了一个求数组长度的函数:
int get_len(int arr[]) { return sizeof(arr) / sizeof(arr[0]); }结果不管传多大的数组进去,返回值永远要么是2要么是1,因为sizeof(arr)变成了指针的尺寸。这就是数组参数退化的经典后果,后面展开细说。
1.2 传数组的本质是传地址,不是传值
C语言只有传值,没有真正意义的传引用。但你传给函数的是一个指针值,也就是数组首元素的地址。所以函数内部可以通过这个地址去修改原始数组的内容,这就会产生“看起来像传引用”的效果。
举个例子:
void change_first(int arr[]) { arr[0] = 100; } int main() { int a[] = {1, 2, 3}; change_first(a); printf("%d\n", a[0]); // 输出 100 return 0; }你在函数里改了arr[0],main里的a[0]也跟着变了,因为两者指向同一块内存。这个特性在写排序、反转、修改数据的函数时非常有用,但如果你没意识到这一点,很容易写出“函数里改了值,外面没变”“函数里没改,外面却变了”这类让人摸不着头脑的bug。
理解到这里,再看一维二维的传参就顺了:既然传的是地址,那函数声明里的arr[]和*arr在绝大多数情况下就是同一个东西,编译器根本不区分。
2. 一维数组传参的三种写法和本质
2.1 三种等价形式,别被它们吓到
一维数组传参,你在教科书、开源项目、各种代码里会看到三种完全等价的写法:
void func(int arr[]); // 形式一:数组形式 void func(int arr[10]); // 形式二:带长度的数组形式 void func(int *arr); // 形式三:指针形式这三种写法在编译器眼里是一样的,参数类型统统是int *。你写int arr[10]并不会强制检查传入数组的长度必须是10,写int arr[]也不会报错说参数信息不完整。这个设计其实是C语言故意为之的——因为数组名已经退化成指针了,长度信息在参数传递过程中丢失,编译器也就无从检查。
日常写代码,我的建议是优先写int *arr或者int arr[],不要在方括号里写数字。写了数字容易给人误解,以为编译器会做长度检查,实际上它什么都不检查。如果你需要边界信息,就老老实实把长度作为另一个参数传进去。
2.2 为什么必须另外传一个长度参数
数组退化成指针后,函数里拿不到数组长度。你必须在调用时把长度一起传进去。常见的签名模式有两种:
// 模式一:长度放在数组参数后面 void process(int *arr, int n); // 模式二:先传长度 void process(int n, int *arr);第二种在涉及到二维数组或者多个数组时更常见,比如void matrix_multiply(int m, int n, int p, int a[][n], int b[][p], int c[][p]),长度参数放在前面是为了让后面数组声明的列数可以直接引用n、p这些变量,这个特性叫变长数组参数,后面会细聊。
如果你忘了传长度,函数的鲁棒性会大打折扣。比如写一个数组求和函数:
// 错误示范 int sum(int arr[]) { int s = 0; for (int i = 0; i < ???; i++) // 你根本不知道边界 s += arr[i]; return s; }有人会说“我可以在数组末尾放一个哨兵值,比如0”,这是字符串的做法,对int数组也行,但要求数据里不能出现哨兵值,限制很大。最稳妥的做法还是显式传长度,一劳永逸。
关于数组参数退化,还有一个常见的坑大家一起注意:不要试图通过sizeof(arr)/sizeof(arr[0])在函数内部计算长度。我在1.1节已经说过,arr在函数里是个指针,sizeof(arr) == sizeof(int *),在64位系统里是8,如果传进来的是int数组,sizeof(arr[0])是4,那结果就是2。你换个编译器、换个平台,数值还可能不同。这种代码属于典型的“看起来有道理,跑起来全错”,我在代码评审里见到一次就想捶一次。
2.3 函数内修改数组,外面会一起变
理解了地址传递,这一点就水到渠成了。你传进去的是地址,函数内部arr[i]++就是直接在原数组所在的内存上操作。所以如果你想写一个“数组加一”的函数:
void add_one(int *arr, int n) { for (int i = 0; i < n; i++) arr[i]++; }调用之后,数组元素真的都加了一。这在写冒泡排序、选择排序、数组反转这类算法时是基本操作,不需要返回值,直接原地改。
反过来,如果函数里写的是arr = malloc(...)或者arr++,那你改的只是指针的副本,不影响外面的指针变量。这就是为什么“在函数里给指针重新分配内存”这种事,需要传指针的指针(int **)才能生效。这一点后面在动态二维数组部分还会再次出现。
3. 二维数组传参:为什么让人头大
3.1 二维数组的“真实外形”不是表格,是连续内存
一维数组传参搞清楚之后,二维数组传参就难在一点:二维数组在内存里实际上是一段连续的一维内存,它的“行”和“列”只是逻辑上的划分。
比如int matrix[3][4],内存布局是先放第0行的4个int,再放第1行的4个int,再放第2行的4个int。总共12个int,依次排开,中间没有任何空隙。而matrix[i][j]这个写法,本质上是在做下标运算:*(matrix + i * 4 + j)。
这就是二维数组传参的难点所在:如果函数不知道“每行有几个元素”,它就不知道第i行第j列到底该跳到哪个位置。因为计算matrix[i][j]必须要用“列数”去乘i。
所以,二维数组传参的关键就是:列数必须出现在函数的参数里,否则编译器根本没法生成正确访问代码。
3.2 正确写法一:直接声明二维数组参数,列数必须带上
最常见、最直观的写法是:
void print_matrix(int mat[][4], int rows) { for (int i = 0; i < rows; i++) { for (int j = 0; j < 4; j++) printf("%d ", mat[i][j]); printf("\n"); } }调用时直接传数组名:
int matrix[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}}; print_matrix(matrix, 3);注意这里的参数int mat[][4],第一个方括号(行数)可以留空,第二个方括号(列数)必须写。如果你写成int mat[][],编译器会直接报错:“array type has incomplete element type”。原因很简单,编译器看到一个二维数组的元素是一维数组,一维数组的长度未知,整个类型就残缺了。这个错误我在vscode里跑代码的时候见过无数次,解决方案永远是补上列数。
还有一个隐藏知识:这种写法里,参数mat的类型其实是指向一维数组的指针,也就是int (*)[4],不是int **。理解这一点有助于解释为什么int **不能直接用来接收二维数组名,下面马上说。
3.3 正确写法二:指针数组的指针形式int (*)[N]
如果你在代码里看到int (*mat)[4]这种写法,别慌,它就是上面int mat[][4]的另一种写法。方括号写法是语法糖,指针写法更直接地揭示了底层类型:
void print_matrix(int (*mat)[4], int rows) { for (int i = 0; i < rows; i++) { for (int j = 0; j < 4; j++) printf("%d ", mat[i][j]); printf("\n"); } }int (*mat)[4]读法:先看括号内*mat,说明mat是一个指针;再看右边的[4],说明它指向的东西是一个长度为4的int数组。所以mat是指向“整行”的指针,加一就跳一行,跳过的字节数是4 * sizeof(int),这是理解二维数组传参的核心。
道理我都懂,但int (*)[4]这个写法确实劝退了不少初学C语言的选手。我给你一个读法口诀:从内往外读,先看括号里,再看右边。类似int *a[4]是“a是有4个int指针的数组”,int (*a)[4]是“a是指向有4个int数组的指针”。两者完全不一样,面试、考试、实际读代码的时候都容易栽跟头。
3.4 常见误区:int **能不能用来接收二维数组?
很多人学到指针数组、二级指针之后,会想当然地认为“二维数组名可以赋值给int **”,因为二维数组看起来像一个“指针的指针”。但真相是:不行,除非你做了转换,否则编译器会给警告,即便你强制转换了,运行也大概率出问题。
原因在前面讲过:matrix在表达式里退化为指向“第一个元素”的指针,而二维数组的第一个元素是“第一行”,是一个长度为4的int数组,所以退化后的类型是int (*)[4],而不是int **。int **的含义是“指向int指针的指针”,它要求内存里存的是指针,而二维数组里存的全是int数据,两种内存布局完全不同。
那什么时候能用int **?只有当你有“指针数组”或者动态分配的“指针的指针”结构时,才可以:
int *rows[3]; rows[0] = malloc(4 * sizeof(int)); rows[1] = malloc(4 * sizeof(int)); rows[2] = malloc(4 * sizeof(int)); // 此时 rows 退化为 int **,可以传给 int ** 参数这种“指针数组”结构是二维数组的替代方案,内存不一定连续,每一行可以单独分配。它和真正的二维数组int matrix[3][4]在内存布局上完全不同,使用方式也有区别。很多人在刷算法题、写LeetCode时用的是这种动态结构,但教科书上讲的二维数组传参又是指连续内存的写法,两者对不上号就懵了。
3.5 正确写法三:C99变长数组VLA,列数用变量传
前面几种写法都要求列数是编译期常量,比如固定写成4。但如果你的数组是动态生成的,或者行数列数在运行时才确定,直接用固定列数就不方便了。C99标准引入了变长数组(Variable Length Array,VLA),允许函数的数组参数使用前面参数传入的整数变量:
void print_matrix(int rows, int cols, int mat[rows][cols]) { for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) printf("%d ", mat[i][j]); printf("\n"); } }注意这里mat[rows][cols],rows和cols是函数参数里先声明好的变量。编译器看到这种声明,知道每行的长度是cols * sizeof(int),就能正确生成下标运算。这是官方支持又简洁的写法,我强烈推荐在GCC、Clang这类现代编译器的环境下使用。
但有两个注意点。第一,C11标准把VLA变成了“可选特性”,不是所有编译器都一定支持;MSVC(Visual Studio的C编译器)对VLA的支持就比较差,如果你在Windows上写C代码,用VS编译,要留意这一点。第二,VLA如果用在栈上,长度过大会导致栈溢出。比如:
int matrix[1000000][1000000]; // 灾难,栈直接爆掉所以VLA适合中小规模数组,大数组还是要动态分配。
4. 动态二维数组的创建与传参实战
4.1 三种常见动态二维数组结构
在实际项目里,数组的大小往往在运行时才知道,比如从文件读取行数和列数。这时候要么用C99的VLA,要么用动态内存分配。动态分配二维数组有几种常见方案,我一条条说。
方案一:一次malloc全部内存,手动计算下标
int (*mat)[cols] = malloc(rows * sizeof(*mat)); // mat 是 int (*)[cols] 类型,可以像二维数组一样用 for (int i = 0; i < rows; i++) for (int j = 0; j < cols; j++) mat[i][j] = i * cols + j;这里的sizeof(*mat)是指向“长度为cols的int数组”的大小,也就是一行的大小。malloc一次到位,内存连续,访问效率高,cache友好。配合前面第3.3节的int (*mat)[cols]传递方式,非常丝滑。缺点是如果cols是变量,那也依赖VLA特性,老编译器可能不认。
方案二:每行单独malloc,形成指针数组
int **mat = malloc(rows * sizeof(int *)); for (int i = 0; i < rows; i++) mat[i] = malloc(cols * sizeof(int));这是最经典的“动态二维数组”写法。内存不连续,每行单独分配,好处是不依赖VLA,对编译器兼容性好;缺点是malloc次数多,释放时要先释放每一行再释放指针数组,一不小心就漏内存。传参时,mat的类型就是int **,和第3.4节说的int **用法就对上了。
方案三:一维数组模拟二维
int *mat = malloc(rows * cols * sizeof(int)); // 访问 mat[i][j] 要自己换算:mat[i * cols + j]这种方案内存完全连续,性能最好,很多科学计算、图像处理库内部就是这么干的。缺点是写法不直观,下标要自己算,容易算错。如果你不在乎代码可读性而更在乎性能,可以考虑。
这三种方案各有适用场景,没有绝对的好坏。我的习惯是:数组规模小且编译器支持VLA,用方案一;规模不确定、要跨平台兼容,用方案二;性能敏感、矩阵运算密集,用方案三。
4.2 动态二维数组的释放问题
动态分配的二维数组,释放顺序很重要。对于方案二(指针数组),必须先释放每一行,再释放外层指针:
for (int i = 0; i < rows; i++) free(mat[i]); free(mat);如果你先free(mat)再free(mat[i]),那mat[i]访问的已经是释放掉的内存,属于未定义行为。虽然很多情况下不会立刻崩溃,但这就是典型的“潜伏bug”,内存检查工具Valgrind一跑就报错。
还有一个小技巧:如果你用方案一(一次malloc),释放只需要一次free(mat),因为所有内存是一块连续空间。这也是方案一在代码简洁性上的一个优势。
4.3 在函数内部为二维数组分配内存,需要传int ***
这是很多人在“把二维数组写到函数里”时会踩的坑。如果你在函数里给动态二维数组分配内存,想要调用方拿到结果,直接传int **mat是不行的,因为形参是实参的副本,函数里对mat的赋值不影响外部变量。
正确做法是传指针的指针的指针,也就是int ***:
void alloc_matrix(int ***mat, int rows, int cols) { *mat = malloc(rows * sizeof(int *)); for (int i = 0; i < rows; i++) (*mat)[i] = malloc(cols * sizeof(int)); } int main() { int **m; alloc_matrix(&m, 3, 4); // 使用 m }这里&m的类型是int ***,函数内部*mat就是外部变量m,给它赋值,外部才能看到。如果需要改的是int (*mat)[cols]这种类型,那就要传更复杂的指针类型,所以我平时更倾向于让函数返回指针:
int **alloc_matrix(int rows, int cols) { int **mat = malloc(rows * sizeof(int *)); for (int i = 0; i < rows; i++) mat[i] = malloc(cols * sizeof(int)); return mat; }用返回值的方式更直观、不容易出错。当然这也有个问题:如果函数内部还要做一些初始化操作,失败了怎么办?可以返回NULL表示分配失败。这个模式写多了,你会觉得比传int ***舒服得多。
5. 数组传参的进阶用法与易错点
5.1 用const修饰数组参数的两个好处
在函数声明里加const,很多初学者觉得这是画蛇添足。其实它在数组传参里特别实用。请看:
void print_array(const int *arr, int n) { for (int i = 0; i < n; i++) printf("%d ", arr[i]); } void process_array(int *arr, int n) { for (int i = 0; i < n; i++) arr[i] *= 2; }const int *arr的意思是“arr指向的内容不能通过arr修改”,它保护的是数组里的数据,不是指针本身。这样做的价值有两层。第一层是自我约束:代码写多了之后容易手滑,在只读函数里不小心写了个arr[i] = 0,有const编译器会直接报错,而不是让你debug半天;第二层是接口文档化:看到参数带const,调用者立刻知道这个函数不会改数据,读代码的人心理负担小很多。
反过来,如果你写的函数确实要修改数组,就不要加const,否则调用者可能会疑惑为什么sort函数传了个const数组进去。const不是装饰品,它是给人和编译器看的合同条款。
5.2 函数内对数组进行排序:冒泡排序为例
数组传参最经典的实战就是排序。在函数内部直接对传入数组做冒泡排序,不需要返回值,因为改的就是原数组:
void bubble_sort(int *arr, int n) { for (int i = 0; i < n - 1; i++) for (int j = 0; j < n - 1 - i; j++) if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } }注意内层循环的边界是n - 1 - i,因为每一轮冒泡之后,最大的元素已经排到最后,不需要再参与下一轮比较。这个边界写错,要么多比一次不影响结果,要么数组越界访问。
如果你要对结构体数组排序,比如学生信息按成绩排序,写法也是一样的,只是交换的时候可以整体交换两个结构体变量。传参时同样传结构体数组的首地址和元素个数,函数内部用arr[i].score这样的形式访问成员。C语言对结构体赋值、传参有自己的一套规则,但结构体数组的传参和一维数组传参的逻辑完全一致,没有额外难度。
5.3 字符串数组的传参:二维字符数组的坑
字符串数组是二维数组传参的一个特例,但坑特别多。比如:
char names[3][20] = {"Alice", "Bob", "Carol"}; void print_names(char str[][20], int n) { for (int i = 0; i < n; i++) printf("%s\n", str[i]); }这里的str[][20],列数是20,也就是每个字符串最大能放19个字符加一个结束符。如果你写成char str[][],编译器会报“incomplete type”,和普通二维数组的情况一模一样。
但更常见的坑是:把char *指针数组和二维字符数组搞混。比如:
char *names[] = {"Alice", "Bob", "Carol"}; // names是char *数组这里的names在传参时退化成char **,每个元素是指向字符串常量的指针。print_names(char *str[], int n)或者print_names(char **str, int n)都能接。这两种写法的内存布局完全不同:二维字符数组是连续的一块矩形区域,指针数组是一堆离散的字符串加盛放指针的数组。访问方式看起来相似,但背后的设计思维不一样,不要混用。如果你把char names[3][20]传给char **参数,编译器会报警,运行也可能莫名其妙读到乱码。这也是我在实际代码里经常看到的一个经典错误。
使用指针数组时还要注意一个细节:如果字符串是字面量,比如"Alice",那它是只读的,你试图修改names[0][0]会触发未定义行为,很多系统上直接段错误。但二维字符数组char names[3][20]是可修改的,因为它的内容是拷贝到数组里的。这一点在写代码前就要想清楚,别在运行时才被segmentation fault教做人。
5.4 数组参数和结构体的组合应用:链表节点数组
结构体数组传参在实际项目里的应用特别多。比如你要维护一个简单的链表,为了节省malloc次数,可以先预分配一块结构体数组作为节点池:
typedef struct Node { int data; struct Node *next; } Node; void init_pool(Node *pool, int n) { for (int i = 0; i < n; i++) { pool[i].data = 0; pool[i].next = NULL; } }Node *pool就是结构体数组退化为指针后的参数。这样函数内部可以通过pool[i]访问每个节点,也能通过pool[i].next链接成链表。这个技巧在嵌入式、单片机这类不能随便malloc的场景里很常用,因为预分配内存可以避免动态内存碎片化。数组传参在这里的用法和int数组完全一致,区别只是元素类型从int变成了结构体。
关于结构体数组还有一个细节:结构体可以整体赋值。所以如果你想交换两个节点,不需要逐个成员交换,直接:
Node tmp = pool[i]; pool[i] = pool[j]; pool[j] = tmp;编译器会帮你按位拷贝整个结构体。如果结构体很大,这个赋值开销会有点高,但代码可读性会好很多。
5.5 用指针运算操作数组参数的两个常用技巧
既然数组参数已经是指针了,那函数内部完全可以用指针运算代替下标访问。比如数组求和可以写成:
int sum(const int *arr, int n) { int s = 0; for (const int *p = arr; p < arr + n; p++) s += *p; return s; }这种写法在指针和数组纠缠不清的时候特别锻炼思维。还有一个技巧是“指针的指针遍历二维数组”:
// 假设mat是int (*mat)[4]类型 void print_matrix(int (*mat)[4], int rows) { for (int i = 0; i < rows; i++) { int *row = mat[i]; // 拿到第i行首地址 for (int j = 0; j < 4; j++) printf("%d ", row[j]); printf("\n"); } }mat[i]的类型是int *(指向第i行的第一个int),拿到它之后就可以当一维数组用。这种转换思维在写图像处理、矩阵运算代码时非常实用,代码也会比直接写mat[i][j]多一些灵活性。
指针运算的边界一定要小心。p < arr + n这个比较只在同一个数组内才安全,如果指针已经跑到数组外面,比较结果是不确定的。我在写循环时总是习惯确认终止条件是否安全,别贪图简洁写出越界访问。
6. 常见问题速查表与调试心得
6.1 一维二维传参典型错误对照
我把这几年见过的、学生作业里反复出现的错误汇总成了一张表。写代码之前对照看一遍,能规避掉大部分坑。
| 错误写法 | 错误原因 | 正确写法 |
|---|---|---|
void f(int a[][]) | 二维数组参数缺少列数,类型不完整 | void f(int a[][N]) |
void f(int a[10])并指望做长度检查 | 参数实际退化为指针,[10]没有强制作用 | void f(int *a, int n) |
函数内sizeof(a)/sizeof(a[0])计算长度 | a是指针,算出来是2或1,不是真实长度 | 显式传长度参数 |
把int a[3][4]直接传给int **param | 类型不匹配,退化后的类型是int (*)[4],不是int ** | 改用int (*param)[4] |
在函数内arr = malloc(...)期望外部指针变化 | 形参是实参的副本,赋值不影响外部 | 用int **或返回指针 |
释放动态二维数组时先free(mat)再释放行 | 先释放外层指针,内层内存无法访问,泄漏 | 先释放所有行,再释放外层指针 |
char names[3][20]传递给char ** | 内存模型不同,编译警告或运行错误 | 用char (*)[20]接 |
这张表我每次在项目组做代码评审时都会拿出来讲一遍。尤其是第二条和第三条,几乎是零基础转行学C语言人群中的重灾区。
6.2 段错误和内存泄漏的排查思路
数组传参最常见的运行时错误就是段错误。段错误发生的原因往往是访问了非法地址,比如数组越界、把错误的类型强转后访问、或者访问了已释放的内存。遇到过段错误不要慌,按下面三条路查:
第一,确认下标范围。把所有访问数组的地方列出来,核对是不是到了n-1又访问了n,或者内层循环的列下标是不是超过了列数。尤其是二维数组的“行优先”和“列优先”搞反的时候,越界几乎是必然的。
第二,用gdb定位崩溃点。在vscode或者Linux终端里,编译时加-g选项,然后用gdb ./a.out启动,崩溃后输入bt(backtrace)查看调用栈,它能直接告诉你崩在哪个文件的哪一行。这个方法比你自己凭空猜要快得多。
第三,用工具检测内存错误。Linux环境下Valgrind是神器:
gcc -g -o test test.c valgrind --leak-check=full ./testValgrind会报告每一条非法的内存读写和未被释放的内存块,包括行号。我自己在写动态二维数组相关代码时,几乎每次都跑一遍Valgrind,能抓到很多肉眼发现不了的越界访问。
说到内存泄漏再啰嗦一句:动态分配的每一个malloc都要有对应的free,数组的malloc和结构体的malloc一样,释放时机错了、少了,都会给程序埋雷。特别是动态二维数组的释放顺序,我在4.2节已经强调过,这里再重复一次:先释放内层每一行,再释放外层指针。
6.3 我对数组传参这个知识点的总体感受
数组传参是C语言从“会写小程序”走向“会写工程代码”的一道分水岭。很多人考二级、期末考的时候把规则背得滚瓜烂熟,但一到了实际项目里,二维数组传参还是会卡壳。我个人的体会是,考试考的是语法点,工程考验的是你对内存模型的理解。
你自己写代码时,多想想这几个问题:数组名在表达式里到底是什么类型?函数参数里的方括号是真的数组还是语法糖?二维数组里matrix[i][j]的寻址要借助列数,列数为什么不能省略?把这些想透了,一维二维的传参就再也不会困扰你了。
最后分享一个小技巧:如果你要写一个接收二维数组的通用函数,又不想把列数写死,C99的VLA语法写起来最舒服:
void process(int rows, int cols, int mat[rows][cols]) { mat[0][0] = 1; }这个写法在GCC和Clang下实测都很稳,参数顺序也符合直觉:先传维度,再传数组。不过在VS环境下编译要格外小心VLA支持问题,我遇到过一次在Linux上写得好好的代码,拿到Windows的MSVC编译直接报错,最后老老实实改成int (*mat)[cols]才通过。平台差异这种东西,踩过一次坑就要长记性。数组传参的规则在所有C编译器上是一致的,但VLA、柔性数组这些C99特性不是,开发前先确认你目标平台的编译器支持情况,能省掉不少移植的麻烦。