普通变量和数组用来存放数据,在内存中都有各自的地址。那么 “函数” 有没有内存地址呢?答案是肯定的。
在 C++ 中,函数编译后会变成一段机器指令并加载到内存中,函数名代表的就是这段指令的入口地址。如果我们定义一个指针变量专门用来保存函数的入口地址,它就叫做 “函数指针”。
C++ 函数指针的使用
在 C++ 中,定义函数指针时需要在声明语句中同时写明目标函数的返回值类型与参数类型列表。
语法:
返回值类型 (*指针变量名)(参数类型列表);说明:
以 int (*p)(int, int); 为例,遇到这种带两对小括号的声明时,我们可以按 “先中间、再右边、后左边” 的顺序分 3 步拆开来看:
- 先看中间的括号:
(*p)表明p是一个指针变量。 - 再看右边的参数列表:
(int, int)表明目标函数需要接收两个int参数。 - 最后看左边的类型:最左边的
int表明目标函数的返回值类型是int。
需要特别注意的是,(*p) 外面的小括号 () 一定不能省略,这一点和数组指针非常相似。一旦漏掉小括号写成 int* p(int, int),由于 () 的优先级高于 *,p 就会先与后面的参数列表结合。我们同样可以在中间加上一个 “的” 字来区分它们:
- 函数指针:即 “函数的指针”(如
int (*p)(int, int)),落脚点在 “指针”,本质上是一个指针变量,用来指向函数。 - 指针函数:即 “指针的函数”(如
int* findMax(int* a, int* b)),落脚点在 “函数”,本质上是一个普通函数,只不过它的返回值是一个int*指针。
接下来,我们通过一个简单的例子来看看函数指针具体是怎么定义和调用的。
示例 1:定义并使用函数指针
#include <iostream>
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
int (*p)(int, int) = nullptr; // 定义函数指针并置空
p = add; // 指向 add 函数
std::cout << p(10, 3) << '\n';
p = subtract; // 改为指向 subtract
std::cout << p(10, 3) << '\n';
return 0;
}运行结果如下。
13
7分析:
int (*p)(int, int) = nullptr; 定义了一个函数指针 p,并先将其初始化为空指针 nullptr。
由于 add() 和 subtract() 都是接收两个 int 参数并返回 int 的函数,我们可以先执行 p = add 让指针指向 add,调用 p(10, 3) 输出 10 + 3 的结果 13。随后再执行 p = subtract 更换指向,同样调用 p(10, 3),输出的结果就变成了 10 - 3 的结果 7。
函数名本身就代表函数的入口地址。因此给函数指针赋值时加不加取地址符 & 都可以,通过函数指针调用函数时直接写 p(10, 3) 和先解引用再写 (*p)(10, 3) 也是完全等价的。
// 方式 1(推荐)
p = add;
int res1 = p(10, 3);
// 方式 2
p = &add;
int res2 = (*p)(10, 3);在实际开发中,我们更推荐使用方式 1,省去多余的符号,代码更加清爽直观。此外还要注意,函数指针的 “返回值类型”、“参数个数” 和 “参数类型” 必须与目标函数完全一致。哪怕只是参数类型不同也不能赋值,下面这种写法就会导致编译报错。
double divide(double a, double b) {
return a / b;
}
// 错误方式
int (*p)(int, int) = divide;函数指针与 auto 类型推导
函数指针的完整类型写起来比较长,刚接触时很容易漏写小括号。如果我们在定义函数指针变量的同时直接让它指向某个已知函数,可以使用前面学过的 auto 关键字,交由编译器自动推导出函数指针的类型。
比如在示例 1 中,直接写 auto p = add; 就等价于 int (*p)(int, int) = add;。不过要注意,如果定义时先初始化为 nullptr,或者在函数形参列表中声明函数指针参数,编译器就无法自动推导类型了。遇到这两种情况时,我们仍然需要写出完整的函数指针类型。
使用函数指针实现回调函数
如果在同一个函数里已经明确知道要调用 add(10, 3),我们直接写函数名调用即可,没必要特意定义一个函数指针。在实际开发中,函数指针最主要的用途,是把一个函数作为参数传递给另一个函数。
平时写代码都是我们主动调用某个函数。如果我们把函数 A 的地址作为参数传给函数 B,等函数 B 执行到特定步骤时再 “回过头来调用” 函数 A,这种机制就叫做 “回调”。其中,被作为参数传进去的函数 A 就叫做 “回调函数(Callback Function)”。
为了方便小伙伴们看清回调函数的传递过程,下面我们先使用简单的整数加法和乘法来演示语法。
示例 2:使用函数指针实现回调
#include <iostream>
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
// op 用于接收外部传入的回调函数
void printResult(int a, int b, int (*op)(int, int)) {
int result = op(a, b); // 调用回调函数
std::cout << "计算结果:" << result << '\n';
}
int main() {
printResult(6, 4, add); // 传入 add 算加法
printResult(6, 4, multiply); // 传入 multiply 算乘法
return 0;
}运行结果如下。
计算结果:10
计算结果:24分析:
在这个例子中,printResult() 的前两个形参 a 和 b 是普通整数,第 3 个形参 int (*op)(int, int) 则是一个函数指针,用来接收外部传入的回调函数。
第 1 次调用 printResult(6, 4, add) 时,我们把函数名 add 作为实参传了进去,此时在 printResult() 内部就相当于执行了下面的代码。
int (*op)(int, int) = add;
int result = op(6, 4);第 2 次调用 printResult(6, 4, multiply) 时,传入的回调函数换成了 multiply。此时内部的 op(a, b) 实际调用的就是 multiply(6, 4),输出结果为 24。
当然了,单纯计算两个整数的加减乘除,其实没必要动用回调函数,这里只是为了方便大家看清语法结构。在真实的软件开发中,回调函数专门用于 “公共流程固定、局部逻辑灵活变化” 的场景,比如给通用排序函数传入自定义的比较规则,或者在图形界面编程中指定点击按钮后要执行的处理函数。
如果小伙伴们觉得形参里的 int (*op)(int, int) 括号太多、写起来容易看花眼,也不用担心。后面学习类型别名时,我们可以使用 using 给函数指针类型起一个简短好记的名字。等到进阶阶段学习 Lambda 表达式与 std::function 时,还会接触到更直观的现代回调写法。
此外,函数指针和普通指针一样,也可以赋值为 nullptr。如果某个任务函数允许调用者传入 nullptr 来表示 “本次不需要执行额外回调”,我们在发起调用前一定要先使用 if (callback != nullptr) 检查非空,千万不能直接通过空指针调用函数。
示例 3:回调函数的空指针检查
#include <iostream>
void onSuccess() {
std::cout << "触发回调:数据保存成功!" << '\n';
}
void runTask(void (*callback)()) {
std::cout << "正在执行任务..." << '\n';
// 确认函数指针非空后再调用
if (callback != nullptr) {
callback();
} else {
std::cout << "未提供回调函数,直接结束。" << '\n';
}
}
int main() {
runTask(onSuccess); // 传入回调函数
runTask(nullptr); // 传入空指针
return 0;
}运行结果如下。
正在执行任务...
触发回调:数据保存成功!
正在执行任务...
未提供回调函数,直接结束。分析:
void (*callback)() 表示指向一个没有参数、返回值类型为 void 的函数的指针。
第 1 次调用 runTask(onSuccess) 时,callback 指向 onSuccess() 函数,非空检查通过并顺利触发回调。第 2 次调用传入 nullptr 时,函数通过 if (callback != nullptr) 识别出空指针并安全跳过,避免了调用空指针引发的程序崩溃。
