C++ 回调函数

什么是回调函数

举一个例子

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

这种把函数作为一个参数传递进另一个函数的函数叫做回调函数。被传入函数的函数叫做中间函数。下面实现了一个简单的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()

如何写形参表

函数指针

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

// 函数返回值类型 (*指针名)(函数形参表)
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;

std::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传引用。

posted @ 2025-02-23 20:03  PHarr  阅读(22)  评论(0)    收藏  举报