C 语言 —— 函数指针

复制代码
复习一下 C 语言的指针中的 函数指针     ...... 矜辰所致

前言

指针作为 C 语言关键与难点之一 ,说是 C 语言的灵魂也不为过,真正的掌握好了它,才能说会 C 语言。

而在指针中,函数指针相对来说既是一大难点,也是非常重要的一点,在 Linux 内核中 函数指针 是核心机制,可见函数指针存在的意义。

所以本文我们主要来 复习一下 C 语言的函数指针,顺带着过一下指针的基础知识。

我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!

目录

  • 前言
  • [一、 指针基础介绍](#一、 指针基础介绍)
    • [1.1 什么是指针](#1.1 什么是指针)
    • [1.2 如何声明指针](#1.2 如何声明指针)
    • [1.3 指针运算符](#1.3 指针运算符)
    • [1.4 指针的常见用途](#1.4 指针的常见用途)
    • [1.5 基本指针分析](#1.5 基本指针分析)
  • 二、函数指针
    • [2.1 函数指针本质](#2.1 函数指针本质)
    • [2.2 函数的定义和语法](#2.2 函数的定义和语法)
    • [2.3 函数指针的基本使用步骤](#2.3 函数指针的基本使用步骤)
    • [2.4 常见应用示例](#2.4 常见应用示例)
      • [2.4.1 用函数指针灵活调用不同函数](#2.4.1 用函数指针灵活调用不同函数)
      • [2.4.2 函数指针作为函数参数](#2.4.2 函数指针作为函数参数)
      • [2.4.3 函数指针数组](#2.4.3 函数指针数组)
    • [2.5 typedef 之于函数指针](#2.5 typedef 之于函数指针)
  • 结语

一、 指针基础介绍

1.1 什么是指针

在C语言中,指针也是一个变量。 它的特别之处是这个变量保存的不是普通的数据,而是另一个变量的地址。

地址怎么理解,内存中每一个字节都有一个唯一的位置,这个位置有一个编号,即为地址。

为了便于理解,我们用常用的 STM32 MCU 举例:

  • 对于变量:编译器会在 RAM地址区域(例如 0x2000 0000 之后)为它分配一个地址。对这个变量的读写操作都会在这个地址上进行。
  • 对于操作代码,比如调用函数时:CPU会从 Flash地址区域(例如 0x0800 0000 之后)的某个地址取出指令来执行。
  • 对于外设:是通过向 外设地址区域(例如 0x4001 1000 这个地址)的某个地址(对应外设寄存器)写入一个值来完成的。CPU 操作外设和操作内存的方式是一样的。

所以我们说的指针,就是保存了一个地址的变量。

还需要说明一下:

指针是个变量!!

只要是个变量就有个值!!

对32位系统而言,指针的大小永远是 4个字节!!(32位系统的地址用 32 位数据(4个字节)来表示的)

那对于 64 位系统而言,指针的大小就是 8 个字节!

1.2 如何声明指针

在 C 语言中,使用如下语句声明指针:

语法:数据类型 *指针变量名;

指针声明需要指定它指向的数据类型,比如:

c 复制代码
int *p1;//声明一个指向整型(int)的指针,名字叫p1 
char *p2;//声明一个指向字符型(char)的指针,名字叫p2

1.3 指针运算符

  • 取地址运算符 &:获取一个变量的内存地址。

&a 就表示获取变量 a 的地址。

  • 解引用运算符 *:取出指针指向的那个地址的内容。根据指针中存储的地址,访问或修改该地址上存储的数据。

*p 表示 "获取 p 指向的那个地址里存储的值",如果给*p 赋值,就表示修改那个地址的数据。

示例:

这里说明一下,因为博主示例是在 Windows 上使用 GCC 编译,实际调的是 MinGW ,所以即便使用 %p 输出,也不会在开头加0x 。在 Linux 下,会自动加上 0x ,如下图同样的代码 :

博主测试使用的是 64 位的系统,所以指针的大小为 8 个字节。

1.4 指针的常见用途

  • 间接访问与修改:提供了一种间接操作内存和数据的手段,允许函数修改其外部变量。
  • 高效的数据处理:传递一个指针(一个地址)比传递整个数据块(如大型结构体)要快得多,也更节省空间。
  • 动态与灵活:动态内存管理(运行时决定内存大小)、构建复杂数据结构(如链表)。

1.5 基本指针分析

此小节是博主很早学习时候的笔记,也作为补充内容:

  • int p; //这是一个普通的整型变量;
  • int *p; //首先从P 处开始,先与*结合,所以说明P是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针;
  • int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组;
  • int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P是一个由返回整型数据的指针所组成的数组;
  • int (*p)[3]; //首先从P 处开始,先与*结合,说明P是一个指针,然后再与[]结合 ( 与"()"这步可以忽略,只是为了改变优先级 ),说明指针所指向的内容是一个数组,然后再与int结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针;
  • int **p; //首先从P开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int结合,说明该指针所指向的元素是整型数据,指向指针的指针;
  • int p(int); //从P 处起,先与()结合,说明P是一个函数, 然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据 ;
  • int(*p)(int); //从P 处开始,先与*结合,说明P是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int结合, 说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针;
  • int *(*p(int))[3]; //从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int结合, 说明函数有一个整型变量参数, 然后再与外面的*结合,说明函数返回的是一个指针, 然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数。

二、函数指针

2.1 函数指针本质

我们知道 普通指针是指向内存中的变量(如 int* p 指向一个整数)。可以通过地址操作变量,对于函数而言,我们也可以 通过指针指向的地址调用函数。

函数的本质是一段内存中的代码(占用一片连续内存)

函数拥有类型,函数类型由 返回类型 和 参数类型列表 组成:

函数名就是函数体代码的起始地址 (函数入口地址),比如函数 add, add&add 都表示函数地址,他们等价。

通过函数名调用函数, 本质为指定具体地址的跳转执行 。

因此,可定义指针,保存函数入口地址,就是我们说的函数指针。

函数指针,就是存储函数"入口地址" 的变量,通过它能间接调用对应的函数。

普通指针 int *p = &a; 是"指向变量 a 的地址",

函数指针 int (*p)(int,int); 是 指向一个 参数为开两个 int ,返回值为 int 的函数的地址。

2.2 函数的定义和语法

函数指针的定义格式如下:

返回类型 (*指针变量名)(参数列表);

  • 括号 (*指针变量名) 必须加:否则会被解析为"返回值是指针的函数"(比如 int *p(int) 是函数声明,不是函数指针)。

  • 参数类型要和目标函数完全匹配(参数个数、类型、顺序),返回值类型也要匹配。

示例:

假如目标函数是:uint32_t mycallback( uint32_t a, uint8_t b);

对应的函数指针定义:

c 复制代码
// mycb 是函数指针变量,指向参数为(uint32_t ,uint8_t )、返回值为 uint32_t  的函数
uint32_t (*mycb)( uint32_t ,uint8_t );

2.3 函数指针的基本使用步骤

函数指针使用步骤:定义函数指针 → 给指针赋值(绑定目标函数) → 通过指针调用函数。

示例:

步骤1:定义目标函数

先完成函数定义,作为作为函数指针的"指向对象":

c 复制代码
// 加法函数:参数int x、int y,返回int
int add(int x, int y) {
    return x + y;

步骤2:定义函数指针并赋值

给函数指针赋"目标函数的地址",我们上文说过add&add 都表示函数地址。

c 复制代码
    // 1. 定义函数指针p
    int (*p)(int, int);
    
    // 2. 赋值:绑定add函数(p存储add的地址)
    p = add;        // 方式1:直接用函数名(推荐,简洁)
    // p = &add;    // 方式2:用&取函数地址,和方式1等价   

步骤3:通过函数指针调用函数

c 复制代码
int main() {
    int (*p)(int, int);
    p = add;
    
    // 调用方式1:指针变量名 + (参数)
    int res1 = p(3, 5);  // 等价于 add(3,5),res1=8
    
    // 调用方式2:(*指针变量名) + (参数)(更直观体现"指针调用")
    int res2 = (*p)(4, 6);  // 等价于 add(4,6),res2=10
    
    printf("res1=%d, res2=%d\n", res1, res2);  // 输出:res1=8, res2=10
    return 0;
}

2.4 常见应用示例

本小节就直接列举一些函数指针的常见使用场景

2.4.1 用函数指针灵活调用不同函数

如果有多个"参数类型、返回值一致"的函数,可通过同一个函数指针动态切换调用,减少重复代码:

c 复制代码
#include <stdio.h>

// 目标函数:加法、减法、乘法
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 main() {
    int (*p)(int, int);  // 通用函数指针
    int a = 10, b = 5;
    
    // 切换1:调用加法
    p = add;
    printf("10+5=%d\n", p(a, b));  // 输出:10+5=15
    
    // 切换2:调用减法
    p = sub;
    printf("10-5=%d\n", p(a, b));  // 输出:10-5=5
    
    // 切换3:调用乘法
    p = mul;
    printf("10*5=%d\n", p(a, b));  // 输出:10*5=50
    
    return 0;
}

2.4.2 函数指针作为函数参数

把函数指针传给另一个函数 ,实现回调函数。

c 复制代码
#include <stdio.h>

// 目标函数:加、减、乘
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; }

// 通用计算器函数:func是函数指针参数,接收"int(int,int)"类型的函数
int calculator(int x, int y, int (*func)(int, int)) {
    // 调用传入的函数指针(动态执行不同运算)
    return func(x, y);
}

int main() {
    int a = 20, b = 4;
    
    // 1. 传入add,实现加法计算
    int sum = calculator(a, b, add);
    printf("20+4=%d\n", sum);  // 输出:20+4=24
    
    // 2. 传入sub,实现减法计算
    int diff = calculator(a, b, sub);
    printf("20-4=%d\n", diff);  // 输出:20-4=16
    
    // 3. 传入mul,实现乘法计算
    int product = calculator(a, b, mul);
    printf("20*4=%d\n", product);  // 输出:20*4=80
    
    return 0;
}

2.4.3 函数指针数组

如果有多个同类型的函数,可用"函数指针数组"统一存储和调用,适合批量处理场景(比如菜单功能切换):

c 复制代码
#include <stdio.h>

// 目标函数:四个功能函数(参数、返回值一致)
void func1() { printf("执行功能1:登录\n"); }
void func2() { printf("执行功能2:查询\n"); }
void func3() { printf("执行功能3:修改\n"); }
void func4() { printf("执行功能4:退出\n"); }

int main() {
    // 定义函数指针数组:数组名arr,每个元素是"无参、无返回值"的函数指针
    void (*arr[])() = {func1, func2, func3, func4};
    
    int choice;
    printf("请选择功能(1-4):");
    scanf("%d", &choice);
    
    // 通过数组下标调用对应函数(choice-1对应数组索引)
    if (choice >= 1 && choice <= 4) {
        arr[choice-1]();  // 比如输入2,调用arr[1]即func2
    } else {
        printf("选择错误\n");
    }
    
    return 0;
}

运行效果:

复制代码
请选择功能(1-4):2
执行功能2:查询

2.5 typedef 之于函数指针

对于函数指针的应用中,使用typedef 给函数指针起别名,新手往往会有点稀里糊涂。

我们知道 typedef 的效果是把"复杂声明"变成"简单类型",对于函数指针而言,学会使用 typedef ,也是用好函数指针的关键点。

首先我们来说说对于函数指针而言, typedef 为什么让很多新手感觉不对劲。

我们对于普通的 typedef

c 复制代码
typedef unsigned char      uint8_t; // unsigned char 取了个新名字叫uint8_t 

函数指针 typedef

c 复制代码
typedef int (*Operation)(int, int);

感觉上去,像是一个函数指针定义: int (*p)(int, int); 上面看起来像是定义了一个叫 Operation 的函数指针变量。

但是实际上这里 Operation 只是一个类型的别名,不是变量!

c 复制代码
// 函数指针完全一样
typedef int (*Operation)(int, int);

int add(int a, int b) { return a + b; }

Operation = add;  // ❌ 编译错误!Operation 是类型,不是变量

Operation my_add = add;  // my_add 是变量,Operation 是类型

int result = my_add(3, 5);  

Operation a 等价于int (*a)(int, int) ,都表示定义一个函数指针,这里只是定义!没有赋值!以后赋值,直接 a=add 即可,使用 typedef 使得函数指针的定义变得简单::

c 复制代码
typedef int (*Operation)(int, int);
//        ^     ^         ^
//        |     |         |
//        |     |         +----- 参数列表(目标函数的特征)
//        |     +--------------- 别名名字(你自己取的小名)
//        +--------------------- 返回类型(目标函数的特征)

// 注意:没有变量名!因为 typedef 不定义变量!

// 使用:用这个别名定义变量
Operation p;  // 现在才是定义变量 p

typedef 作用只是把 "复杂声明" 变成 "简单类型" 。

任何 typedef 语句,你把 typedef 删掉,剩下的部分就是在定义一个变量。

既然这里这么难懂,为什么函数指针要用 typedef ?

还是得回到 typedef 的作用上来,使得代码阅读使用更加的简化。

c 复制代码
/*不使用typedef 简化复杂的声明,Callback函数声明如下*/
Callback(u8 *begin, void (*pFunCallback)(u8* pchar, u8 Len), bool state); 
/*使用typedef ,Callback函数声明如下*/
typedef void (*PFunCallBack)(u8* pchar, u8 Len);

Callback(u8 *begin,PFunCallBack pFun, bool state);//等价于上面

再看一个示例(不使用 typedef ):

c 复制代码
// 1. 定义函数指针数组(100 行代码里出现 5 次)
int (*event_handlers[256])(int, void*, void*); 

// 2. 函数参数(声明时)
void register_handler(int (*handler)(int, void*, void*));

// 3. 函数参数(实现时)
void register_handler(int (*handler)(int, void*, void*)) {
    // 150 行代码...
}

// 4. 结构体成员
struct EventSystem {
    int (*handlers[256])(int, void*, void*);
};

// 5. 返回函数指针的函数
int (*(*get_handler(const char*))(int, void*, void*);

// 6. 调用时
int result = (*event_handlers[type])(event, data, context);

使用 typedef 后:

c 复制代码
// 第 1 行:一次定义,全局通用
typedef int (*EventHandler)(int, void*, void*);

// 2. 函数指针数组
EventHandler handlers[256];

// 3. 函数参数(声明)
void register_handler(EventHandler handler);

// 4. 函数参数(实现)
void register_handler(EventHandler handler) {
    // 150 行代码...
}

// 5. 结构体成员
struct EventSystem {
    EventHandler handlers[256];
};

// 6. 返回函数指针的函数
/*
// 声明一个"返回函数指针的函数"
int (*(*get_op(char)))(int, int);

// 第1步:给函数指针类型起别名
typedef int (*Operation)(int, int);

// 第2步:给"返回函数指针的函数"类型起别名
typedef Operation (*GetOpFunc)(char);

// 第3步:使用别名声明函数(清晰!)
GetOpFunc my_getter = get_op;  // 一目了然
*/

typedef EventHandler (*HandlerGetter)(const char*);

HandlerGetter get_handler;

// 7. 调用时
int result = handlers[type](event, data, context);

额外说明:

在上面的例子中,还有一个地方再单独说明一下吧,就似乎下面这几句代码:

c 复制代码
// 声明一个"返回函数指针的函数"
// 问题1:可读性 - 看到声明不知道参数/返回值是什么
// 问题2:可维护性 - 如果 Operation 要改签名,这里也要手动改
// 问题3:无法复用 - 你需要"返回 Operation 的函数"时,每次都重写语法      
int (*(*get_op(char)))(int, int);
c 复制代码
// 如果使用 typedef 第1步:给函数指针类型起别名
typedef int (*Operation)(int, int);

// 第2步:给"返回函数指针的函数"类型起别名
// 等价于:typedef int (*(*GetOpFunc)(char))(int, int);
typedef Operation (*GetOpFunc)(char);

// 第3步:使用别名声明函数(清晰!)
GetOpFunc my_getter = get_op;  

typedef Operation (*GetOpFunc)(char); 本来我们使用 Operation 就是一个为了简单取的别名,为什么要使用这个 把 简单的别名 搞复杂了?

这个地方 Operation 虽然简单,但它不能描述"返回 Operation 的函数" ,具体的嘛还得是要多使用代码测试,这里找了一个示例:

c 复制代码
#include <stdio.h>

// ==================== 第0步:制造4个具体工具(加减乘除) ====================
int add_tool(int a, int b) { 
    printf("加法");
    return a + b; 
}

int sub_tool(int a, int b) { 
    printf("减法");
    return a - b; 
}

int mul_tool(int a, int b) { 
    printf("乘法");
    return a * b; 
}

int div_tool(int a, int b) { 
    printf("除法");
    return b != 0 ? a / b : 0; 
}

// ==================== 第1步:给"工具"起别名 ====================
// Operation = 所有"能进行加减乘除的工具"的类型
typedef int (*Operation)(int, int);

// 现在,Operation 就是 int (*)(int, int) 的简短名字
// 它代表:任何一个接收两个int、返回int的工具函数

// ==================== 第2步:给"领工具的窗口"起别名 ====================
// GetOpFunc = 所有"根据编号发放工具的窗口"的类型
// 这种窗口的特点是:输入一个字符,返回一个 Operation 类型的工具
typedef Operation (*GetOpFunc)(char);

// 注意:GetOpFunc 描述的是"窗口的行为",不是"窗口里的工具"

// ==================== 第3步:实现一个具体的"领工具窗口" ====================
// 这个函数就是"工具领取窗口"的实体
// 它根据字符返回对应的工具(Operation类型)
Operation tool_window(char tool_id) {
    switch(tool_id) {
        case '+': return add_tool;  // 发加法工具
        case '-': return sub_tool;  // 发减法工具
        case '*': return mul_tool;  // 发乘法工具
        case '/': return div_tool;  // 发除法工具
        default:  
            printf("[错误]");
            return NULL;
    }
}

// ==================== 第4步:使用工具箱 ====================
int main() {
    // 方式1:直接操作单个工具(只用 Operation)
    printf("\n【方式1:直接拿工具用】\n");
    Operation my_tool = add_tool;  // 直接拿
    int result1 = my_tool(10, 5);   //
    printf("%d\n", result1);
    
    // 方式2:通过窗口领工具(用 GetOpFunc)
    printf("\n【方式2:通过窗口领工具】\n");
    GetOpFunc window = tool_window;  // 我找到"工具领取窗口"
    
    // 现在我想做减法,去窗口领减法工具
    Operation sub = window('-');     // 窗口发给我 sub_tool
    int result2 = sub(20, 8);        // 我用减法工具干活
    printf("%d\n", result2);         // 输出: 12
    
    // 方式3:一行代码完成领工具+用工具(函数式编程)
    printf("\n【方式3:现领现用】\n");
    int result3 = window('*')(6, 7);  // 领乘法工具,立刻用
    printf("%d\n", result3);          // 输出: 42
    
    // 方式4:批量测试所有工具
    printf("\n【方式4:测试所有工具】\n");
    char tools[] = {'+', '-', '*', '/'};
    for (int i = 0; i < 4; i++) {
        Operation op = window(tools[i]);  // 从窗口领工具
        if (op != NULL) {
            int result = op(100, 20);     // 用工具算 100 和 20
            printf("%d\n", result);
        }
    }
    return 0;
}

如果这里没有这一步typedef Operation (*GetOpFunc)(char); 大家可以自己尝试一下。

最后总结一下,建议在大家学习使用函数指针的时候,从一开始就学会使用 typedef 的方式去应用,这里也只有用得熟练了,在以后的应用中,才能更加的顺利。

c 复制代码
// 原始方式
int (*handler)(int, void*) = my_func;  // 

// typedef 方式,建议一开始就学会这种
typedef int (*Handler)(int, void*);
Handler h = my_func;  // 

结语

本文复习了一下 C 语言指针以及函数指针的基础知识,在日常使用中,用现成的代码,相信大家只要认真学习过都不会有什么问题,可是如果遇到需要自己写驱动,写工程,必须要学会使用函数指针,这就需要多花点功夫了,希望大家能够熟练的掌握函数指针的用法,让日后的程序设计更加优雅高效。

好了,本文就到这里。谢谢大家!

相关推荐
zore_c1 小时前
【C语言】struct结构体内存对齐和位段(超详解)
c语言·开发语言·经验分享·笔记
MC皮蛋侠客1 小时前
C++17多线程编程全面指南
开发语言·c++
郝学胜-神的一滴1 小时前
Linux C++系统编程:使用mmap创建匿名映射区
linux·服务器·开发语言·c++·程序人生
新手村领路人1 小时前
c++ opencv缺少openh264-1.8.0-win64.dll
开发语言·c++
周杰伦fans1 小时前
C# - 直接使用 new HttpClient() 和使用 HttpClientFactory 的区别
开发语言·c#
kyle~1 小时前
C++ --- noexcept关键字 明确函数不抛出任何异常
java·开发语言·c++
不知所云,1 小时前
6. c++ 20 Modules 使用
开发语言·c++20·c++ modules
沐浴露z1 小时前
详解Java ArrayList
java·开发语言·哈希算法
x***B4111 小时前
Rust unsafe代码规范
开发语言·rust·代码规范