本章目标搞懂数组名的两个特殊身份 彻底分清「指针数组」和「数组指针」 掌握二级指针引入数组和指针到底是什么关系很多同学学到这一章的时候会感觉脑子里一团浆糊「数组名不是指针吗」「为什么arr和arr不一样」「int *arr[5]和int (*arr)[5]到底谁是谁」这一章我们把这些全部理清楚。先说结论数组名在大多数情况下等价于指针首元素地址但数组名不是指针——它们是两个不同的东西只是在大多数场景下「行为一致」。第一部分数组名的两个特殊身份1.1 普通情况数组名 首元素地址数组名在大多数情况下就是首元素的地址intarr[5]{1,2,3,4,5};printf(%p\n,arr);// 等价于 arr[0]printf(%p\n,arr[0]);// 首元素地址printf(%p\n,arr1);// 等价于 arr[1]跳过 4 字节在这个层面arr和int*几乎没有区别——可以用指针运算可以用下标访问可以解引用。1.2 例外一sizeof(数组名) 整个数组的字节数intarr[5]{1,2,3,4,5};printf(%zu\n,sizeof(arr));// 输出205 × 4 字节printf(%zu\n,sizeof(arr0));// arr0 是 int*输出864位arr作为sizeof的参数时编译器把它视为「整个数组对象」而非「首元素地址」所以返回的是整个数组的大小。⚠️提醒这个例外是编译器规定的特殊行为不是语法错误。sizeof是运算符不是函数——编译器在编译阶段就能确定数组的实际大小因为数组在栈上或全局区大小在编译时就固定了。1.3 例外二数组名 整个数组的地址intarr[5]{1,2,3,4,5};printf(%p\n,arr);// int*首元素地址printf(%p\n,arr);// int(*)[5]整个数组的地址arr的类型是int(*)[5]——「指向包含 5 个整型的数组」的指针。这和int*有什么区别最大的区别是 1 跳的步长printf(%p\n,arr1);// arr1 arr 4字节 → 跳到 arr[1]printf(%p\n,arr1);// arr1 arr 20字节 → 跳过整个数组表达式类型1 跳多少字节arr/arr[0]int*4 字节1 个 intarrint(*)[5]20 字节5 个 int 整个数组图解内存布局arr[0] arr[1] arr[2] arr[3] arr[4] ├────────┬────────┬────────┬────────┬────────┤ arr ──→ ↑ arr1 跳到这里跳过整个数组 arr ──→ ↑──20字节──────────┘ arr1 ──→ ↑──4字节──┘第二部分一维数组传参——形参退化为指针2.1 数组传参时发生了什么当你把数组传给一个函数时实际上传的是首元素地址函数内部的形参会退化为指针#includestdio.hvoidtest(intarr[])// 语法上写成数组{printf(%zu\n,sizeof(arr));// 输出 864位不是 20}intmain(){intarr[5]{1,2,3,4,5};printf(%zu\n,sizeof(arr));// 输出 20test(arr);}在test函数里arr实际上是int*不是数组。所以sizeof(arr) 8而不是20。⚠️提醒这就是为什么在函数内部用 sizeof 计算数组长度是错误的——函数接收到的不是整个数组只是一个指针。任何形如int arr[]的参数本质上都是int* arr只是一个存地址的变量。2.2 正确做法长度由调用者传入// 正确让主调函数计算长度传进来voidprint_array(int*p,intlen)// 推荐写法语义最清晰{for(inti0;ilen;i)printf(%d ,p[i]);// p[i] 等价于 *(pi)}// 也可以写成数组语法但本质相同voidprint_array2(intarr[],intlen){for(inti0;ilen;i)printf(%d ,arr[i]);}推荐写法void print_array(int *p, int len)——显式告诉调用者「我接收的是一个指针和一个长度」不会让人误以为函数内部能拿到数组的大小。第三部分二级指针——指向指针的指针3.1 什么场景需要二级指针想象一个场景你想在函数内部修改一个指针变量本身的值不是它指向的内容而不是修改它指向的变量的值。voidtest(int**pp)// pp 是指向 int* 的指针{*ppNULL;// 修改了调用者传入的指针变量本身}这和传址调用的逻辑一样——如果你想改「指针变量」本身那就要传「指针变量的地址」也就是二级指针。3.2 二级指针的定义与使用inta10;int*p1a;// 一级指针p1 存 a 的地址int**p2p1;// 二级指针p2 存 p1 的地址printf(%p\n,*p2);// *p2 p1 aprintf(%d\n,**p2);// **p2 *p1 a 10解引用次数表达式结果0 次p2p1p1 自己的地址1 次*p2p1存的是a2 次**p2a值 103.3 实战用二级指针让函数重置指针#includestdio.h#includestdlib.h// 在函数内部将指针置为 NULL修改了外部的 pvoidreset_pointer(int**pp){*ppNULL;// 把 pp 指向的指针变量外部的 p改成 NULL}intmain(){int*pmalloc(sizeof(int));// p 指向一块堆内存*p100;printf(p 的值 %p\n,p);reset_pointer(p);// 把 p 本身置为 NULLprintf(p 的值 %p\n,p);// p NULL (0x0)return0;}如果不使用二级指针这个函数根本做不到——因为void reset(int *p)只能修改p的副本不能修改外部的p本身。第四部分指针数组 vs 数组指针——最易混淆的一对4.1 先搞清优先级[] 高于 *C语言运算符优先级[]下标*解引用所以int*arr[5];// arr 先和 [5] 结合 → arr 是数组// 再和 * 结合 → 数组的元素类型是 int*// → 元素是指针的数组 指针数组int(*p)[5];// p 先和 * 结合 → p 是指针// 再和 [5] 结合 → 指针指向 int[5]// → 指向数组的指针 数组指针记忆口诀arr[5]先成数组再被*修饰(*p)[5]先成指针再指向数组。4.2 指针数组Array of Pointers本质数组数组里的每个元素都是指针。inta1,b2,c3;int*arr[3]{a,b,c};// arr 是包含 3 个 int* 元素的数组printf(%d\n,*arr[0]);// *arr[0] 1printf(%d\n,*arr[1]);// *arr[1] 2printf(%d\n,*arr[2]);// *arr[2] 3典型应用字符串数组char*names[]{Alice,Bob,Charlie};for(inti0;i3;i){printf(%s\n,names[i]);// names[i] 是 char*直接打印字符串}Alice这种字符串常量本身就是一个char*首字符地址所以可以直接放在指针数组里。4.3 数组指针Pointer to Array本质指针指针指向一个数组。intarr[5]{1,2,3,4,5};int(*p)[5]arr;// p 是指向 int[5] 的指针printf(%d\n,(*p)[0]);// 先解引用得到数组再用 [0] 取第1个元素1printf(%d\n,(*p)[2]);// (*p)[2] arr[2] 3printf(%d\n,*(*p3));// 等价于 arr[3] 4为什么需要数组指针最常见的场景是二维数组的函数参数#includestdio.hvoidprint_matrix(int(*p)[3],introws)// p 是数组指针指向 int[3]{for(inti0;irows;i){for(intj0;j3;j)printf(%d ,p[i][j]);printf(\n);}}intmain(){intmatrix[2][3]{{1,2,3},{4,5,6}};print_matrix(matrix,2);// matrix 退化为 int(*)[3]return0;}4.4 速查对比表写法谁先结合本质1 跳多少字节int *arr[5]arr[5]数组5 × sizeof(int*)int (*p)[5]*p[5]指针5 × sizeof(int)第五部分综合实战——sizeof 全家福笔试题这道题在面试和考研中极高频也是最容易混淆arr、arr、arr0、arr[0]等各种写法的综合题。#includestdio.hintmain(){inta[5]{1,2,3,4};printf(%zu\n,sizeof(a));// ① 整个数组 → 5×420printf(%zu\n,sizeof(a0));// ② a0 int* → 8printf(%zu\n,sizeof(a));// ③ a int(*)[5] → 8printf(%zu\n,sizeof(a1));// ④ a1 跳过整个数组 → 8printf(%zu\n,sizeof(a[0]1));// ⑤ a[0]1 int* → 8return0;}逐题解析题号表达式类型sizeof 大小解析①sizeof(a)数组名特例20sizeof(数组名) 整个数组②sizeof(a0)int*8a0变成指针不再是「数组名特例」③sizeof(a)int(*)[5]8a是数组指针仍是指针大小④sizeof(a1)int(*)[5]8同上类型不变大小不变⑤sizeof(a[0]1)int*8a[0]1是int*是指针大小⚠️提醒很多人搞混第③和第④题以为a和a1大小不一样。实际上两者类型完全相同都是int(*)[5]所以sizeof结果也一样。步长不同一个是 20 字节一个是 40 字节但指针本身的大小永远是 8 字节。第六部分经典面试题精讲面试题一二维数组的每一行在内存中是连续的吗答案是的C语言二维数组在内存中是按行连续存储的。intm[2][3]{{1,2,3},{4,5,6}};printf(%p\n,m[0][0]);// 第1行第1列printf(%p\n,m[0][1]);// 第1行第2列地址差 4 字节printf(%p\n,m[0][2]);// 第1行第3列地址差 4 字节printf(%p\n,m[1][0]);// 第2行第1列连续地址差 12 字节面试加分回答二维数组int m[2][3]本质上是一个「数组的数组」在内存中展开后是int m[6]6 个 int 连续排列。一行存完接着存下一行没有任何间隙。面试题二指针数组和数组指针的应用场景是什么指针数组典型应用命令行参数intmain(intargc,char*argv[])// argv 是 char*[]每个元素是一个字符串{// argv[0] 程序名// argv[1] 第1个参数// ...return0;}char *argv[]是指针数组每个元素是char*指向一个字符串。数组指针典型应用遍历二维数组intmatrix[3][4]{...};int(*p)[4]matrix;// p 是数组指针指向每一行for(inti0;i3;i){for(intj0;j4;j)printf(%d ,p[i][j]);printf(\n);}面试题三char *p hello;和char arr[] hello;有什么区别char*phello;// 字符串常量存放在.rodata只读数据段chararr[]hello;// 字符数组存放在栈上// p[0] H; // ❌ 危险p 指向只读内存写入会崩溃arr[0]H;// ✅ 可以栈上的数组内容可以改答案前者p指向的是只读数据段的字符串常量p本身是指针变量后者arr是一个栈上的字符数组内容可以修改。⚠️提醒在 C 语言标准中对字符串常量的修改行为是「未定义行为」——有的编译器会直接报错段错误有的编译器可能会悄悄执行取决于编译器实现。所以如果你想修改字符串必须用字符数组不能用指针指向字符串常量。本章总结数组名的两个特殊例外表达式类型sizeof / 含义sizeof(数组名)—整个数组的大小不是指针数组名int(*)[n]整个数组的地址1 跳 n×sizeof(T)指针数组 vs 数组指针写法本质1 跳指针数组int *arr[5]元素是指针的数组n × sizeof(int*)数组指针int (*p)[5]指向数组的指针5 × sizeof(int)二级指针的使用场景需要在函数内部修改指针变量本身而非它指向的内容动态分配二维数组时作为输出参数