news 2026/9/26 7:12:59

C语言数组传参全解析:从一维到二维、指针退化到动态分配

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言数组传参全解析:从一维到二维、指针退化到动态分配

如果你学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 ./test

Valgrind会报告每一条非法的内存读写和未被释放的内存块,包括行号。我自己在写动态二维数组相关代码时,几乎每次都跑一遍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特性不是,开发前先确认你目标平台的编译器支持情况,能省掉不少移植的麻烦。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 7:12:31

DeskcommCRM实战:从部署到权限管理的自建客户关系管理系统指南

1. 为什么我最终选择了 DeskcommCRM 而不是市面上那些 CRM做了几年销售团队管理&#xff0c;我前后换过三套客户管理系统&#xff0c;从大厂的企业版到小团队免费工具都用了一圈。说实话&#xff0c;每套系统都有让我咬牙的地方&#xff1a;要么收费按人头算&#xff0c;团队一…

作者头像 李华
网站建设 2026/9/26 7:11:47

AI Agent沙箱为何失效?事件复盘与多层防护实战

前两周我在内部环境跑Gemini的Agent测试&#xff0c;任务本身不复杂&#xff1a;让Agent调研三家公司公开的定价页面和技术栈信息&#xff0c;最后输出一份对比报告。我原本预期它会老老实实待在受控的浏览器容器里&#xff0c;所有出站请求都经过白名单过滤&#xff0c;所有动…

作者头像 李华
网站建设 2026/9/26 7:10:41

支付系统设计与实践:金融服务中台从账户到风控的完整架构

做支付系统这几年&#xff0c;最深的体会是“钱的事情最容易在细节里翻车”。我刚接手 financial-services 这个项目时&#xff0c;原以为就是把支付接口包一层再开放出去&#xff0c;真正深入之后才发现&#xff0c;金融服务要解决的是“交易状态、资金状态、风险状态”三者之…

作者头像 李华
网站建设 2026/9/26 7:10:37

Atlas 300V 24G部署YOLOv5实战:从模型转换到推理优化

前阵子接了个项目&#xff0c;要在边缘侧做实时目标检测&#xff0c;模型用的是YOLOv5s&#xff0c;算力平台纠结了很久&#xff0c;最后选定华为Atlas 300V 24G这张卡。很多人听到这卡的第一反应就是&#xff1a;“这不就是个运算加速卡吗&#xff1f;跟显卡有区别吗&#xff…

作者头像 李华
网站建设 2026/9/26 7:10:12

NAS本地部署LandPPT:Docker一键搭建AI自动生成PPT工具

1. 从一句话到一套PPT&#xff1a;LandPPT到底解决了什么问题第一次看到“一句话生成PPT”这个说法&#xff0c;我的反应和大多数人一样&#xff1a;又是营销噱头吧。直到我在自己的NAS上把LandPPT跑起来&#xff0c;输入了一句“帮我做一个关于家庭NAS选购指南的PPT&#xff0…

作者头像 李华