一、函数指针
函数指针是 C 语言中一种特殊的指针类型,它指向函数的入口地址(而非数据),可以像普通指针一样赋值、传递、作为函数参数 / 返回值,是实现回调、动态函数调度的核心机制。
一、核心概念
1. 函数的内存特性
C 语言中,函数编译后会被加载到内存的代码段 ,每个函数都有一个唯一的入口地址(函数名本质上是该地址的常量别名)。函数指针的作用就是存储这个入口地址,通过指针可以间接调用函数。
2. 函数指针与普通指针的区别
| 类型 | 指向区域 | 核心用途 | 支持的操作 |
|---|---|---|---|
| 普通指针 | 数据段 / 堆 / 栈 | 访问变量(数据) | 解引用*、算术运算± |
| 函数指针 | 代码段 | 调用函数(执行代码) | 解引用*、函数调用() |
二、函数指针的声明与初始化
1. 声明语法(核心)
函数指针的声明必须匹配目标函数的返回值类型 和参数列表,语法格式:
返回值类型 (*指针变量名)(参数类型1, 参数类型2, ...);
- 关键括号:
(*指针变量名)的括号不可省略(否则会被解析为 "返回指针的函数")。 - 参数列表:只需写参数类型,参数名可省略(增强可读性时也可写)。
反例(易踩坑)
cs
// 错误:这是"返回int*类型的函数func",而非函数指针
int *func(int a);
2. 初始化(赋值)
函数名本身就是函数的入口地址,因此有两种等价的赋值方式:
cs
// 步骤1:定义一个普通函数
int add(int a, int b) {
return a + b;
}
// 步骤2:声明函数指针
int (*fp)(int, int);
// 步骤3:初始化(两种方式等价)
fp = add; // 推荐:函数名直接作为地址
// fp = &add; // 等价写法:&+函数名也表示函数地址
三、函数指针的调用
通过函数指针调用函数有两种等价方式,效果完全一致:
cs
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
int (*fp)(int, int) = add;
// 方式1:解引用后调用(直观体现"指针"特性)
int res1 = (*fp)(3, 5);
// 方式2:直接调用(简化写法,编译器自动解析)
int res2 = fp(3, 5);
printf("res1=%d, res2=%d\n", res1, res2); // 输出:res1=8, res2=8
return 0;
}
说明:方式 2 更常用,因为函数指针本质是 "函数入口地址",直接调用符合直觉。
四、函数指针的核心应用场景
场景 1:回调函数(最常用)
回调函数是指通过函数指针传递给另一个函数,并在该函数内部调用的函数。典型场景:排序、遍历、事件处理。
示例:自定义规则的数组排序
C 标准库qsort就是基于函数指针实现的通用排序,我们模拟实现简化版:
cs
#include <stdio.h>
// 1. 通用排序函数(接收函数指针作为比较规则)
void my_sort(int arr[], int len, int (*cmp)(int, int)) {
for (int i = 0; i < len-1; i++) {
for (int j = 0; j < len-1-i; j++) {
// 通过函数指针调用比较规则
if (cmp(arr[j], arr[j+1]) > 0) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
// 2. 比较规则1:升序
int cmp_asc(int a, int b) {
return a - b; // a>b返回正数,触发交换
}
// 3. 比较规则2:降序
int cmp_desc(int a, int b) {
return b - a; // b>a返回正数,触发交换
}
int main() {
int arr[] = {3, 1, 4, 2};
int len = sizeof(arr)/sizeof(int);
// 传递升序比较函数
my_sort(arr, len, cmp_asc);
for (int i=0; i<len; i++) printf("%d ", arr[i]); // 1 2 3 4
printf("\n");
// 传递降序比较函数
my_sort(arr, len, cmp_desc);
for (int i=0; i<len; i++) printf("%d ", arr[i]); // 4 3 2 1
return 0;
}
场景 2:函数指针数组(批量管理函数)
当需要管理一组同类型的函数时,可将函数指针存入数组,实现 "索引式" 调用(如计算器、菜单驱动程序)。
示例:简易计算器
cs
#include <stdio.h>
// 四则运算函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return b==0 ? 0 : a/b; }
int main() {
// 声明并初始化函数指针数组(所有函数类型一致)
int (*calc_funcs[])(int, int) = {add, sub, mul, div};
int op; // 操作符选择:0=加,1=减,2=乘,3=除
int a = 10, b = 5;
printf("请选择操作(0-加,1-减,2-乘,3-除):");
scanf("%d", &op);
if (op >=0 && op <4) {
// 通过数组索引调用函数
int res = calc_funcs[op](a, b);
printf("结果:%d\n", res);
} else {
printf("无效操作\n");
}
return 0;
}
场景 3:typedef 简化函数指针声明
函数指针语法冗长,可通过typedef定义别名,简化代码(尤其适合复杂场景)。
示例:typedef 定义函数指针别名
cs
#include <stdio.h>
// 步骤1:定义函数指针别名(CalcFunc是"int(*)(int,int)"的别名)
typedef int (*CalcFunc)(int, int);
// 普通函数
int add(int a, int b) { return a + b; }
// 接收CalcFunc类型参数的函数
int calc(CalcFunc fp, int a, int b) {
return fp(a, b);
}
int main() {
// 用别名声明函数指针(更简洁)
CalcFunc fp = add;
printf("3+5=%d\n", calc(fp, 3, 5)); // 输出:8
return 0;
}
五、进阶:指向函数指针的指针(二级函数指针)
与普通二级指针类似,二级函数指针指向 "函数指针变量" 的地址,语法:
cs
返回值类型 (*(*二级指针名))(参数类型列表);
示例:二级函数指针
cs
#include <stdio.h>
int add(int a, int b) { return a + b; }
int main() {
// 一级函数指针
int (*fp)(int, int) = add;
// 二级函数指针(指向fp的地址)
int (*(*ppf))(int, int) = &fp;
// 调用方式(三种等价)
printf("%d\n", (*fp)(3,5)); // 8
printf("%d\n", (*ppf)(3,5)); // 8
printf("%d\n", (**ppf)(3,5)); // 8
return 0;
}
用途:主要用于动态修改函数指针(如在函数中修改外部函数指针的指向)。
六、注意事项
1. 类型严格匹配
函数指针的返回值类型 、参数类型 / 个数 / 顺序必须与目标函数完全一致,否则会导致未定义行为(编译警告 / 运行崩溃)。
cs
// 错误示例:参数个数不匹配
int func1(int a) { return a; }
int (*fp)(int, int) = func1; // 编译警告,运行风险
2. 函数指针不支持算术运算
普通指针(如int*)可通过±移动地址,但函数指针不允许(代码段函数地址无连续意义):
cs
int (*fp)(int, int) = add;
fp++; // 编译错误:函数指针不支持++/--
3. void* 与函数指针的转换(慎用)
C 标准规定:void*仅用于存储数据指针,不能直接转换为函数指针 (部分编译器(如 GCC)支持扩展,但不可移植)。如需转换,建议用中间类型(如uintptr_t):
cs
#include <stdint.h>
int add(int a, int b) { return a + b; }
int main() {
void *p = (void*)add; // 编译器扩展,非标准
// 标准兼容写法
int (*fp)(int, int) = (int(*)(int,int))(uintptr_t)p;
printf("%d\n", fp(3,5)); // 8
return 0;
}
4. 空指针检查
使用函数指针前需检查是否为NULL,避免空指针调用(程序崩溃):
cs
int (*fp)(int, int) = NULL;
if (fp != NULL) {
fp(3,5); // 安全调用
}
七、总结
函数指针是 C 语言的高级特性,核心价值在于:
- 实现回调函数 (如
qsort、事件驱动); - 动态切换函数逻辑(如插件化、策略模式);
- 批量管理同类型函数(函数指针数组)。
掌握函数指针的关键是:匹配函数类型 、理解语法优先级 、结合实际场景使用(避免过度设计)。
二、两个函数含义解析
1.(*(void (*) )0 )();
以上代码是一次函数调用,调用的是0作为地址处的函数
1.把0强制类型转换为:无参,返回类型是void的函数的地址
2.调用0地址处的这个函数
第 1 层:void (*)() ------ 函数指针类型(无名称)
这是函数指针的 "类型标识符"(省略了指针变量名),表示:
- 指向的函数返回值为 void;
- 指向的函数无参数(括号内为空)。
👉 核心:这是一个 "类型",而非变量 / 表达式,对应 "指向无参、返回 void 的函数的指针类型"。
第 2 层:(void (*)())0 ------ 把 0 强制转换为函数指针
这一步是强制类型转换:
- 操作数:整数
0(内存地址 0x00000000); - 目标类型:第 1 层的
void (*)()(函数指针类型); - 括号:
(void (*)())是完整的类型转换符(必须加括号,否则语法错误)。
👉 语义:将 "内存地址 0" 解释为 "一个指向无参、返回 void 的函数的指针"。(注:普通程序中地址 0 是无效的 "空指针区域",但嵌入式 / 内核中地址 0 可能是复位向量 / 入口函数)。
第 3 层:*(void (*)())0 ------ 解引用函数指针
* 是解引用运算符,作用于函数指针:
- 函数指针的本质是 "存储函数入口地址的指针",解引用
*指针等价于 "获取指针指向的函数本身"; - C 语言语法允许省略解引用(即
fp()和(*fp)()效果完全一致),这里显式写*只是为了语义清晰。
👉 语义:获取地址 0 处的那个 "无参、返回 void 的函数" 本身。
第 4 层:(*(void (*)())0)() ------ 调用函数
最后的 () 是函数调用运算符,作用于 "函数名 / 解引用后的函数指针":
- 操作数:第 3 层解引用得到的 "地址 0 处的函数";
- 括号:空括号表示调用时不传参数(匹配函数的无参签名)。
👉 最终语义:调用内存地址 0 处的、无参数、返回值为 void 的函数。
补充说明(关键细节)
-
括号的必要性
void (*)()中的括号:如果写成void *(),会被解析为 "返回 void * 的函数声明",而非函数指针类型;(void (*)())0中的外层括号:强制类型转换符必须用括号包裹类型,否则void (*)()0语法错误;(*(void (*)())0)中的外层括号:解引用运算符*的优先级低于函数调用(),必须用括号把解引用表达式括起来,否则*(void (*)())0()会被错误解析为 "先调用 0 (),再转换类型"(语法完全错误)。
总结
| 层级 | 代码片段 | 核心含义 |
|---|---|---|
| 1 | void (*)() |
无参、返回 void 的函数指针类型 |
| 2 | (void (*)())0 |
把地址 0 转换为上述函数指针 |
| 3 | *(void (*)())0 |
解引用指针,获取地址 0 处的函数 |
| 4 | *(...)() |
调用地址 0 处的这个无参、返回 void 的函数 |
这段代码的核心目的是强制跳转到内存地址 0 处执行函数,是嵌入式 / 内核开发中常见的 "裸机启动" 写法,普通应用层代码中几乎不会出现(执行即崩溃)。
2.void (* signal(int ,void(*)(int) ))q(int)
signal是函数名,以上代码是一次函数声明
声明signal函数的第一个参数类型是int,第二参数的类型是函数指针,该函数指针指向的
函数参数是int,返回类型是void,signal函数的返回类型也是一个函数指针,该函数指针
指向的函数参数是int,返回类型是void
要彻底理解 void (* signal(int ,void(*)(int) ))(int) 这个复杂的 C 语言声明,我们依然遵循从内到外、逐层拆解的思路,先明确核心语法规则,再分析每一层的含义
前置知识:核心语法规则
- 函数指针基础 :
void (*fp)(int)表示fp是指向「参数为 int、返回值为 void 的函数」的指针。 - 声明的核心逻辑 :C 语言声明遵循 "离变量 / 函数名最近的符号优先解析",复杂声明的本质是 "函数名 + 参数列表 + 返回值类型",其中返回值 / 参数可能嵌套函数指针。
- 函数不能返回函数:函数的返回值可以是 "函数指针",但不能是 "函数本身",因此这类声明会用括号把返回值的函数指针类型包裹,避免优先级错误。
逐层拆解 void (* signal(int ,void(*)(int) ))(int)
我们把声明拆分为 "核心函数名(signal)""参数列表""返回值类型" 三大部分,从最内层的子类型开始解析:
第 1 层:最内层的函数指针类型 void(*)(int)
这是声明中参数部分的子类型,也是返回值类型的核心:
- 格式:
void(*)(int)(省略了指针名); - 含义:指向「参数为 1 个 int、返回值为 void 的函数」的指针类型;
- 作用:既是
signal函数的第二个参数类型,也是signal函数的返回值类型的核心。
第 2 层:signal 函数的参数列表 int ,void(*)(int)
聚焦声明中 signal 后的括号 (),这是 signal 函数的完整参数列表:
- 第一个参数:
int------ 一个整型参数(标准库中代表 "信号编号",比如 SIGINT、SIGTERM); - 第二个参数:
void(*)(int)------ 第 1 层的函数指针类型(标准库中代表 "信号处理函数的指针",即收到信号时要执行的函数); - 完整含义:
signal是一个函数,它接收两个参数:① 一个整型值;② 一个指向 "参数为 int、返回 void 的函数" 的指针。
第 3 层:signal(...) ------ 函数名 + 参数列表的整体
把 signal 和它的参数列表结合,得到:signal(int ,void(*)(int)),这部分的含义是:
signal是一个函数,参数如第 2 层所述;- 此时尚未解析返回值,声明的剩余部分(
void (* ... )(int))都是用来描述signal的返回值类型。
第 4 层:void (* ... )(int) ------ 包裹返回值的函数指针类型
声明中最外层的 void (* ... )(int) 是对 signal 返回值的描述(... 代表第 3 层的 signal(参数列表)):
- 拆解这个返回值类型:
void (*fp)(int)(补全指针名 fp),即 "指向参数为 int、返回 void 的函数的指针"; - 括号的关键作用:
(* signal(...))中的括号必须存在 ------ 如果省略,void * signal(...) (int)会被解析为 "signal 函数返回 void*,且后面跟着一个 int 参数",完全违背原意(函数声明中参数列表只能出现一次); - 含义:
signal函数的返回值是一个指向 "参数为 int、返回 void 的函数" 的指针(标准库中代表 "该信号之前的处理函数指针")。
第 5 层:整体拼接 ------ 完整语义
把以上层级拼接,最终解析:
signal是一个函数,它接收两个参数(一个 int、一个指向 "参数为 int 且返回 void 的函数" 的指针),返回值是一个指向 "参数为 int 且返回 void 的函数" 的指针。
补充说明(关键细节)
1. 括号的必要性(核心易错点)
| 错误写法(省略括号) | 错误解析 | 正确写法的括号作用 |
|---|---|---|
void * signal(...) (int) |
把 signal 解析为 "返回 void * 的函数",后面的 (int) 无意义 | (* signal(...)) 强制把 signal 和参数列表作为一个整体,绑定到解引用符*,明确返回值是函数指针 |
void (* signal(...)) int |
括号位置错误,int 脱离函数指针类型 | (int) 包裹参数,明确函数指针的参数类型 |
总结(层级表格)
| 层级 | 代码片段 | 核心含义 |
|---|---|---|
| 1 | void(*)(int) |
指向 "参数为 int、返回 void 的函数" 的指针类型 |
| 2 | signal(int ,void(*)(int)) |
signal 是函数,接收 int 和上述函数指针作为参数 |
| 3 | void (* ... )(int) |
(... 代表层级 2)返回值是上述函数指针类型 |
| 4 | 整体声明 | signal:接收 (int, 函数指针),返回函数指针 |
这个声明的复杂度源于 "函数返回函数指针" 的嵌套写法,核心是通过括号控制优先级,明确 "函数名 - 参数 - 返回值" 的对应关系;而 typedef 是简化这类复杂声明的通用技巧。