C++基础整理(8)之函数指针与回调函数
注:整理一些突然学到的C++知识,随时mark一下
例如:忘记的关键字用法,新关键字,新数据结构
C++ 的函数指针/回调函数
提示:本文为 C++ 中 函数指针、回调函数 的用法和举例
一、函数指针
在C++中,函数指针是一个指向函数的指针,它存储了函数的地址而不是函数的返回值。函数指针的主要用途是允许程序员在运行时动态地调用不同的函数,或者将函数本身作为参数传递给其他函数。
1、函数指针的声明和初始化
声明函数指针的格式是return_type (*funPtr)(paras_type)
,即用(*函数指针的名字)
代替原函数定义时给函数取的名字,然后参数列表中只写参数的数据类型。
以下是一个简单的定义声明一个指向无参数、无返回值函数的指针:
void (*functionPtr)();
functionPtr = myfun;//初始化这个函数指针,赋以已经定义的函数myfun
如果函数接受参数或返回值,你需要在函数指针的定义中包括这些参数和返回类型。
例如,指向接受一个整数参数并返回整数的函数的指针的定义:
int (*functionPtrWithInt)(int);
functionPtrWithInt = myfun2;//初始化这个函数指针,赋以已经定义的函数myfun2
注意,以上在C++引入<functional>
库后也可以写成:std::function<int(int)> functionPtrWithInt = myfun2
;这个功能更加灵活。
2、使用 typedef 定义函数指针的类型
可以将函数指针当成一个新的数据类型,这时仅需将函数指针的声明格式前面加一个 typedef 即可,注意这时括号内的FunctionPtr不再是一个函数指针,而是一个函数指针的类型。
以下是个示例可以比较一下与刚才函数指针初始化的区别。
// 使用typedef定义函数指针类型
typedef int (*FunctionPtr)(int);
// 用这个类型声明一个函数指针变量
FunctionPtr fPtr;
// 初始化这个函数指针,赋以已经定义的函数myfun2
fPtr = myfun2;
注意以上写法在C++中也可以用关键字 using 代替:
// 使用using定义函数指针类型
using FunctionPtr = int (*)(int);
注意:int (*s[N])(int)
表示一个函数指针的数组,N个元素都是函数指针,且每个指针都指向某个参数是int,返回是int的函数(不一定是同一个函数)。
3、函数指针的用途
(1)回调函数:在异步编程或事件驱动编程中,我们常常需要将函数指针作为回调,以便在某个事件发生时调用特定的函数。
(2)将函数作为参数传递,因为函数名在大多数情况下会被解释为指向该函数的指针,所以能够直接将函数名作为参数传递给接受函数指针的函数。
(3)函数表:可以创建函数指针数组(或称为函数表),以在运行时根据某些条件选择并执行不同的函数。
(4)高级功能:如虚函数表(在C++的对象模型中用于实现多态)、信号处理函数等内部机制都依赖于函数指针。
4、通过函数指针将函数作为参数传递
下面是一个简单的例子,展示了如何将函数作为参数传递:
// 定义一个函数,它接受一个函数指针作为参数
void callFunction(void (*func)()) {
func(); // 调用传入的函数
}
// 定义一个普通的函数
void sayHello() {
std::cout << "Hello from sayHello!" << std::endl;
}
int main() {
// 将函数名(它隐式地转换为函数指针)作为参数传递给callFunction
callFunction(sayHello);
return 0;
}
传参也可以使用 typedef 写法:
// 定义一个指向无参数且返回类型为void的函数的指针类型别名
typedef void (*FunctionPointer)();
// 定义一个函数,它接受一个FunctionPointer类型的参数
void callFunction(FunctionPointer func) {
func(); // 调用传入的函数
}
在本例中,callFunction 函数接受一个无参数、无返回值的函数指针作为参数,并在其函数体内调用这个函数。在 main 函数中,直接传递了 sayHello 函数的名称给 callFunction,而不需要显式地获取其地址(因为函数名在上下文中被解释为地址)。
对于带参数的函数,你需要确保传递函数指针时参数类型和数量与定义时一致。同样地,接收函数指针的函数也需要有匹配的参数类型和数量来正确地调用传入的函数。
5、通过函数指针调用函数
即把一个函数名字赋给参数匹配的、定义好的的函数指针,然后这个函数指针就可以当成这个函数用了。
下面给个稍微复杂的例子,一个二维向量类,拥有向量相加、数乘、点乘的三个成员方法,现在为每一个成员函数声明一个单独的成员函数指针,并分别使用它们(typedef 写法,不用 typedef 直接声明的写法略,非常简单)。把它们初始化为指向 Vector2D 类的add、scale和dot成员函数。请注意,由于dot函数返回一个标量值,它不能使用与返回向量类型值的add和scale相同的成员函数指针类型(必须参数、返回类型均匹配)。
class Vector2D {
public:
Vector2D(double x = 0.0, double y = 0.0) : x(x), y(y) {}
Vector2D add(const Vector2D& other) const {
return Vector2D(this->x + other.x, this->y + other.y);
}
Vector2D scale(double factor) const {
return Vector2D(this->x * factor, this->y * factor);
}
double dot(const Vector2D& other) const {
return this->x * other.x + this->y * other.y;
}
private:
double x, y;
};
int main() {
Vector2D vec1(1.0, 2.0);
Vector2D vec2(3.0, 4.0);
// 声明指向成员函数的指针类型
typedef Vector2D (Vector2D::*AddFunc)(const Vector2D&);
typedef Vector2D (Vector2D::*ScaleFunc)(double);
typedef double (Vector2D::*DotFunc)(const Vector2D&);
// 初始化成员函数指针
AddFunc fptr_add = &Vector2D::add;
ScaleFunc fptr_scale = &Vector2D::scale;
DotFunc fptr_dot = &Vector2D::dot;
// 使用成员函数指针调用 add 函数
Vector2D result_add = (vec1.*fptr_add)(vec2);
std::cout << "Addition: (" << result_add.x << ", " << result_add.y << ")\n";
// 使用成员函数指针调用 scale 函数
Vector2D result_scale = (vec1.*fptr_scale)(2.0);
std::cout << "Scaling: (" << result_scale.x << ", " << result_scale.y << ")\n";
// 使用成员函数指针调用 dot 函数
double result_dot = (vec1.*fptr_dot)(vec2);
std::cout << "Dot Product: " << result_dot << "\n";
return 0;
}
在这个例子中,我们为add、scale和dot分别定义了成员函数指针类型AddFunc、ScaleFunc和DotFunc。然后,我们创建了三个成员函数指针变量fptr_add、fptr_scale和fptr_dot,并将它们分别初始化为指向Vector2D类的相应成员函数。最后,我们使用这些指针来调用相应的成员函数。
请注意,当使用成员函数指针调用函数时,我们使用特殊的语法(object.*member_function_pointer)(arguments),其中object是对象实例,member_function_pointer是成员函数指针,arguments是传递给成员函数的参数列表(如果有的话)。对于add和scale,它们返回Vector2D对象,而对于dot,它返回double。
二、回调函数
回调函数(Callback Function)是允许一个函数作为参数传递给另一个函数并在需要时由后者调用的一种机制,常用于实现事件处理、异步编程、插件系统等。在C++中,除了使用函数指针将函数作为参数传递外,还有其他几种方法可以实现类似的功能:(如函数对象/仿函数、lambda表达式或std::function),这些方法提供了更加灵活和类型安全的方式来传递函数。
1、使用Lambda表达式
Lambda表达式是一种创建匿名函数对象的方法。它们可以捕获当前作用域中的变量,并定义自己的参数和返回类型。Lambda表达式本身可以作为参数传递给其他函数。关于lambda表达式
#include <functional>
void callFunction(const std::function<void()>& func) {
func(); // 可传入Lambda表达式
}
int main() {
auto lambda = []() {
std::cout << "Hello from Lambda!" << std::endl;
};
callFunction(lambda); // 传递Lambda表达式
return 0;
}
2、使用std::function
std::function是一个通用的、多态的函数封装器,它可以存储、复制和调用任何可调用的目标(函数、Lambda表达式、函数对象等)。使用std::function可以将函数作为参数传递,并提供类型擦除和更灵活的函数调用语法。
#include <functional>//必须包含这个库
void printHello() {
std::cout << "Hello from printHello!" << std::endl;
}
void callFunction(const std::function<void()>& func) {
func(); // 调用传入的函数对象
}
int main() {
callFunction(printHello); // 传递函数名
// 或者使用Lambda表达式
callFunction([]() {
std::cout << "Hello from Lambda!" << std::endl;
});
return 0;
}
3、使用函数对象(仿函数 / Functors)
函数对象(也称为仿函数或functors)是重载了operator()的类的对象。它们可以像函数一样被调用,并且可以作为参数传递给其他函数。
#include <functional>
class Greet {
void operator()() const {
std::cout << "Hello from Greet functor!" << std::endl;
}
};
void callFunction(const std::function<void()>& func) {
func(); // 调用传入的函数对象
}
int main() {
Greet greet;
callFunction(greet); // 传递函数对象实例
return 0;
}
4、使用模板
还可以使用模板来编写可以接受任何可调用对象的函数。这种方法比std::function更加轻量级,但不能保证类型安全,不推荐。
template <typename Callable>
void callFunction(Callable&& func) {
func(); // 调用传入的函数或Lambda表达式
}
int main() {
callFunction([]() {
std::cout << "Hello from Lambda in template function!" << std::endl;
});
return 0;
}
在这些方法中,std::function是最通用且类型安全的,它几乎可以封装任何可调用的目标。然而,它可能有一些性能开销,因为它提供了类型擦除。模板方法则更加高效,但牺牲了类型安全,因为它们需要在编译时确定传入的类型。Lambda表达式和仿函数则提供了更加灵活和自定义的解决方案。