13、函数指针:函数指针的定义与赋值、通过函数指针调用函数、函数指针数组

各位同学,今天我们来聊一个让很多C语言学习者又爱又恨的话题——函数指针。说实话,我当年刚接触这个概念时,也觉得它有点绕。明明函数就是函数,为什么还要搞个指针指向它?这不是多此一举吗?

直到我在一个嵌入式项目中,需要根据不同的传感器类型调用不同的数据处理函数。那时候我才恍然大悟:函数指针,说白了就是让代码变得灵活、可配置的利器。你想想看,如果不用函数指针,我可能得写一堆if-else或者switch-case,代码又臭又长。用了函数指针,整个架构瞬间清爽了。

13.1 函数指针的定义与赋值

先说说函数指针到底是什么。函数指针,顾名思义,就是一个指向函数的指针变量。它存储的是函数的入口地址。在C语言中,函数名本身就是一个地址——函数的首地址。

定义函数指针的语法,说实话有点反人类。我刚开始学的时候也经常搞混。它的基本形式是:

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

注意那个括号!(*指针变量名) 必须用括号括起来。如果不加括号,就变成了返回指针的函数声明,那完全是两码事了。嗯,这里要特别小心。

来看个具体的例子:

#include <stdio.h>

// 定义一个普通函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 定义一个函数指针,指向返回值为int、参数为两个int的函数
    int (*pFunc)(int, int);
    
    // 赋值:将函数名赋给指针
    pFunc = add;
    
    // 或者写成 &add 也可以,但函数名本身就是地址
    // pFunc = &add;
    
    printf("函数指针的地址: %p\n", pFunc);
    printf("函数本身的地址: %p\n", add);
    
    return 0;
}

我在项目中遇到过一个问题:有人把函数指针定义成了 int *pFunc(int, int),结果编译器报了一堆错。这就是没加括号的后果——它变成了一个返回int指针的函数声明,而不是函数指针。

核心要点:

  • 函数指针的类型必须与指向的函数签名完全一致(返回值类型和参数类型都要匹配)
  • 函数名就是函数的地址,可以直接赋值给函数指针
  • 定义时括号不能省略:(*指针名)

13.2 通过函数指针调用函数

函数指针赋值之后,怎么用它来调用函数呢?有两种方式:

// 方式一:直接通过指针名调用
result = pFunc(3, 5);

// 方式二:显式解引用调用
result = (*pFunc)(3, 5);

我个人习惯用第一种方式,简洁明了。第二种方式虽然更直观地体现了"指针解引用"的概念,但写起来多了一层括号,看着有点啰嗦。

来看一个完整的例子:

#include <stdio.h>

int multiply(int a, int b) {
    return a * b;
}

int main() {
    int (*calc)(int, int);
    
    calc = multiply;
    
    // 通过函数指针调用
    int result = calc(4, 6);
    printf("4 * 6 = %d\n", result);
    
    // 也可以这样
    result = (*calc)(4, 6);
    printf("4 * 6 = %d\n", result);
    
    return 0;
}

这里有个小细节:如果你用 calc(4, 6) 这种方式,编译器会自动把 calc 当作函数指针解引用。所以两种写法效果完全一样。

我的经验:在嵌入式系统中,函数指针经常被用来实现回调机制。比如定时器超时后,通过函数指针调用用户注册的处理函数。这样框架代码不需要知道具体要做什么,只负责在合适的时候调用指针指向的函数即可。

13.3 函数指针数组

函数指针数组,就是把多个函数指针放在一个数组里。这个在实现菜单系统、状态机或者命令分发器时特别有用。

定义函数指针数组的语法:

返回类型 (*数组名[数组大小])(参数类型列表);

举个例子,假设我们有三个数学运算函数:

#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 main() {
    // 定义函数指针数组,包含3个元素
    int (*ops[3])(int, int) = {add, sub, mul};
    
    // 也可以先定义再赋值
    // int (*ops[3])(int, int);
    // ops[0] = add;
    // ops[1] = sub;
    // ops[2] = mul;
    
    int a = 10, b = 5;
    
    // 通过数组下标调用不同的函数
    printf("add(10, 5) = %d\n", ops[0](a, b));
    printf("sub(10, 5) = %d\n", ops[1](a, b));
    printf("mul(10, 5) = %d\n", ops[2](a, b));
    
    return 0;
}

我曾经在一个工控项目中,用函数指针数组实现了命令解析器。上位机发来不同的命令码,程序直接通过命令码作为数组下标,调用对应的处理函数。代码简洁、执行效率高,而且添加新命令只需要在数组里加一个函数指针就行,完全不用改主逻辑。

注意:函数指针数组虽然好用,但要注意数组越界问题。如果下标超出了数组范围,程序会访问到未知的内存地址,轻则崩溃,重则产生难以排查的bug。我建议在使用前做边界检查。

13.4 知识体系总览

下面这张图总结了函数指针的核心知识点和它们之间的关系:

函数指针 定义与赋值 语法:返回类型 (*指针名)(参数类型列表) 赋值:指针名 = 函数名(函数名即地址) 通过指针调用函数 方式一:指针名(参数) 方式二:(*指针名)(参数) 函数指针数组 语法:返回类型 (*数组名[大小])(参数类型列表) 通过下标选择要调用的函数 典型应用场景 回调机制 | 命令分发器 | 状态机 | 菜单系统 | 插件架构 —— 让代码更灵活、可配置、易扩展 ——

13.5 避坑指南

最后,分享几个我踩过的坑:

  • 类型不匹配:函数指针的类型必须与函数完全一致。我曾经把返回 int 的函数指针赋值给了返回 void 的函数,编译没报错,但运行时数据全乱了。
  • 空指针调用:函数指针在使用前一定要检查是否为NULL。我曾经在系统初始化时忘记注册回调函数,结果程序一运行就崩溃了。
  • 作用域问题:如果函数指针指向的是局部函数(比如在另一个文件中的static函数),跨文件使用时要注意链接问题。

我的建议:在嵌入式开发中,函数指针经常用于实现"分层设计"。比如驱动层提供一组函数指针,应用层通过注册的方式把具体实现挂上去。这样驱动层和应用层解耦,换硬件时只需要换驱动实现,应用代码完全不用动。

好了,函数指针的内容就讲到这里。这东西刚开始可能觉得别扭,但用多了就会发现它真的很强大。下次写代码时,不妨想想:这里能不能用函数指针让代码更灵活一些?


公众号:蓝海资料掘金营,微信deep3321