深入理解指针(4) — 内容总结
1. 字符指针变量(char*)
1.1 指向单个字符
intmain(){charch='w';char*pc=&ch;*pc='w';return0;}1.2 指向字符串字面量
#include<stdio.h>intmain(){//加cosnt是为了不让该字符串里的内容,若是修改代码会崩溃constchar*pstr="hello bit.";// pstr 存的是字符串首字符 'h' 的地址printf("%c\n",*pstr);// 输出首字符 'h'printf("%s\n",pstr);// 输出 hello bit.return0;}注意:字符串字面量存储在只读数据区,所以
pstr最好用const char*修饰,防止通过pstr修改时程序崩溃。
1.3 经典题:字符数组 vs 字符指针
#include<stdio.h>intmain(){charstr1[]="hello bit.";charstr2[]="hello bit.";constchar*str3="hello bit.";constchar*str4="hello bit.";if(str1==str2)// str1和str2的地址不一样{printf("str1 and str2 are same\n");}else{printf("str1 and str2 are not same\n");// ✅ 输出这行}if(str3==str4){printf("str3 and str4 are same\n");// ✅ 输出这行}else{printf("str3 and str4 are not same\n");}return0;}结果:
str1 and str2 are not same
str3 and str4 are same
原因:
str1 和 str2 是两个独立的数组,分别占据内存中不同的位置。
str1 == str2 比较的是两个数组的起始地址(也就是数组名被隐式转换为指针)。
虽然它们的内容完全相同,但地址不同,所以比较结果为假(不相等)。
“hello bit.” 是一个字符串常量,存储在常量区(只读数据段)。
编译器为了节省内存,会将内容相同的字符串常量优化为只存一份。
str3 和 str4 都指向同一块内存地址。
str3 == str4 比较的是两个指针存储的地址值,它们相同,所以比较结果为
真(相等)。
结论:
| 变量 | 存储位置 | 比较结果 |
|---|---|---|
str1、str2 | 各自在栈上开辟独立的字符数组 | 不相同(地址不同) |
str3、str4 | 都指向只读数据区的同一字符串字面量 | 相同(编译器优化,指向同一地址) |
2. 数组指针变量
2.1 数组指针 vs 指针数组(分辨技巧)
int*p1[10];// 指针数组:p1 先与 [] 结合 → 10 个 int* 的数组//指向首元素int(*p2)[10];// 数组指针:p2 先与 * 结合 → 指向含 10 个 int 的数组的指针(指向数组的指针变量 )p1是数组,类型是指针,所有是指针数组
p2是指针,指向数组,所有是数组指针
口诀:[]优先级高于*。不加括号 → 数组;加括号 → 指针。
2.2 数组指针的初始化
intarr[10]={0};int(*p)[10]=&arr;// p 指向整个数组(int (*)[10] 类型)关键区别:arr表示数组首元素的地址(类型为int*),而&arr表示整个数组的地址(类型为int (*)[10])。虽然两者的数值可能相同,但指针类型和步长不同。
图解:
int (*p)[10] = &arr; │ │ │ └── p 指向一个包含 10 个 int 的数组 └────── p 是一个指针关键:&arr的类型是int (*)[10](数组指针),与普通指针int*不同。
3. 二维数组传参的本质
二维数组作为函数参数时,会退化为指向第一行(一维数组)的指针。
3.1 传统写法(数组形式)
#include<stdio.h>voidtest(inta[3][5],intr,intc){inti=0;intj=0;for(i=0;i<r;i++){for(j=0;j<c;j++){printf("%d ",a[i][j]);}printf("\n");}}intmain(){intarr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};test(arr,3,5);return0;}3.2 本质写法(数组指针形式)
#include<stdio.h>// int a[3][5] 等价于 int (*p)[5]voidtest(int(*p)[5],intr,intc)//int (*p)[5]是指向第一个数组的地址{inti=0;intj=0;for(i=0;i<r;i++){for(j=0;j<c;j++){printf("%d ",*(*(p+i)+j));// 等价于 p[i][j]}printf("\n");}}intmain(){intarr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};test(arr,3,5);//传的是一维数组的指针return0;}核心理解:
| 表达式 | 含义 |
|---|---|
p | 指向第 0 行(一个int[5]) |
p + i | 指向第 i 行,跳过一个int[5](即 20 字节) |
*(p + i) | 得到第 i 行的数组名(int[5])也就是该数组首元素的地址 |
*(p + i) + j | 第 i 行中第 j 个元素的地址 |
*(*(p + i) + j) | 第 i 行第 j 个元素的值 |
结论:
int a[3][5]作为参数时退化为int (*)[5](指向含 5 个 int 的数组的指针),而非二级指针int**。
4. 函数指针变量
4.1 函数地址
函数名和&函数名都是函数的地址:
#include<stdio.h>voidtest(){printf("hehe\n");}intmain(){printf("test: %p\n",test);// 输出 test: 005913CAprintf("&test: %p\n",&test);// 输出 &test: 005913CAreturn0;}结论:
test和&test数值相同,都是函数的入口地址。
4.2 函数指针的定义与使用
voidtest(){printf("hehe\n");}void(*pf1)()=&test;// ✅ 写法一void(*pf2)()=test;// ✅ 写法二(等价)intAdd(intx,inty){returnx+y;}int(*pf3)(int,int)=Add;// 或者:int(*pf3)(intx,inty)=&Add;// x、y 可写可不写&Add和Add都是函数地址
函数指针拆解:
int (*pf3) (int x, int y) ↑ ↑ ↑ | | └── 参数列表 | └──────────── pf3 是指针 └───────────────── 返回值类型为 int调用方式(两种等价):
#include<stdio.h>intAdd(intx,inty){returnx+y;}intmain(){int(*pf3)(int,int)=Add;//(*pf3)==Add==&Add==pf3printf("%d\n",(*pf3)(2,3));// 输出 5(注意一定要把*pf3括起来)printf("%d\n",pf3(3,5));// 输出 8(等价写法)return0;}4.3 两个经典考验
例1:
(*(void(*)())0)();拆解步骤:
| 步骤 | 解释 |
|---|---|
0 | 整数 0 |
(void (*)())0 | 将 0 强制转换为函数指针类型(无参,返回 void) |
*(void (*)())0 | 解引用,得到该函数 |
(*(void (*)())0)() | 调用地址为 0 的那个函数 |
本质:调用一个地址为 0 的函数(危险操作,会导致段错误)。
例2:
void(*signal(int,void(*)(int)))(int);拆解步骤:
| 步骤 | 解释 |
|---|---|
signal | 函数名 |
signal(int, void(*)(int)) | signal 函数有两个参数:一个int,一个函数指针(参数为 int、返回 void) |
void (*...)(int) | signal 的返回类型是函数指针(参数为 int、返回 void) |
4.4 用typedef简化函数指针
typedefunsignedintuint;// 给类型起别名//unsigned int == unittypedefint*pint;// 将 int* 起别名为 pint//int*=pinttypedefint(*parr_t)[5];// 数组指针类型起别名//int(*)[5] == parr_ttypedefvoid(*pfun_t)(int);// 函数指针类型起别名//void (*)(int) == pfun_t//pfun_t p2 = test;//可定义为 pfun_t(类型) p2(函数指针变量的名字) = test(函数)// 简化例2:typedefvoid(*pfun_t)(int);pfun_tsignal(int,pfun_t);// 简洁明了!和#define的区别:
// 使用 typedeftypedefchar*STRING;STRING p1,p2;// p1 和 p2 都是 char* 类型(两个都是指针)✅// 使用 #define#defineSTRINGchar*STRING p1,p2;// 展开后:char* p1, p2; 只有 p1 是指针,p2 是 char!❌5. 函数指针数组
int*arr[10];// 整型指针数组int(*parr1[3])();// 函数指针数组:3 个函数指针,返回 int、无参int*parr2[3]();// ❌ 错误:函数不能返回数组int(*)()parr3[3];// ❌ 语法错误int (*parr1[3])();拆解:
int (*parr1[3])(); ↑ └── parr1 先与 [3] 结合 → 数组 再与 * 结合 → 数组中存放的是指针 再与 () 结合 → 指针指向函数(返回 int、无参)结论:
parr1是一个包含 3 个元素的数组,每个元素是int (*)()类型的函数指针。
6. 转移表 — 函数指针数组的应用
函数指针数组可用于替换冗长的switch-case,实现转移表。
6.1 传统写法(switch-case 冗长)
#include<stdio.h>intadd(inta,intb){returna+b;}intsub(inta,intb){returna-b;}intmul(inta,intb){returna*b;}intdiv(inta,intb){returna/b;}intmain(){intx,y;intinput=1;intret=0;do{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");printf("请选择:");scanf("%d",&input);switch(input){case1:printf("请输入两个操作数:");scanf("%d %d",&x,&y);ret=add(x,y);printf("ret = %d\n",ret);break;case2:printf("请输入两个操作数:");scanf("%d %d",&x,&y);ret=sub(x,y);printf("ret = %d\n",ret);break;case3:printf("请输入两个操作数:");scanf("%d %d",&x,&y);ret=mul(x,y);printf("ret = %d\n",ret);break;case4:printf("请输入两个操作数:");scanf("%d %d",&x,&y);ret=div(x,y);printf("ret = %d\n",ret);break;case0:printf("退出程序\n");break;default:printf("输入错误\n");break;}}while(input);return0;}6.2 转移表写法(函数指针数组,简洁优雅)
#include<stdio.h>intadd(inta,intb){returna+b;}intsub(inta,intb){returna-b;}intmul(inta,intb){returna*b;}intdiv(inta,intb){returna/b;}intmain(){intx,y;intinput=1;intret=0;// 转移表:函数指针数组,下标直接对应菜单编号int(*p[5])(intx,inty)={0,add,sub,mul,div};//创建函数指针数组,把函数指针存到数组里do{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");printf("请选择:");scanf("%d",&input);if(input<=4&&input>=1){printf("请输入两个操作数:");scanf("%d %d",&x,&y);ret=(*p[input])(x,y);// 通过函数指针调用printf("ret = %d\n",ret);}elseif(input==0){printf("退出程序\n");}else{printf("输入错误\n");}}while(input);return0;}对比总结:
| 方式 | 代码量 | 可扩展性 |
|---|---|---|
| switch-case | ~60 行,每加一个运算多 6 行 | 繁琐 |
| 转移表 | ~40 行,只需在数组中加一个函数指针 | 简洁优雅 |
核心思路:用函数指针数组的下标替代
switch分支,通过(*p[input])(x, y)一行调用对应的函数。
总结
本讲是函数指针的深入内容,重点掌握:
- 字符指针
char*:区分数组与字面量,str1 != str2但str3 == str4 - 数组指针
int (*p)[10]:[]优先级高于*,加括号才是指针 - 二维数组传参退化:
int a[3][5]→int (*p)[5],不是二级指针 - 函数指针:
int (*pf)(int, int),函数名就是地址,typedef可简化复杂声明 - 函数指针数组:
int (*arr[3])(),存放多个函数指针 - 转移表:用函数指针数组替代
switch-case,代码更简洁