C语言指针痛点—>指针数组与数组指针
一、痛点之情景再现
我们学习C语言到了指针进阶阶段时,虽然知道这两个指针是什么名字,但是可能会分不清它们存的是指针(地址)还是元素,或者指向的是什么?
char*p1[4];// 指针数组char(*p2)[4];// 数组指针我们根据上方代码片段可知,指针数组与数组指针这两个代码写法只差一对括号,但它们的含义、内存布局天差地别。我们在写代码时很容易将它们混淆。
二、破局核心:掌握“优先级”这把金钥匙
- C语言运算符优先级:在C语言中,
[](数组下标)的优先级高于*(指针解引用)。
2.记忆口诀:“谁离变量名近,变量就是什么类型”。带括号的,变量先和 * 结合,就是指针;不带括号的,变量先和 [] 结合,就是数组。
三、 指针数组(Array of Pointers)—— 本质是数组
//代码示例:char*p[4];1.拆解分析:
因为 [] 优先级高,所以 p 先和 [4] 结合。结论:p 是一个数组。
然后看数组里装的是什么?是 char *(字符指针)。
2.本质:它是一个数组,里面存放了 4 个指针。
3.内存模型:想象一排连续的格子(数组),每个格子里装的不是字符,而是地址(指向其他字符串或变量的首地址)。它占用的内存是 4 * sizeof(char *)(32位系统是16字节,64位系统是32字节)。
图例:
4.完整可运行示例:下面这段代码演示了指针数组的定义、初始化与访问方式。
#include<stdio.h>intmain(){// 定义并初始化一个指针数组:数组里存放 3 个 char* 指针char*p[3]={"Hello","World","C"};// 访问:p[i] 取出第 i 个指针,再解引用得到字符串for(inti=0;i<3;i++){printf("p[%d] = %s\n",i,p[i]);}// 内存布局:p 是连续 3 个格子,每个格子存一个地址(指向字符串首字符)printf("p 占用的总内存 = %zu 字节\n",sizeof(p));// 3 * sizeof(char*)printf("p[0] 指向的地址 = %p\n",(void*)p[0]);printf("p[1] 指向的地址 = %p\n",(void*)p[1]);return0;}运行输出示例:
p[0] = Hello p[1] = World p[2] = C p 占用的总内存 = 24 字节 p[0] 指向的地址 = 0x55a1c2d3e010 p[1] 指向的地址 = 0x55a1c2d3e016说明:
p本身是数组,p[i]取出的是第i个指针;sizeof(p)得到的是整个数组占用的字节数(3 * sizeof(char *),64 位系统下为 24 字节)。地址的具体数值每次运行可能不同,但相邻指针指向的地址通常相差 6 个字节(对应字符串长度)。
四、 数组指针(Pointer to an Array)—— 本质是指针
//代码示例:char(*p)[4];1.拆解分析:
因为有 () 强制结合,p 先和 * 结合,形成(*p)。结论:p 是一个指针。
然后看指针指向什么?指向 char [4] 这个数组类型。
2.本质:它是一个指针,存放的是一个数组的地址(即整个数组的起始地址)。
3.内存模型:它不是一排格子,它只有一个格子(一个变量),里面存了一个地址,这个地址指向一个长度为4的字符数组。对它进行 p + 1,它不是移动1个字节,而是跨越整整4个字节(跳过一个完整的数组)。
图例:
4.完整可运行示例:下面这段代码演示了数组指针的定义、初始化与访问方式。
#include<stdio.h>intmain(){// 定义一个长度为 4 的整型数组intarr[4]={10,20,30,40};// 定义数组指针:p 指向整个 int[4] 数组int(*p)[4]=&arr;// 访问:(*p)[i] 先解引用得到数组,再按下标取元素for(inti=0;i<4;i++){printf("(*p)[%d] = %d\n",i,(*p)[i]);}// 指针运算:p + 1 会跨越整个数组(4 个 int),而不是 1 个 intprintf("p 指向的地址 = %p\n",(void*)p);printf("p + 1 指向的地址 = %p\n",(void*)(p+1));printf("地址差 = %ld 字节(即跨越 4 个 int)\n",(char*)(p+1)-(char*)p);return0;}运行输出示例:
(*p)[0] = 10 (*p)[1] = 20 (*p)[2] = 30 (*p)[3] = 40 p 指向的地址 = 0x7ffd4a2b3c00 p + 1 指向的地址 = 0x7ffd4a2b3c10 地址差 = 16 字节(即跨越 4 个 int)说明:
p本身是一个指针,存放的是整个数组的起始地址;(*p)[i]等价于arr[i]。对p做p + 1,指针会跨越整个int[4]数组(64 位系统下int为 4 字节,共跨越 16 字节),这正是数组指针与普通指针最大的区别。
五、常见错误与排查
初学者在指针数组与数组指针上最容易踩坑,下面列举 3 个典型错误,每个都给出错误代码、运行结果、原因分析和正确写法。
错误 1:混淆声明写法
错误代码:
#include<stdio.h>intmain(){char*p[4];// 想定义数组指针,却写成了指针数组charstr[4]="abc";p=&str;// 编译报错:类型不匹配return0;}运行结果:
error: assignment to expression with array type原因分析:char *p[4]中[]优先级高于*,p先和[4]结合,本质是数组,不能作为指针被赋值。想定义数组指针必须加括号char (*p)[4]。
正确写法:
#include<stdio.h>intmain(){char(*p)[4];// 数组指针:p 指向整个 char[4] 数组charstr[4]="abc";p=&str;// 正确:p 存放数组的起始地址printf("%s\n",(*p));// 输出 abcreturn0;}错误 2:错误解引用
错误代码:
#include<stdio.h>intmain(){intarr[4]={10,20,30,40};int(*p)[4]=&arr;printf("%d\n",*p);// 错误:*p 是数组,不是 intprintf("%d\n",p[0]);// 错误:p[0] 是数组,不是 intreturn0;}运行结果:
warning: format ‘%d’ expects argument of type ‘int’, but argument has type ‘int *’原因分析:数组指针解引用一次得到的是整个数组,而不是单个元素。要访问元素必须再按下标取,即(*p)[i]或p[0][i]。
正确写法:
#include<stdio.h>intmain(){intarr[4]={10,20,30,40};int(*p)[4]=&arr;for(inti=0;i<4;i++){printf("%d ",(*p)[i]);// 正确:先解引用得到数组,再按下标取元素}printf("\n");return0;}错误 3:指针运算越界
错误代码:
#include<stdio.h>intmain(){intarr[4]={10,20,30,40};int(*p)[4]=&arr;// 错误:p + 1 已经越过整个数组,再解引用访问越界printf("%d\n",(*(p+1))[0]);return0;}运行结果:
(*(p + 1))[0] = 0 // 未定义行为,可能输出垃圾值或崩溃原因分析:数组指针做p + 1会跨越整个int[4]数组(16 字节),此时p + 1已指向数组末尾之后的内存,解引用属于越界访问,结果是未定义行为。
正确写法:
#include<stdio.h>intmain(){intarr[4]={10,20,30,40};int(*p)[4]=&arr;// 只在数组范围内访问for(inti=0;i<4;i++){printf("%d ",(*p)[i]);}printf("\n");// 若确实需要遍历多个数组,应使用数组指针数组intarr2[4]={50,60,70,80};int(*pa[2])[4]={&arr,&arr2};// 指针数组,每个元素指向一个 int[4]for(intj=0;j<2;j++){for(inti=0;i<4;i++){printf("%d ",(*pa[j])[i]);}printf("\n");}return0;}小结:写代码时先按“谁离变量名近”判断类型,再决定如何解引用;做指针运算前务必确认指针是否仍在数组边界内。
六、总结
| 名称 | 定义写法 | 本质 | 占用的内存 | 谁先结合 |
|---|---|---|---|---|
| 指针数组 | char *p[4] | 数组 | 4个指针的大小 | p 先和 [4] 结合 |
| 数组指针 | char (*p)[4] | 指针 | 1个指针的大小 | p 先和 * 结合 |
指针数组本质上是数组,数组指针本质上是指针,做这类题画图还是很有必要的。