一、函数指针基础:函数指针的声明、定义与初始化,理解函数名就是地址

各位同学,今天我们来聊聊函数指针。说实话,我刚入行那会儿,觉得函数指针这玩意儿挺玄乎的——指针我懂,函数我也懂,但「函数指针」是个什么鬼?后来在一个嵌入式项目中,我被逼着用回调机制重构代码,才真正体会到它的威力。嗯,咱们今天就从最基础的东西开始,把函数指针的底裤扒干净。

1.1 函数名就是地址——这事儿你得信

先问大家一个问题:你写了一个函数 void foo(void),然后你打印 foo 这个符号的值,会得到什么?

我告诉你,得到的是一个地址。没错,函数名本质上就是一个地址——它指向代码段中该函数机器码的起始位置。你想想看,CPU 执行函数时,总得知道从哪条指令开始跑吧?这个起始地址,就是函数名代表的那个值。

核心认知:函数名在表达式中会被隐式转换为指向该函数的指针,就像数组名会被转换为指向首元素的指针一样。唯一的区别是,你不能对函数名做取地址再取地址这种套娃操作——虽然语法上允许,但结果还是它自己。

我曾在调试一个 bootloader 时,发现某个函数跳转总是不对。打印出来一看,函数指针的值和实际链接地址差了 0x1000——原来是链接脚本里把代码段放到了错误的地址。从那以后,我养成了一个习惯:但凡遇到函数指针相关的 bug,先打印地址看看。

1.2 函数指针的声明——看着别扭,其实有规律

函数指针的声明,说白了就是把一个普通函数声明中的函数名换成 (*指针名)。举个例子:

// 普通函数声明
int add(int a, int b);

// 函数指针声明
int (*p_func)(int a, int b);

看到没?把 add 换成 (*p_func),其他部分一模一样。为什么加括号?因为 * 的优先级低于 (),如果不加括号,int *p_func(int a, int b) 就变成了「返回 int 指针的函数」,而不是「函数指针」。

我的小技巧:声明函数指针时,我习惯先写出目标函数的原型,然后把函数名用 (*指针名) 替换掉。这样不容易出错。比如你要指向一个 void handler(int) 类型的函数,就写 void (*p_handler)(int);

1.3 定义与初始化——三种方式,一种本质

函数指针的初始化,说白了就是把一个函数的地址赋给它。有三种写法:

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

int (*p1)(int, int);
p1 = add;           // 方式1:直接赋值函数名
p1 = &add;          // 方式2:取地址(其实多余)
p1 = *add;          // 方式3:解引用再赋值(纯属折腾)

// 定义时直接初始化
int (*p2)(int, int) = add;

这三种方式,最终 p1 的值都一样——就是 add 函数的入口地址。我个人习惯用方式1,简洁明了。方式2加个取地址符,虽然语义上更明确「我在取地址」,但编译器眼里都一样。

注意:函数指针的类型必须与目标函数完全匹配——返回值类型、参数个数、每个参数的类型,一个都不能差。我曾经在项目中把一个 void (*)(int) 类型的指针赋值给了 int (*)(int, int) 类型的变量,编译器只给了一个 warning,结果运行时栈乱了,查了整整两天。

1.4 通过函数指针调用函数——两种写法,随你喜好

拿到函数指针后,怎么调用它指向的函数?有两种写法:

int (*p)(int, int) = add;

// 写法1:隐式解引用
int result1 = p(3, 5);

// 写法2:显式解引用
int result2 = (*p)(3, 5);

两种写法结果一样。我个人偏爱写法1,因为少打两个字符,而且看起来就像在调用一个叫 p 的函数——实际上也确实如此。写法2更明确地表达了「我在通过指针调用函数」,适合教学场景。

1.5 实战中的避坑指南

说了这么多,我总结几个实际项目中容易踩的坑:

  • 未初始化就调用:函数指针声明后默认值是野指针,直接调用必崩。我曾经接手过一个遗留代码,里面有个全局函数指针没初始化,某个模块在特定条件下调用了它,结果系统随机死机。排查了三天才发现是这个问题。
  • 类型不匹配:回调函数的参数类型、个数必须和函数指针声明完全一致。嵌入式开发中,中断服务函数的指针尤其容易搞错——参数类型差一个字节,整个栈就歪了。
  • 函数指针数组:当你有一组功能相似的函数时,可以用函数指针数组来管理。比如状态机中的状态处理函数:void (*state_table[STATE_MAX])(void); 这样写比 switch-case 优雅得多。

一句话总结:函数名就是地址,函数指针就是存放这个地址的变量。声明时注意括号,初始化时注意类型,调用时注意判空。记住这三点,函数指针你就入门了。

1.6 知识体系图

下面这张图帮你理清函数指针的核心脉络:

函数指针 声明 int (*p)(int, int); 初始化 p = add; 或 p = &add; 调用 p(3,5) 或 (*p)(3,5) 函数名就是地址 类型必须完全匹配 调用前必须判空 本质:函数指针 = 存放函数入口地址的变量

1.7 小结

函数指针的基础就这些。说白了,它就是一个指针变量,只不过指向的是函数而不是普通数据。理解「函数名就是地址」这个事实,后面的回调机制、函数指针数组、面向对象风格的 C 代码,你都能轻松拿捏。

嗯,今天就到这儿。记住我强调的那三点:声明注意括号,初始化注意类型,调用注意判空。下节课我们聊回调机制——那才是函数指针真正发光发热的地方。


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