指针(3)
目录
代码 2:void (*signal(int, void(*)(int)))(int);
✨ 引言:
指针是 C 语言的 “灵魂进阶篇”🤯—— 字符指针、数组指针、函数指针、回调函数…… 这些概念层层递进,新手容易绕晕,但一旦吃透,代码效率和灵活性会翻倍!
1. 📝 字符指针:常量字符串的 “只读陷阱” 与存储秘密
字符指针不仅能指向单个字符,还能指向字符串 —— 但这里藏着新手最容易踩的 “只读坑”,还有存储的小秘密!
1.1 字符指针 vs 字符数组:可变 vs 只读
字符数组是 “可修改的草稿纸”,常量字符串是 “贴了只读标签的打印件”:
int main()
{
char ch = 'w';
char* pc = &ch;//指向单个字符的指针
char arr[10] = "abcdef";//字符数组(栈区,内容可修改)
//内存布局:a b c d e f \0 0 0 0(像可擦写的草稿纸)
char* p1 = arr;
*p1 = 'w';//合法:修改数组内容(擦改草稿纸)
const char* p2 = "abcdef";//指向常量字符串(代码段,只读)
//*p2 = 'w'//err:修改只读内存会崩溃!(试图改打印件)
printf("%s\n", p1);//wbcdef
printf("%s\n", p2);//abcdef
return 0;
}
📌 核心区别(必记!):
| 类型 | 存储区域 | 内容是否可修改 |
|---|---|---|
| 字符数组 | 栈区 | 是 |
| 常量字符串 | 代码段 | 否(const是提醒,不加也报错) |
1.2 为什么str1≠str2但str3=str4?
就像 “两个独立小区 vs 共享一个公共设施”:
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2) printf("str1和str2相同\n");//❌ 输出不同
else printf("str1和str2不同\n");//✅
if (str3 == str4) printf("str3和str4相同\n");//✅
else printf("str3和str4不同\n");//❌
return 0;
}
🤔 通俗解释:
str1和str2是栈区的两个独立数组(两个小区),各占一块内存,首地址自然不同;str3和str4指向代码段的同一个常量字符串(共享一个公园)—— 系统为了节省空间,相同的常量字符串只存储一份,所以地址相同。
💡 小贴士:%s打印字符串时,只需传入起始地址,会自动打印到\0结束,不用手动循环!
2. 📚 数组指针:指向数组的 “专属导航仪”
数组指针不是 “指针数组”!它是专门指向整个数组的指针—— 就像导航仪专门定位 “整个小区”,而不是单个 “住户”。
2.1 数组指针 vs 指针数组:别再搞混双胞胎!
int main()
{
int arr[10] = { 0 };
int(*p)[10] = &arr;//p是数组指针:指向包含10个int的数组
//int* p1[10];//p1是指针数组:数组里存的是10个int*指针
return 0;
}
⚠️ 优先级陷阱(关键!):
int(*p)[10]:()优先级高,p先和*结合,是指针,指向 “10 个 int 的数组”;int* p1[10]:[]优先级高,p1先和[]结合,是数组,存的是 “int * 指针”。
2.2 数组指针的 “步长”:一跳就是整个数组
普通指针跳 “一个元素”,数组指针跳 “整个数组”:
int main()
{
int arr[10] = { 0 };
int* p1 = arr;//int*:指向首元素(住户导航)
int(*p2)[10] = &arr;//int(*)[10]:指向整个数组(小区导航)
printf("%p\n", p1);//000000983ED3FA28
printf("%p\n", p1 + 1);//000000983ED3FA2C(+4字节:1个int,到下一户)
printf("%p\n", p2);//000000983ED3FA28
printf("%p\n", p2 + 1);//000000983ED3FA50(+40字节:10个int,到下一个小区)
return 0;
}
📌 结论:数组指针的 “步长”= 指向数组的总大小(sizeof(数组))。
3. 🧩 二维数组传参:数组指针的实战舞台
二维数组传参的本质是 “传递行地址”—— 而行地址就是数组指针!
3.1 二维数组的地址逻辑:行地址≠元素地址
二维数组可以看作 “数组的数组”,首元素是 “第一行的一维数组”:
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
// arr:二维数组名 → 第一行的地址(int(*)[5],数组指针)
// arr+i:第i行的地址(int(*)[5])
// *(arr+i) = arr[i] → 第i行的数组名 → 第i行首元素地址(int*)
// *(arr+i)+j → 第i行第j列元素地址(int*)
// *(*(arr+i)+j) = arr[i][j] → 第i行第j列元素(int)
🤔 比喻:二维数组是 “3 栋楼,每栋 5 户”:
arr是 1 号楼的地址(数组指针);arr+1是 2 号楼的地址;*(arr+1)是 2 号楼 1 单元的地址(普通指针);*(arr+1)+2是 2 号楼 3 单元的地址;*(*(arr+1)+2)是 2 号楼 3 单元的住户(元素值)。
3.2 传参的两种写法:数组形式 vs 指针形式
二维数组传参时,列数不能省略(因为要知道 “每栋楼有几户”):
// 写法1:数组形式(直观,本质是数组指针)
// void print(int arr[3][5], int r, int c)
// {
// // ...
// }
// 写法2:数组指针形式(更贴近本质)
void print(int(*arr)[5], int r, int c)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", *(*(arr + i) + j));//等价于arr[i][j]
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
print(arr, 3, 5);
// 输出:
// 1 2 3 4 5
// 2 3 4 5 6
// 3 4 5 6 7
return 0;
}
📌 关键:二维数组名arr的类型是int(*)[5](数组指针),所以形参必须用相同类型接收!
4. 🔧 函数指针:存储函数地址的 “特殊名片”
函数也有地址!函数指针就是存放函数地址的变量 —— 就像用名片存着 “公司地址”,拿着名片就能找到公司(调用函数)。
4.1 函数地址的获取:函数名 =(& 函数名)
和数组名不同,函数名和&函数名完全等价,都表示函数的入口地址:
int Add(int x, int y)
{
return x + y;
}
int main()
{
printf("%p\n", &Add);//00DF10B9(函数地址)
printf("%p\n", Add);//00DF10B9(和&Add完全相同)
// 函数指针定义:返回值类型 (*变量名)(参数类型1, 参数类型2)
int(*pf)(int,int) = &Add;//pf是函数指针变量
return 0;
}
📌 函数指针类型格式:返回值类型 (*变量名)(参数类型列表)。
4.2 函数指针的调用:*可省可加
调用函数指针时,*只是 “仪式感”,加不加都能调用:
int main()
{
int(*pf)(int, int) = Add;
int c = Add(2, 3);//直接调用(常用)
int d = (*pf)(3, 4);//解引用调用(*可省)
int e = pf(4, 5);//函数指针直接调用(更简洁)
printf("%d\n", c);//5
printf("%d\n", d);//7
printf("%d\n", e);//9
return 0;
}
💡 小贴士:函数指针的核心是 “通过地址调用函数”,*的作用是 “解引用地址”,但编译器会自动识别,所以可省略。
4.3 两段 “天书代码” 的拆解
代码 1:(*(void(*)())0)();
int main()
{
(*(void(*)())0)();
// 拆解(从内到外):
// 1. void(*)() → 无参数、返回值为void的函数指针类型(“函数类型模板”)
// 2. (void(*)())0 → 把0强制转换成该函数指针类型(假设0地址有个这样的函数)
// 3. *(void(*)())0 → 解引用0地址的函数指针(找到这个函数)
// 4. (*...)() → 调用这个函数(无参数)
return 0;
}
代码 2:void (*signal(int, void(*)(int)))(int);
这是标准库signal函数的声明,返回值是 “函数指针”,用typedef简化后更清晰:
// typedef简化函数指针类型
typedef void(*pf_t)(int);//pf_t = void(*)(int)(给函数指针类型起别名)
pf_t signal(int, pf_t);//简化后:返回pf_t,参数是int+pf_t
4.4 typedefvs#define:类型重命名的坑
typedef是 “真正的类型重命名”,#define只是 “文本替换”,别踩坑!
typedef int* ptr_t;
#define PTR_T int*
ptr_t p1, p2;//p1、p2都是int*(typedef是类型重命名,统一类型)
PTR_T p3, p4;//p3是int*,p4是int(#define文本替换:int* p3,p4)
📌 区别:typedef会把整个别名当作一个类型,#define只做简单替换,*只会绑定第一个变量。
5. 🗂️ 函数指针数组:打造函数的 “工具箱”
函数指针数组是 “存放多个函数指针的数组”—— 把功能相似的函数(如加减乘除)的地址 “打包”,像工具箱一样按需取用!
int Add(int x, int y) { return x+y; }
int Sub(int x, int y) { return x-y; }
int Mul(int x, int y) { return x*y; }
int Div(int x, int y) { return x/y; }
int main()
{
// 函数指针数组定义:int(*数组名[元素个数])(参数类型)
int(*pfarr[4])(int, int) = { Add, Sub, Mul, Div };
int i = 0;
for (i = 0; i < 4; i++)
{
int r = pfarr[i](8, 4);//通过下标调用对应函数(工具箱取工具)
printf("%d\n", r);//12 4 32 2
}
return 0;
}
✨ 优势:批量管理函数,避免重复的switch/if判断,代码更简洁!
6. ⚙️ 转移表:函数指针数组的经典实战(计算器案例)
计算器的加减乘除功能,用函数指针数组(转移表)实现,代码更简洁、扩展性更强!
传统 switch 实现(冗余)
// 原代码:重复的输入、计算逻辑
switch (input)
{
case 1:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
z = Add(x, y);
printf("%d\n", z);
break;
case 2:
// 重复输入、计算逻辑(Sub)
break;
// ...(case3/4重复类似代码)
}
转移表实现(简洁高效)
void menu()
{
printf("*********************************\n");
printf("******* 1.add 2.sub *********\n");
printf("******* 3.mul 4.div *********\n");
printf("******* 0.exit *********\n");
printf("*********************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int z = 0;
// 函数指针数组(转移表):下标对应功能选项
int(*pfArr[5])(int,int) = { 0 , Add , Sub , Mul , Div};
// 0 1 2 3 4
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if(input >= 1 && input <= 4)
{
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
z = pfArr[input](x,y);//通过下标调用对应函数
printf("%d\n", z);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
📌 优势:新增功能(如取模)只需添加函数和更新数组,不用修改switch结构,扩展性拉满!
7. 🔄 回调函数:指针驱动的 “灵活响应机制”
回调函数是 “通过函数指针调用的函数”—— 把函数作为参数传递给另一个函数,在需要时触发调用,像 “按需响应的开关”!
定义与示例
// 回调函数:具体的功能实现(Add/Sub等)
int Add(int x, int y)
{
return x + y;
}
// 主调函数:接收函数指针参数,统一处理逻辑
void calc(int(*pf)(int,int))
{
int x = 0;
int y = 0;
int z = 0;
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
z = pf(x, y);//调用回调函数(触发功能)
printf("%d\n", z);
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
calc(Add);//把Add作为参数传给calc,calc中回调Add
break;
case 2:
calc(Sub);//回调Sub
break;
// ...(case3/4类似)
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
💡 核心特点:
- 回调函数不由自己直接调用,而是由主调函数(如
calc)在特定条件下触发; - 主调函数统一处理公共逻辑(如输入输出),回调函数处理具体功能,实现 “公共逻辑复用 + 功能灵活切换”。
实际应用:
qsort(快速排序函数)就是典型的回调函数应用 ——qsort负责排序框架,用户通过回调函数定义排序规则(升序 / 降序、结构体字段排序等)。
🎉 总结:指针进阶的核心逻辑
- 字符指针指向常量字符串时,牢记 “只读属性”,避免修改崩溃;
- 数组指针指向整个数组,步长 = 数组总大小,二维数组传参本质是传 “行地址”;
- 函数指针是函数的 “地址名片”,
typedef能简化复杂声明; - 函数指针数组(转移表)是批量管理函数的利器,提升代码扩展性;
- 回调函数通过函数指针实现 “灵活响应”,是模块化编程的关键。
指针进阶的本质是 “用地址管理一切”—— 字符、数组、函数都能通过地址灵活操作,掌握这些知识点,你就能写出更高效、更灵活的 C 语言代码!如果这篇博客帮你理清了思路,欢迎点赞收藏🌟~
C语言指针进阶核心解析
239

被折叠的 条评论
为什么被折叠?



