• C++ 回调函数详解


    1、什么是回调函数
      回调函数本质上也是普通函数,只是调用机制有所区别——首先通过传参的形式将该函数的地址传递给其他函数,然后在其他函数中通过函数指针调用该函数。在其他函数中通过函数指针调用该函数的过程称为回调,而作为被调用的该函数则被称为回调函数。有人可能会疑惑何必多此一举,直接在其他函数中调用该函数就好,为何一定要通过指针中转。
    2、为什么需要回调函数
      这就不得不提到项目联合开发带来的必然后果——接口的兼容性问题。举个超级简单的例子:程序员小A和程序员小B联合开发一个项目,要求小A开发的函数必须为小B开发的函数提供灵活可用的接口。 假如小A小B是好朋友,他们天天在一起,而且也不考虑开发效率问题,那就串行方式一起开发好了。如下例所示,在同一个文件中,小A先负责开发Add()函数,小B后负责开发add()函数,小A需要调用小B的函数,即小A为小B提供了一个接口,通过这个接口小B可以随意通过更改add()函数,间接更改了Add()函数。 由于小A在使用小B的函数前小B还没有实现,所以他们需要先一起商量一下小B需要实现的函数名称、函数返回值、参数类型,然后小A在调用前先声明。小A在完成自己的工作后就可以休息了,然后小B就按照之前商量好的定义函数,对于函数体中具体怎么写完全由小B自由发挥了:

     1 /*test.h*/
     2 #include<iostream>
     3 using namespace std;
     4 
     5 int add(int,int);
     6 void Add(int a, int b)
     7 {
     8     cout << add(a, b) << endl;
     9 }
    10 int add(int a, int b)
    11 {
    12     return a + b;
    13 }
    14 /*main.cpp*/
    15 #include"test.h"
    16 void main()
    17 {
    18     Add(1, 2);
    19 }

    延续上文的故事,但现在场景变了。为了提高效率需要小A和小B并行操作,但工作类容没有变。显然他们需要在两个文件中完成各自的任务。小A同样需要声明小B的函数。对于现实中的项目开发而言,这种方式最为常见。

     1 /*test.h*/
     2 #pragma once
     3 #include<iostream>
     4 using namespace std;
     5 int add(int, int);
     6 void Add(int a, int b)
     7 {
     8     cout << add(a, b) << endl;
     9 }
    10 /*test.cpp*/
    11 #include"test.h"
    12 int add(int a, int b)
    13 {
    14     return a + b;
    15 }
    16 void main()
    17 {
    18     Add(1, 2);
    19 }

      看到这里你肯定回想,这也太简单了,完全没有啰嗦的必要,其实我只是想一步步引出为什么要有回调函数的存在。通过上面的例子我们不难发现,小A为小B提供了接口,使得小B定义的函数能够在合适的时机被调用,这里必须有一个前提是小A和小B要提前商量好小B所需实现的函数的名称、返回值、参数列表。 而现在我们延续上面的故事,但场景又变了,小A和小B完全不认识,甚至小A不知道小B要用自己的接口,那么再按照上面的思路无论对小A还是小B来说都不那么友好了。首先对于小A来说,每次调用小B定义的函数前都需要声明,很是麻烦。而对于小B来说,由于小A定义的函数函数体对于小B来说是封装看不见的,小B对于自己定义的函数如何传到小A定义的函数中去很不直观,而且小B定义的函数名称、返回值类型、参数列表必须与小A在之前做出的声明保持完全一致。 因而,回调函数闪亮登场。如下例所示,小A不再需要每次调用小B定义的函数之前都要进行声明,小A只需要提供一个函数指针来接收小B传过来的函数的地址,而不用管函数名是什么。小A用这个指针就可以直接调用小B定义的函数。小B可以自己起函数名,只是返回类型和参数列表必须和小A的声明保持一致。

     1 /*test.h*/
     2 #pragma once
     3 #include<iostream>
     4 using namespace std;
     5 void Add(int(*callbackfun)(int, int), int a, int b)
     6 {
     7     cout << callbackfun(a, b) << endl;
     8 }
     9 /*test.cpp*/
    10 #include"test.h"
    11 int add(int a, int b)
    12 {
    13     return a + b;
    14 }
    15 void main()
    16 {
    17     Add(add, 1, 2);
    18 }

    上面例子可以看出,回调函数必须通过函数指针进行传递和调用,为了简化代码,一般会将函数指针起个别名,格式为:

    typedef 返回值类型 (*指针名) (参数列表)

    则上面的例子部分代码可以修改如下。无论是pf(a,b)还是(*pf)(a,b)都是可以的,我打印了pf和*pf,发现它们都表示的是同一函数的地址。

    1 typedef int(*callbackfun)(int, int);
    2 void Add(callbackfun pf, int a, int b)
    3 {
    4     cout<<pf<<endl;
    5     cout<<*pf<<endl;
    6     cout << (*pf)(a, b) << endl;
    7     cout << pf(a, b) << endl;
    8 }

      回调函数规避了必须在调用函数前声明的弊端,而且能够让用户直观地感受到自己定义的函数被调用。小A需要在声明函数指针时规定参数列表,小B在定义回调函数时需要与小A声明的函数指针保持相同的参数列表。当然小B可以自己决定在函数体中是否使用这些参数,前提是小A会提前为回调函数形参设置默认值。

     1 /*test.h*/
     2 #pragma once
     3 #include<iostream>
     4 using namespace std;
     5 typedef int(*callbackfun)(int, intint);
     6 void Add(callbackfun pf, int a=0, int b=0,int c=0)
     7 {
     8     cout << pf(a, b,c) << endl;
     9 }
    10 /*test.cpp*/
    11 #include"test.h"
    12 int add(int a, int b,int c)
    13 {
    14     return a + b;
    15 }
    16 void main()
    17 {
    18     Add(add, 1, 2);
    19 }

    3、有哪些函数可以做回调函数
      可以做回调函数的函数在C++中目前我遇到过的有两种,第一种是C语言风格的函数,第二种是静态成员函数。第一种上面已经详细介绍过了,就不再赘述。第二种我在此做详细说明。
    3.1静态成员函数做回调函数
      众所周知,类的非静态成员函数的参数列表中隐含了一个this指针,当用类的对象访问类的非静态成员函数时,C++编译器会将this指针指向该对象,从而保证了函数体中操纵的成员变量是该对象的成员变量。即使你没有写上this指针,编译器在编译的时候自动加上this指针。 而在调用回调函数的函数中,它会提供接受回调函数地址的函数指针,该函数指针严格规定了参数列表。由于this指针的存在,非静态成员函数的参数列表和函数指针的参数列表参数个数无法匹配。如下例所示,函数指针callbackfun有两个参数,非静态成员函数隐含this指针从而有三个参数,显然不匹配。

     1 class AddClass {
     2 private:
     3     int a, b;
     4 public:
     5     int add(int aa, int bb);
     6 };
     7 int AddClass::add(int aa, int bb) {
     8     a = aa;
     9     b = bb;
    10     return a + b;
    11 }
    12 void Add(int(*callbackfun)(int, int), int a, int b) {
    13     cout << callbackfun(a, b) << endl;
    14 }
    15 void main()
    16 {
    17     Add(AddClass().add, 1, 2);
    18 }

    当然如果你执着于使用非静态成员函数,就只能通过普通全局函数做中转,如下例所示。虽然能够实现,但看起来总是傻傻的。

     1 class AddClass {
     2 private:
     3     int a, b;
     4 public:
     5     int add(int aa, int bb);
     6 };
     7 int AddClass::add(int aa, int bb) {
     8     a = aa;
     9     b = bb;
    10     return a + b;
    11 }
    12 void Add(int(*callbackfun)(int, int), int a, int b) {
    13     cout << callbackfun(a, b) << endl;
    14 }
    15 int myAdd(int aa, int bb) {
    16     AddClass a;
    17     return a.add(aa,bb);
    18 }
    19 void main()
    20 {
    21     Add(myAdd, 1, 2);
    22 }

      类的静态成员函数属于类,为所有对象所共享,它没有this指针,因此这里我们采用静态成员函数作为回调函数。但这里我们又遇到了一个问题,就是静态成员函数是无法访问类的非静态成员的,因此如果用户有访问非静态成员的需求,那么需要在静态成员函数的参数列表中做一点小小的修改。在形参列表中加入万能指针void*作为一个形参,然后在函数体中再进行类型强转。

     1 /*test.h*/
     2 #include<iostream>
     3 using namespace std;
     4 typedef int(*callbackfun)(int, int, void*);
     5 void Add(callbackfun pf, int a, int b,void* p) {
     6     cout << pf(a, b,p) << endl;
     7 }
     8 /*test.cpp*/
     9 #include<test.h>
    10 class AddClass {
    11 private:
    12     int a, b;
    13 public:
    14     static int add(int aa, int bb,void* temp);
    15 };
    16 int AddClass::add(int aa, int bb,void* p) {
    17     AddClass* temp = (AddClass*)p;
    18     if (temp) {
    19         temp->a = aa;
    20         temp->b = bb;
    21     }        
    22     return temp->a + temp->b;
    23 }
    24 void main()
    25 {
    26     AddClass* a = new AddClass;
    27     Add(AddClass::add, 1, 2,a);
    28 }

    4、小结
    ①回调函数本质其实是对函数指针的一种应用,上面的例子都比较简单,还没有完全体现回调函数的威力。

    ②回调函数是一种设计系统的思想,能够解决系统架构中的部分问题,但是系统中不能过多使用回调函数,因为回调函数会改变整个系统的运行轨迹和执行顺序,耗费资源,而且会使得代码晦涩难懂。

    ③C++ STL中有大量使用回调函数的例子。如下例所示,遍历函数for_each()中的lamda表达式就是一个回调函数,当然我们也可以像上面那样定义全局函数或静态成员函数然后将地址传进去。

     1 #include<iostream>
     2 #include<vector>
     3 #include<algorithm>
     4 using namespace std;
     5 void main()
     6 {
     7     vector<int> v{ 1,4,6,5,3 };
     8     for_each(v.begin(), v.end(), [=](int val) {
     9         cout << val << " ";
    10     });
    11 }

    最后,我只是站在应用者的角度提出了我对于回调函数的一点浅显的见解,可能还有许多更高端更系统的观点,如果有能指出我的不足,将不胜感激。

  • 相关阅读:
    conda更换为清华镜像源
    NVDIA的GPU驱动升级
    Windows使用nvidia-smi查看GPU信息
    查询GPU是否支持CUDA
    PyTorch教程【一】Pytorch环境的配置及安装
    JAVA基础篇—继承
    SQL
    随机数
    hdu 4751 Divide Groups 二分图
    hdu 4126 Genghis Khan the Conqueror hdu 4756 Install Air Conditioning 最小生成树
  • 原文地址:https://www.cnblogs.com/ybqjymy/p/16531689.html
Copyright © 2020-2023  润新知