☰
C语言二维数组传参三种写法详解:内存布局与避坑指南
2026/10/17 23:19:43 网站建设 项目流程

简介:这份PDF资料面向C语言初学者与需要巩固指针、数组知识的开发者,系统讲解二维数组作为函数参数传递的三种方法,帮助解决形参声明与实参匹配时容易混淆的问题。资源共1个PDF文件,压缩包约36KB,内容以文字讲解配合示例代码为主,篇幅精炼便于随时查阅。三种方法分别为形参给出第二维长度、形参声明为指向数组的指针、形参声明为指针的指针,每种都配有完整可运行的代码片段,并特别说明第三种方式下不能使用array[i][j]取值,而应按一维数组思路用array[i*columns+j]访问,同时给出输出二维数组的标记控制写法。目前已有22385人学习,适合想理清二维数组传参本质、避免指针退化踩坑的读者参考。

1. 二维数组传参的三种写法:从编译报错到内存布局一次说清

刚接手一个图像处理模块的移植任务时,我遇到一段代码在本地编译通过、换到目标板就段错误。排查半天,问题出在二维数组传参上——函数签名写的是char **,调用方传的却是char [3][5]。编译器没报错,运行时直接翻车。这类问题在 C 语言里太常见了:二维数组作为函数参数传递,写法有好几种,但每种背后的内存模型完全不同,混用就是给自己埋雷。

这份资源把三种传参方式拆开讲透了:形参给出第二维长度、形参声明为指向数组的指针、形参声明为指针的指针。它适合两类人——刚学 C 语言、被[]和*绕晕的新手,以及做嵌入式或系统编程、需要精确控制内存布局的从业者。核心价值在于:它不只给代码,还点出了第三种方式下不能用array[i][j]取值这个关键边界。下面我按自己的理解重新梳理一遍,把每种写法的适用场景和坑位标清楚。

2. 三种传参方式的底层逻辑与代码实现

2.1 为什么char **接不住char [3][5]

要理解三种写法的区别,得先看二维数组在内存里长什么样。char str[3][5]是一片连续的 15 字节内存,按行优先排列:str[0][0]到str[0][4]占前 5 字节,接着str[1][0]到str[1][4],以此类推。数组名str在大多数表达式里退化为指向首元素的指针,而首元素是char[5],所以str的类型是char (*)[5]——指向长度为 5 的字符数组的指针。

char **则是完全不同的东西:它是指向指针的指针,内存里存的是一个地址,那个地址再指向另一个地址。如果你把str强制转成char **传给函数,函数里执行str[i]时,它会去读str指向的内存的前 8 字节(64 位系统)当作地址——但那片内存里存的是字符'a'、'b'、'c',根本不是有效地址。这就是段错误的根源。

所以三种传参方式的本质区别在于:前两种保留了二维数组的“数组”属性,编译器知道每行多长,能做正确的地址计算;第三种把二维数组降级成了指针数组,行与行之间不再保证连续,取值方式也必须跟着变。

2.2 方法一与方法二:形参给出第二维长度 vs 指向数组的指针

这两种写法在编译器眼里其实是等价的。先看代码:

#include <stdio.h> // 方法一:形参给出第二维长度 void func_v1(int n, char str[][5]) { for (int i = 0; i < n; i++) { printf("str[%d] = %s\n", i, str[i]); } } // 方法二:形参声明为指向数组的指针 void func_v2(int n, char (*str)[5]) { for (int i = 0; i < n; i++) { printf("str[%d] = %s\n", i, str[i]); } } int main(void) { char str[][5] = {"abc", "def", "ghi"}; func_v1(3, str); func_v2(3, str); return 0; }

char str[][5]在函数参数列表里,编译器会自动把它调整成char (*str)[5]。也就是说,方法一和方法二生成的机器码完全一样,只是写法不同。方法一更直观,适合教学和快速阅读;方法二更明确地表达了“这是一个指针”的语义,在复杂声明里不容易看错。

参数说明:n是行数,str是指向char[5]的指针。函数内部用str[i]就能拿到第 i 行的首地址,因为编译器知道每行跨度是 5 字节,str + i实际偏移i * 5字节。这里printf用%s输出是安全的,因为每行末尾有字符串结束符。

注意:第二维长度必须写死在形参里,不能传变量。C89 不支持变长数组,C99 虽然支持 VLA 但嵌入式编译器往往不认。如果每行长度不固定,得换第三种方式或手动计算偏移。

2.3 方法三:指针的指针与手动地址映射

第三种写法最灵活,也最容易踩坑。先看资源里给的示例,我补全并修正了输出格式:

#include <stdio.h> // 方法三:形参声明为指针的指针 void func_v3(int rows, int cols, char **str) { int tag = 0; printf("使用传递过来的二维数组参数输出二维数组\n"); for (int i = 0; i < rows * cols; i++) { printf("%c,", str[0][i]); // 注意:这里按一维方式访问 if (tag == cols - 1) { tag = 0; printf("\n"); } else { tag++; } } } int main(void) { char str[][5] = {"abc", "def", "ghi"}; char *p[3]; p[0] = &str[0][0]; p[1] = str[1]; p[2] = str[2]; func_v3(3, 5, p); return 0; }

这里的关键在于p是一个指针数组,p[0]指向str[0][0],p[1]指向str[1][0],p[2]指向str[2][0]。传给函数后,str是char **,str[0]拿到的是p[0]的值,也就是&str[0][0]。但str[1]拿到的是p[1],指向str[1][0]——注意,p[0]和p[1]之间没有固定的偏移关系,它们只是恰好指向了连续内存的不同位置。

资源里提到“不能使用array[i][j]这种形式来取值”,这个说法需要修正。准确地说:如果array是char **,array[i][j]在语法上是合法的,但它做的是“先取array[i]这个指针,再偏移 j 字节”。如果array[i]指向的是每行连续的二维数组,那array[i][j]恰好能取到正确值。真正不能用array[i][j]的场景是:你把二维数组强行转成char **传进去,此时array[i]读到的不是地址而是字符数据,再偏移就是非法访问。

资源里建议用array[i * cols + j]这种一维索引方式,前提是array被当作一维指针使用。但在上面的代码里,str[0][i]能正常工作,因为str[0]确实是一个有效指针。所以更准确的理解是:第三种方式下,函数内部把str当成“指针数组”来用,每个str[i]是一个独立的行指针,行与行之间不保证连续。如果你需要按连续内存遍历,应该用str[0][i * cols + j]这种形式。

参数说明:rows和cols分别表示行数和列数,str是char **。调用方需要自己构建指针数组p,把每行的首地址填进去。这种方式适合行长度不一致的“锯齿数组”,也适合从其他语言接口传进来的字符串数组。

3. 避坑指南:三种传参方式下的典型翻车现场

3.1 段错误:把char [3][5]直接传给char **

现象:函数签名是void f(char **arr),调用时写f(str),其中str是char str[3][5]。编译通过,运行时段错误。

原因:str退化为char (*)[5],强制转成char **后,函数里arr[0]读的是str前 8 字节的内容,也就是'a'、'b'、'c'、'\0'和后面几个字节,拼成一个非法地址。解引用这个地址就崩了。

解决:要么改函数签名为char (*arr)[5],要么在调用方构建char *p[3]指针数组再传。不要指望编译器帮你检查这种类型不匹配。

3.2 第二维长度写错导致数据错位

现象:函数形参写char str[][4],实际传入的数组是char str[3][5]。函数内str[1]访问的是偏移 4 字节的位置,但实际第二行从偏移 5 字节开始,输出全是乱码。

原因:编译器按形参声明的第二维长度计算行偏移,声明和实际不一致时,地址计算全错。

解决:第二维长度必须和实际数组声明完全一致。如果拿不准,用sizeof在调用方算一下:sizeof(str[0])就是每行字节数。或者干脆用第三种方式,把行长度作为参数传进去。

3.3 在函数内用sizeof算行数

现象:函数里写int rows = sizeof(str) / sizeof(str[0]),结果 rows 永远是 1 或 2(取决于指针大小),不是实际行数。

原因:数组作为函数参数传递时,退化为指针,sizeof(str)算的是指针大小,不是数组总大小。

解决:行数必须作为参数传进来。这是 C 语言的基本规则,没有例外。如果不想手动传,可以用宏在调用方计算,或者把数组包装成结构体传指针。

3.4 第三种方式下混用arr[i][j]和arr[0][i * cols + j]

现象:函数内用arr[i][j]取值,有时正确有时错误,取决于调用方怎么构建指针数组。

原因:arr[i][j]等价于*(*(arr + i) + j)。如果arr[i]指向的行内存是连续的,且j没超出行长度,结果正确。但如果arr[i]指向的是独立分配的内存块,arr[i][j]仍然正确——只要j在合法范围内。真正出问题的是:调用方把二维数组首地址直接转成char **,此时arr[i]不是有效指针。

解决:统一约定。如果函数签名是char **,调用方必须传指针数组,函数内用arr[i][j]或arr[i]都可以。如果调用方只有连续二维数组,要么改签名,要么在函数内用char (*)[N]接收。

3.5 字符串没有结束符导致越界读取

现象:char str[3][5] = {'a','b','c','d','e', ...},用%s输出时打印出乱码或崩溃。

原因:%s要求字符串以'\0'结尾,但数组被填满了 5 个字符,没有结束符。printf会一直往后读,直到遇到内存中偶然的 0。

解决:要么留一个字节给'\0',声明成char str[3][6];要么用%.*s指定宽度输出:printf("%.*s", 5, str[i])。在嵌入式环境里,后者更可控。

4. 进阶技巧:用sizeof和宏做编译期检查

实际项目里,我一般会加一层宏来减少手写错误。比如定义一个传参宏,自动计算行数和列数:

#include <stdio.h> #define ARRAY_ROWS(a) (sizeof(a) / sizeof((a)[0])) #define ARRAY_COLS(a) (sizeof((a)[0]) / sizeof((a)[0][0])) #define PRINT_2D_STR(arr) do { \ for (size_t i = 0; i < ARRAY_ROWS(arr); i++) { \ printf("row %zu: %.*s\n", i, (int)ARRAY_COLS(arr), (arr)[i]); \ } \ } while (0) int main(void) { char str[][5] = {"abc", "def", "ghi"}; PRINT_2D_STR(str); return 0; }

这个宏只在数组未退化的作用域内有效——也就是说,必须在main或其他直接声明数组的函数里用。一旦传给函数,sizeof就失效了。所以我的习惯是:在调用方用宏算好行数和列数,作为参数传给函数;函数内部只负责按参数遍历,不再尝试用sizeof推导。

另一个技巧是用_Static_assert(C11)做编译期检查:

#define CHECK_COLS(arr, expected) \ _Static_assert(sizeof((arr)[0]) / sizeof((arr)[0][0]) == (expected), \ "column count mismatch")

在调用函数前加一句CHECK_COLS(str, 5),如果第二维长度和函数期望的不一致,编译直接报错,不用等到运行时才发现数据错位。这个手段在跨平台移植时特别有用——不同平台的对齐方式可能不同,但列数不匹配是硬错误,早发现早省事。

从那以后我每次写二维数组传参,都强制走一遍:先确认内存布局是连续还是指针数组,再决定函数签名,最后在调用方加编译期检查。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询