C++ 回调函数

1|0什么是回调函数


举一个例子

你入住一家酒店,这个酒店前台提供了叫醒服务,你可以自定义叫醒时间和叫醒服务。对于前台来说,叫醒时间只是变量,但是叫醒服务可以认为是一个函数。

这种把函数作为一个参数传递进另一个函数的函数叫做回调函数。被传入函数的函数叫做中间函数。下面实现了一个简单的python例子

# 回调函数 def double(x) : return x * 2 def quadruple(x) : return x *4 # 中间函数 def getOddNum(x, getEvenNum) : return getEvenNum(x) # 起始函数 def main() : k = 1 print("k = ", k) print("2 * k + 1 = ", getOddNum(k, double)) print("4 * k + 1 = ", getOddNum(k, quadruple)) if __name__ == "__main__" : main()

2|0如何写形参表


2|1函数指针


当我们定义一个函数时,函数的变量名就是一个函数指针,但是我们如何定义函数指针。

// 函数返回值类型 (*指针名)(函数形参表) void (*fun1)(); // fun1 指向 void f() int (*fun2)(int); // fun2 指向 int f(int a) int *(*fun3)(int, int *)// fun3 指向 int* f(int a, int *b)

初始化函数指针

int add(int x, int y) { return x + y; } // 定义时赋值 int (*f1)(int, int) = add; // 先定义 再赋值 int (*f2)(int, int); f2 = add; // 列表初始化 int (*f3)(int, int){add};

简化定义

using FunPtrType = int(*)(int,int); //typedef int(*FunPtrType)(int, int); FunPtrType f1 = add, f2 = add, f3 = add;

2|2std::function


std::function是一个通用的函数包装器,可以存储可调用对象,如函数指针、lamda表达式、bind表达式、仿函数,类成员函数、类成员变量。使用std::function可以实现函数回调目的。

函数指针

// 回调函数 int Add(int x, int y) { return x + y; } void func(int x, int y, std::function<int(int, int)> op) { std::cout << op(x, y) << std::endl; } int main() { func(1, 3, Add); func(2, 4, Add); }

既然可以用std::function实现,也就可以用模板推到出这个类型

// 回调函数 int Add(int x, int y) { return x + y; } template<class T> void foo(int x, int y, T op) { std::cout << op(x, y) << std::endl; } int main() { foo(1, 3, Add); }

lambda

void func(int x, int y, std::function<int(int, int)> op) { std::cout << op(x, y) << std::endl; } template<class T> void foo(int x, int y, T op) { std::cout << op(x, y) << std::endl; } int main() { auto Add = [](auto x, auto y) { return x + y; }; func(1, 3, Add); func(2, 4, Add); foo(3, 5, Add); }

同样也可以用模板推到出来。

仿函数

当用户定义的类重载了函数调用运算符operator()时,它就成为了函数对象类型,这样的一个类就被称为仿函数(functor)。下面是一个简单的例子。

仿函数通常不需要实现构造函数和析构函数。

struct Calc { double a, b; double operator()(double x) const { return a * x + b; } }; int main() { Calc f{2, 1}, g{-1, 0}; // f(x) = 2x + 1, g(x) = -x std::cout << f(1) << " " << g(2) << std::endl; }

实现回调函数

// 回调函数 template<typename T> struct Add { T operator()(T x, T y) const { return x + y; } }; // 中间函数 void func(int x, int y, std::function<int(int, int)> op) { std::cout << op(x, y) << std::endl; } int main() { func(1, 3, Add<int>()); func(2, 4, Add<int>()); }

当然了,由于这里的Add是一个类,因此也我们可以直接用模板表示

// 函数对象 template<typename T> struct Add { T operator()(T x, T y) const { return x + y; } }; // 中间函数 template<class T> void foo(int x, int y, T op) { std::cout << op(x, y) << std::endl; } int main() { foo(1, 3, Add<int>()); }

std::bind

std::bind可以看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。std::bind主要有以下两个作用:

  • 将可调用对象和其参数绑定成一个仿函数
  • 只绑定部分参数,减少可调用对象传入的参数

基础的使用方法如下

auto new_f = std::bind(f, args);

f必须是可调用对象,args是参数列表,对于未绑定的参数可以可以用std::placeholders的占位符_1,_2,_3等替换。

int main() { auto liner = [](double a, double b, double x) { return a * x + b; }; auto calc = std::bind(liner, 2, 3, std::placeholders::_1); // 计算 2x + 3 auto add = std::bind(liner, 1, std::placeholders::_1, std::placeholders::_2);//计算 x + y std::cout << calc(2) << " " << calc(4) << std::endl; std::cout << add(1, 2) << " " << add(3, 4) << std::endl; }

std::bind表达式实现回调函数的方法与上述类似,就不在赘述了。

我们要明确一点,std::bind默认是传值,并不是传引用,因此会出现一些特殊情况。

如果std::bind需要绑定静态成员函数和普通函数一样。但如果要绑定非静态成员函数则有一点不同,因为非静态成员函数还有this指针。具体实现看下面的例子。

struct A { std::string s; void print() { cout << s << "\n"; } void printX(int x) { cout << x << "\n"; } }; int main() { A a{"Apple"}, b{"banana"}; auto f1 = std::bind(&A::print, &a); auto f2 = std::bind(&A::print, &b); auto f3 = std::bind(&A::printX, &a, 123); f1(), f2(), f3(); }

首先这个例子中的&都是取地址符。

&A::print,是因为 C++ 中,成员函数指针必须通过 &ClassName::MemberFunction 的形式显式获取。

再看&a,为什么需要写a?因为在std::bind在绑定成员函数时,必须要知道调用该函数的对象。

为什么需要取地址符。其实这里并不一定需要取地址。只是如果只写a就是传值,会触发拷贝构造。

我们看下面的例子。

struct A { int x{}; void print() { cout << "x = " << (++x) << "\n"; }; }; int main() { A a, b; auto f = std::bind(&A::print, a); auto g = std::bind(&A::print, &b); f(), f(), f(), a.print(); g(), g(), g(), b.print(); }

我们运行就会发现,a.pirnt()的结果时x = 1,而b.print()的结果是x = 4。这是因为f在绑定时会触发拷贝构造,因此f()就不会修改a.x的值。而g绑定的对象就是b

当然了对于这里的&b也可以用std::ref()代替。

auto g = std::bind(&A::print, std::ref(b));

请注意,这里的形参实际上是一个指针类型,因此可以用std::ref代替。但并不等于&可以代替std::ref。比如下面这种情况。

void print(int &x) { cout << "x = " << (++x) << "\n"; }

正确的绑定应当是

int main() { int x{}; auto f = std::bind(print, std::ref(x)); f(), f(), f(); }

如果这里使用了&x就会导致类型不匹配。

再提到另一点,如果需要std::bind绑定仿函数有两种方法。

struct A { void operator()(int x) { cout << "x = " << x << "\n"; } }; int main() { A a; auto f = std::bind(A(), 123); // 通过构造函数,构造对象 auto g = std::bind(a, 456); // 直接绑定对象 f(), g(); }

当然了,如果要保留对象的状态,也可采用std::ref传引用。


__EOF__

本文作者PHarr
本文链接https://www.cnblogs.com/PHarr/p/18732716.html
关于博主:前OIer,SMUer
版权声明CC BY-NC 4.0
声援博主:如果这篇文章对您有帮助,不妨给我点个赞
posted @   PHarr  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示