指针进阶:函数指针详解

一、函数指针

函数指针是 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 语言的高级特性,核心价值在于:

  1. 实现回调函数 (如qsort、事件驱动);
  2. 动态切换函数逻辑(如插件化、策略模式);
  3. 批量管理同类型函数(函数指针数组)。

掌握函数指针的关键是:匹配函数类型理解语法优先级结合实际场景使用(避免过度设计)。


二、两个函数含义解析

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 的函数

补充说明(关键细节)

  1. 括号的必要性

    • 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 语言声明,我们依然遵循从内到外、逐层拆解的思路,先明确核心语法规则,再分析每一层的含义

前置知识:核心语法规则

  1. 函数指针基础void (*fp)(int) 表示 fp 是指向「参数为 int、返回值为 void 的函数」的指针。
  2. 声明的核心逻辑 :C 语言声明遵循 "离变量 / 函数名最近的符号优先解析",复杂声明的本质是 "函数名 + 参数列表 + 返回值类型",其中返回值 / 参数可能嵌套函数指针。
  3. 函数不能返回函数:函数的返回值可以是 "函数指针",但不能是 "函数本身",因此这类声明会用括号把返回值的函数指针类型包裹,避免优先级错误。

逐层拆解 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 是简化这类复杂声明的通用技巧。

相关推荐
胡萝卜3.02 小时前
C++现代模板编程核心技术精解:从类型分类、引用折叠、完美转发的内在原理,到可变模板参数的基本语法、包扩展机制及emplace接口的底层实现
开发语言·c++·人工智能·机器学习·完美转发·引用折叠·可变模板参数
leoufung2 小时前
LeetCode 22:Generate Parentheses 题解(DFS / 回溯)
算法·leetcode·深度优先
9ilk2 小时前
【C++】--- C++11
开发语言·c++·笔记·后端
FMRbpm2 小时前
队列练习--------最近的请求次数(LeetCode 933)
数据结构·c++·leetcode·新手入门
biter down2 小时前
C++ 函数重载:从概念到编译原理
开发语言·c++
断剑zou天涯3 小时前
【算法笔记】bfprt算法
java·笔记·算法
youngee113 小时前
hot100-47岛屿数量
算法
ZouZou老师3 小时前
C++设计模式之解释器模式:以家具生产为例
c++·设计模式·解释器模式
yue0084 小时前
C# winform自定义控件
开发语言·c#