目录
4. 使用 std::function 和 std::bind
(一)应用场景
在 C++ 中,回调是一种常见的设计模式,它允许将函数作为参数传递给另一个函数,从而在某个事件发生时调用该函数。回调可以用于实现各种功能,比如事件处理、异步操作、策略模式等。下面将详细介绍回调的实现方式。
(二)实现方式
1. 函数指针作为回调
在C++中,函数指针扮演着关键角色,作为一种独特的指针类型,它直接指向程序中的函数而非内存中的数据项。这种机制尤其适用于实现回调函数,它们是在某个特定条件或事件触发时,由系统或库代码自动调用的用户定义函数。换句话说,回调函数为用户提供了一个接口,允许在预设的回调点插入自定义的处理逻辑,而无需修改原有框架或库的源代码。
//回调函数
void PrintInfo()
{
for (int i = 0; i < 100; ++i) {
if (i % 10 == 0) {
cout << i << " ";
}
}
cout << endl;
}
void TestSimple() {
cout << "hello world" << endl;
}
//中间函数
void show()
{
PrintInfo();
TestSimple();
}
int main()
{
show();
return 0;
}
【说明】
- 如果不使用函数指针来实现上述代码中类似 show() 函数的功能,主要的缺点会体现在代码的灵活性:
- 如果不使用函数指针,show() 函数将只能调用一个硬编码在其中的函数。这意味着如果你想要show()函数能够展示不同的信息或执行不同的操作,你需要修改 show()函数的内部逻辑,这可能会引入错误。
上述代码通过函数指针进行实现。代码如下:
void PrintInfo()
{
for (int i = 0; i < 100; ++i) {
if (i % 10 == 0) {
cout << i << " ";
}
}
cout << endl;
}
void TestSimple() {
cout << "hello world" << endl;
}
void show(void (*Func)()) //通过函数指针方式
{
Func();
}
int main()
{
show(PrintInfo);
show(TestSimple);
return 0;
}
输出显示:
上述无参数类型,也可以传递参数进行相应的功能实现。具体如下:
//参数为int类型,返回值为int类型
typedef int (*Func)(int, int);
// 回调函数示例
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
// 使用函数指针作为回调的函数
int operate(int x, int y, Func func) {
return func(x, y);
}
int main()
{
cout << "10 + 5 = " << operate(10, 5, add) << endl;
cout << "10 - 5 = " << operate(10, 5, sub) << endl;
return 0;
}
【说明】
- 在这个例子中,
operate
函数接受三个参数:两个整数和一个函数指针; - 使用这个函数指针来调用一个回调函数,该回调函数接受两个整数作为参数并返回一个整数;
- 通过向
operate
传递不同的函数指针(如add
或sub
),我们可以在不修改operate
函数本身的情况下改变其行为。
2. 使用仿函数
仿函数重载了
operator()
,使得对象可以像函数一样被调用。函数对象可以用于实现回调,因为它们可以被存储和传递,就像普通函数指针一样,但提供了更多的灵活性和功能。
class MyFunc {
public:
void operator()() {
cout << "Hello World!!!" << endl;
}
};
void CallFunc(function<void()> func) {
// 调用回调函数对象
func();
}
int main()
{
MyFunc obj;
CallFunc(obj);
return 0;
}
输出显示:
【说明】
函数对象相较于普通函数指针有几个优势:
- 状态保持:函数对象可以包含状态(成员变量),这意味着它们可以保存和修改状态,而普通函数指针则不能。
- 灵活性:函数对象可以重载
operator()
以接受不同数量或类型的参数,提供更灵活的调用方式。 - 封装性:函数对象可以封装相关的数据和操作,使得代码更加模块化和易于管理。
3. 使用类成员函数作为回调
需要注意的是,成员函数指针与普通的函数指针不同,因为成员函数需要隐式地传递一个指向对象本身的指针(
this
指针)。因此,成员函数指针的类型需要指定其所属的类。
class MyClass {
public:
void PrintInfo() {
cout << "Hello World!!!" << endl;
}
// 成员函数指针类型声明
typedef void (MyClass::* Func)();
void CallFunc(Func ptr) {
(this->*ptr)(); // 使用成员函数指针调用成员函数
}
};
int main()
{
MyClass obj;
MyClass::Func ptr = &MyClass::PrintInfo;
// 调用成员函数指针指向的函数
obj.CallFunc(ptr);
return 0;
}
输出显示:
4. 使用 std::function 和 std::bind
从 C++11 开始,可以使用
std::function
来代替函数指针,它提供了更灵活的函数封装方式。结合std::bind
,可以创建带有预设参数的回调函数。
【优势如下】
1.函数指针的局限性:传统的函数指针只能指向具有特定签名的普通函数,不能指向成员函数或lambda表达式等。
2.手动绑定的复杂性:在没有 std::bind
的情况下,手动绑定成员函数或带有默认参数的函数到特定的上下文需要编写额外的代码,这既繁琐又容易出错。
3.类型不安全:函数指针不提供类型检查,这可能导致在运行时才发现类型不匹配的问题。
4.缺乏灵活性:函数指针不能存储lambda表达式或函数对象,这限制了回调的灵活性和功能。
5.捕获上下文的限制:使用裸函数指针无法捕获定义它们的上下文中的局部变量,这限制了回调函数的表达能力。
class MyFunc {
public:
void NumCall(int value) {
cout << "the value is : " << value << endl;
}
};
// 一个函数,接受一个std::function作为回调
void FunCall(const function<void(int)>& func, int value) {
if (func) {
func(value); // 调用回调函数,并传递参数
}
}
int main()
{
MyFunc obj;
// 使用std::bind将MyClass的成员函数与对象实例绑定,并创建一个可调用实体
auto tmp = bind(&MyFunc::NumCall, &obj, placeholders::_1);
// 调用FunCall,传递绑定后的回调函数和参数
FunCall(tmp, 10);
return 0;
}
输出显示:
除此之外,还可以使用lambda表达式作为回调,这在某些情况下更加简洁 :
(三)回调小结
回调在 C++ 中的实现方式多种多样,从简单的函数指针到现代的 std::function
和 lambda 表达式,每种方式都有其适用场景。选择合适的回调实现方式,可以让你的代码更加灵活和强大。在实际开发中,推荐使用 C++11 之后引入的特性,如 std::function
和 lambda 表达式,因为它们提供了更好的类型安全性和易用性。