16. 【C语言】指针与数组的亲密关系

📅 2026/7/5 14:57:22 👤 编程新知 🏷️ 技术资讯
16. 【C语言】指针与数组的亲密关系 上一篇我们初识指针知道了数组名就是首元素地址arr[i]就是*(arri)。但如果你就此认为“数组和指针就是一回事”那可就埋下隐患了——它们有微妙而重要的差别而且围绕它们还衍生出几个让无数初学者抓狂的概念指针数组、数组指针、二级指针。今天我们就来彻底理清这些关系。顺便你会真正理解 C 语言字符串的本质以及main函数那两个神秘参数argc和argv到底是怎么回事。一、数组名 ≠ 指针三条铁律数组名在绝大多数表达式中会被自动转换成指向首元素的指针但这条规则有三个重要的例外。记住这三个例外你就能分清什么时候数组是数组什么时候它会变成指针。例外 1sizeof(数组名)得到整个数组的大小intarr[10];printf(%zu\n,sizeof(arr));// 40假设 int 4 字节printf(%zu\n,sizeof(arr[0]));// 8指针大小64位系统如果arr真是一个指针sizeof(arr)应该是指针大小。但它不是它返回整个数组的字节数。这是区分数组和指针最直接的方法。例外 2数组名得到指向整个数组的指针intarr[5];int*p1arr;// 指向 int 的指针类型 int*int(*p2)[5]arr;// 指向“5个int组成的数组”的指针类型 int(*)[5]p2就是所谓的数组指针它的类型不是int*而是int(*)[5]。p21会跳过整个数组20 字节而不是一个int。这个区别我们稍后详细展开。例外 3用字符串字面量初始化字符数组时charstr[]hello;// 在栈上创建一个数组内容是 hello 的副本char*ptrhello;// ptr 指向存储在只读数据区的字符串字面量这是两种完全不同的东西前者是可修改的字符数组后者是指向只读字符串的指针。这也是字符串的经典陷阱我们马上会讲。二、指针数组 vs 数组指针别被名字绕晕这是 C 语言最经典的混淆点。记住一条规则从右往左读[]的优先级高于*括号可以改变结合顺序。指针数组int *p[10]读法p是一个数组有 10 个元素每个元素是int*指向 int 的指针。inta1,b2,c3;int*ptr_array[3]{a,b,c};// 一个存了三个指针的数组for(inti0;i3;i){printf(%d ,*ptr_array[i]);// 输出 1 2 3}指针数组最常见的应用就是存储多个字符串char*fruits[]{apple,banana,cherry};// fruits[0] 是一个 char*指向字符串 apple 的首字符这比用二维char数组更灵活——每个字符串的长度可以不同不用浪费空间。数组指针int (*p)[10]读法p是一个指针它指向一个“有 10 个 int 的数组”。intarr[10];int(*p)[10]arr;// p 指向 arr 整个数组// 访问元素(*p)[3]100;// 等价于 arr[3] 100;为什么需要括号因为[]优先级高于*。没有括号的int *p[10]是“装指针的数组”有括号的int (*p)[10]是“指数组的针”。数组指针最常用于处理二维数组。当你把二维数组传给函数时形参也可以写成数组指针voidprint_matrix(int(*mat)[4],introws){for(inti0;irows;i){for(intj0;j4;j){printf(%d ,mat[i][j]);}printf(\n);}}intmain(void){intmatrix[3][4]{{1,2,3,4},{5,6,7,8},{9,10,11,12}};print_matrix(matrix,3);return0;}matrix作为参数传递时退化为指向首元素一行的指针那一行是 4 个 int 的数组所以类型恰好是int(*)[4]。这就是为什么二维数组形参必须写列数——编译器需要知道每一行多长。快速对照表写法本质解释int *p[10]数组10 个 int* 组成的数组int (*p)[10]指针指向“10个int数组”的指针int *p(int)函数函数名为 p返回 int*今天不讲int (*p)(int)指针指向“接收int返回int”的函数的指针以后讲记忆口诀最后一步看符号——最后解析为数组就是数组最后解析为指针就是指针。三、字符串与指针字符数组 vs 字符指针在 C 语言里字符串并不是一种独立的类型。它就是以\0结尾的字符数组。但对字符串的存储方式有两种截然不同的形式这个区别一定要分清楚。形式一字符数组可修改charstr[]hello;str[0]H;// 合法数组内容可以修改printf(%s\n,str);// Hellostr是一个数组在栈上分配了 6 个字节h e l l o \0。hello的每个字符被复制到了这个数组里。你是这个内存的主人可以随意改。形式二字符指针指向只读字符串字面量char*ptrhello;ptr[0]H;// 危险未定义行为通常会崩溃这里ptr是一个指针指向字符串字面量hello。这个字面量存储在只读数据区.rodata操作系统不允许你修改它。试图修改要么崩溃段错误要么什么都没发生但这是未定义行为千万不要依赖。正确做法如果你声明了一个指向字符串字面量的指针并且以后不会修改它用const保护起来constchar*ptrhello;// ptr[0] H 直接编译报错形式三用指针遍历字符串不管字符串是以数组还是字面量形式存在你都可以用指针来遍历它charstr[]hello;char*pstr;while(*p!\0){printf(%c ,*p);p;}// 输出 h e l l o标准库的strlen本质上就是这么干的size_tmy_strlen(constchar*s){constchar*ps;while(*p)p;// *p 为 \0 时退出returnp-s;// 两个指针相减得到元素个数}四、命令行参数argc与argv也许你一直好奇main函数的另一种写法intmain(intargc,char*argv[]){// ...}现在我们可以理解它了。argcargument count命令行参数的个数包含程序名本身。argvargument vector一个指针数组每个元素是一个char*指向一个参数字符串。char *argv[]等价于char **argv数组作为形参退化为指针指针数组退化为二级指针。我们先直观理解二级指针下一节细说。假如你的程序叫echo运行命令./echo hello world123那么argc 4argv[0]./echoargv[1]helloargv[2]worldargv[3]123argv[4]NULL标准保证最后一个指针是 NULL一个简单程序打印所有参数#includestdio.hintmain(intargc,char*argv[]){printf(参数个数%d\n,argc);for(inti0;iargc;i){printf(argv[%d] %s\n,i,argv[i]);}return0;}argv的图形化理解argv - [0] - ./echo\0 [1] - hello\0 [2] - world\0 [3] - 123\0 [4] - NULLargv本身是一个char**它指向一个由char*指针组成的数组每个char*又指向一个实际的字符串。五、二级指针初识如果指针是一个变量存着另一个变量的地址那二级指针就是一个变量存着另一个指针的地址。inta10;int*pa;// p 指向 aint**ppp;// pp 指向 pprintf(%d\n,**pp);// **pp *(*pp) *p a 10**pp就是a。二级指针最常见的用途在函数里修改一级指针本身比如动态分配内存并传出操作指针数组比如argv就是char**一个函数示例修改传入的指针让它指向新分配的内存后面动态内存会讲这里先感性认知voidallocate(int**ptr){*ptrmalloc(sizeof(int));// 修改 ptr 指向的那个指针**ptr100;}intmain(void){int*pNULL;allocate(p);// 传 p 的地址printf(%d\n,*p);// 100free(p);return0;}在二维动态数组的分配中二级指针也是核心角色这个我们在动态内存部分会专门展开。六、如何读懂复杂声明右左法则当声明混有指针、数组、函数时比如int *(*p[10])(int)怎么读懂可以按“右左法则”从变量名开始。先往右看遇到[ ]或( )就读出来。再往左看遇到*就读“指针”。遇到括号就跳进去重复上述过程。以char *argv[]为例argv是……先右看[]说明是数组。再左看*说明是指针 →argv是一个指针数组。再左看char说明每个元素指向字符。以int (*p)[10]为例p遇到括号先处理括号内。左看*p是指针。括号外右看[10]指向有 10 个元素的数组。左看int数组元素是 int。结论p是指向有 10 个 int 的数组的指针。这个法则很实用以后遇到复杂的声明多试几次就熟了。七、常见错误与陷阱1. 把指针数组和数组指针搞反int(*ptr)[5];// 指针指向5个int的数组int*ptr[5];// 数组有5个int*2. 试图修改字符串字面量char*shello;s[0]H;// 未定义行为崩溃或无效用const char *或字符数组。3. 对函数参数中的数组用sizeofvoidfunc(intarr[]){printf(%zu\n,sizeof(arr));// 总是指针大小不是数组大小}4. 二级指针解引用层级混乱inta5;int*pa;int**ppp;printf(%d\n,*pp);// 输出的是 p 的值即 a 的地址不是 a*pp是p**pp才是a。八、小结今天我们把指针和数组的关系理清了。核心记住数组名在大多数情况下退化为指针但sizeof、数组名、字符串字面量初始化是例外。指针数组int *p[10]和数组指针int (*p)[10]是完全不同的东西读声明的技巧是看优先级和结合方向。字符串可以用字符数组可修改或字符指针指向字面量不可修改来存储。main的argv是指针数组/二级指针的典型应用。二级指针是指向指针的指针用于间接修改指针本身。现在你对指针已经有了基本的感觉。但指针最灵活也最危险的用途还在后面——动态内存分配。下一篇我们就进入malloc、free、堆与栈的广阔天地让程序能够按需索取内存而不再受编译时数组大小的束缚。课后小练习分别声明一个指针数组和一个数组指针并写出它们的类型含义。用sizeof验证它们的大小数组指针是指针大小指针数组是多个指针大小。用argv实现一个简单的echo程序要求将命令行参数逆序输出。分析这段代码并修正错误char*msghello;msg[0]H;printf(%s\n,msg);写一个函数void reverse_string(char *str)用指针操作不用下标实现字符串的原地反转。挑战解释以下声明的含义并尝试各写一个简单用例int*arr1[10];int(*arr2)[10];int**arr3;我们下期见获取本系列示例代码请访问 GitCode 仓库。