跟我学C++高级篇——回调函数及应用

222 篇文章 92 订阅

一、回调函数

什么是回调函数?顾名思意,回调函数就是调用方被(被调用方)调用,有点绕口啊。一般的函数调用,都是一方向另一方发起调用,然后得到调用的结果。一般情况下,回调函数通过参数传递到指定回调的场景下。例如下面的代码:

int A(){return 5;}
int B(){return A();}
int main()
{
  int ret = B();
  std::cout<<"call result is:"<<ret<<std::endl;
}

可在某些情况下,B调用A函数时,A函数需要一个较长的时间来处理或者A函数中有一种行为需要在某种情况下触发通知A函数,但B函数又不能在此等待。这象不象是异步处理的情况?但异步只是其中的一种情况。这时候儿该如何处理呢?最简单的就是让A函数执行完成后再通知B函数不就可以了。OK,非常好。但如何通知呢?这方法可就多了,回调函数就是一种方式。
可以在调用A时,把需要处理的通知定义为一个函数,假设为与A同一模块内的C函数,跟随调用A时注册到B内,让B在允许的情况下调用这个函数C,而在C函数内处理相关的事宜,如下代码:

typedef void(*CB)(int) ;//函数指针
void C(int d){std::cout<<"call back value:"<<d<<std::endl;}
int A(CB b){
  b(5+5);
  return 5;
}
int B(){
  int r = A(C);
  return r;
}
int main()
{
  int ret = B();
  std::cout<<"call result is:"<<ret<<std::endl;
}

如果把B、C两个函数划分为一个模块1,把A函数划分成另外一个模块2,那么模块1调用模块2后,模块2又调用模块1(这个在线程里更容易理解).这就是调用方被调用。这样容易理解,如果较真非要是B函数调用A函数再返回调用B函数,这就需要处理一下内部的逻辑。有兴趣可以自己搞一搞,有点类似于递归,一定要有一个终结的点。
所以说一般情况下,回调函数是一个函数指针。当然,在c++中提供了一些函数对象的封装,可以把它们从宏观上都划到函数指针一类。
在C这类面向过程开发的语言中,回调函数是非常重要的一个环节。有过嵌入式开发经验的知道,一些官方提供的代码例程中会有一个特殊的指针数组,用来处理各种中断或者异常的回调。在Linux的内核中,也可以看到类似的代码。
回调函数和普通函数从本质上没有区别,这一点大家一定要明白,它也有类似的调用约定方式和相关属性等。可以这样理解,回调函数只是功能层次上的应用的不同,而与本身的定义无关,它就是一种函数。

二、回调函数的优势

在上面基本弄明白了,什么是回调函数,回调函数初步的应用。那么,为什么要使用回调函数呢?它有什么好处?最重要的一点,就是使用回调函数可以安全可靠的进行异步编程。由于其使用函数参数传递,那么这种回调的场景应用就变得非常灵活。同样,由于回调可以进行触发性管理,而不用将两个模块紧密的耦合在一起。达到了设计上的解耦。比如在常见的场景中,如果想实现A与B两个类间的互动,一般是互相包含头文件,然后在指定的情况下进行互相调用。可这样,就会让双方互相包含各自的头文件,紧密的耦合在一起。如下:

//a.h
class A{
  void Display();
  void WorkerA();
};

//a.cpp
#include "b.h"
B b;
void A::Display(){std::cout<<"A Display!"<<std::endl;}
void A::WorkerA(){b.Display();}

//b.h
class B{
  void Display();
  void WorkerB();
};
//b.cpp
#inlcude "a.h"
A a;
void B::Display(){std::cout<<"B Display!"<<std::endl;}
void B::WorkerB(){a.Display();}

而如果使用使用回调函数,只需要注册一下B或A的相关函数到对方即可。

三、回调函数的应用缺点

事件往往是有多面性的,回调函数有优势,则必有劣势。最常见的就是人们常说的“回调地狱”。大家可能听说过DLL地狱,那么回调地狱也类似。可能国外把一些容易搞成大问题的事件都叫地狱。这和中国人动不动就叫老天爷估计没啥区别。
回调地狱一般是指在异步编程中,回调函数的深层嵌套调用。即,A->B->C->D->E,但返回来可能是E->D->F->C->B->A,中间莫名多了一个工序,即使没有多一道工序,直接返回的话,也是很难阅读代码和定位问题的。大家可能有这种经验,特别是在读Linux内核的代码时,阅读工具往往无法跟踪函数指针的流程,而一般来说,函数指针往往多是回调函数。
回调函数的嵌套带来的复杂性,往往是后续维护者和学习者头大。那种线性的单纯的前进和回退的还好说一些,往往一些回调会拐个弯进行通知一下,然后再搞回来,这会让很多人在不懂得业务逻辑的情况下,不明所以。这也是异步回调难于调度的原因。如果中间再增加一些基于线程同步的通信,则更是复杂的难于理解。
那么问题已经出现,如何解决这种回调地狱的问题呢?仍然是编程的解决思想之一,引入中间层。但这个中间层,目的不是进一步回调,而是把回调的深度打平,既然深度大,就把一些不必要的深度去除即可。这种方式最简单的方法就是使用设计模式中的链式调用。这和c++11后的std::promis的机制类似。另外一种是继续抽象,增加一种异步机制,将复杂的异步回调隐藏的框架内部。

四、回调函数与异步编程

一般来说,异步编程和回调基本是一种难兄难弟。焦不离孟,孟不离焦。异步是相对于同步而言,而同步大家都明白,就是共同步进协调完成工作。而异步而不是,它是你干你的我干我的,大家谁干完就通知一声。等最后大家都完成了,这事儿也就完了。
同步更适合一些批量相同类似的工作的完成。比如工厂打螺丝,就可以流水线同步作业。因为每个熟练的工人,其打螺丝的速度基本是同步的。而农民的庄稼的收割则是异步的,庄稼不可能同时成熟,也不可能每块地大小一样。那么收割机就不能同步的安排作业,很可能割完A家的村南的一块地,又去B家割村东的,然后才可能回来割A家的村面另外一块地。
异步更有点人情社会的味道而同步更接近工厂生产的流程。
回调函数在设计模式中应用也非常广泛,如观察者模式、职责链模式等都可以在底层应用回调函数来实现。需要特别注意的是,在使用Lambda表达式作为回调函数时,由于Lambda表达式和闭包概念的密切相关性,特别是在处理对闭包外部变量的处理时,要千万谨慎。

五、例程

回调函数,不单纯限于函数,只要是具有函数性质的对象、表达式等都可以归为回调函数一类。说这些是因为在c++中为了安全,将函数进行了封装,既有普通的函数,也有Lambda,更有传统的仿函数(functor),还有std::mem_fn和std::function。至于还会发展出什么来,估计没人知道。
下面看相关的例程:

#include <functional>
class SerialCom
{
public:
  void initSerialCom(std::function<void(byte*,int)> func){
    this->m_onPackaged = func;
  }
private:
  int packageDoWith(byte *buf,int len){
    ...
    if (isEnd){
      this->m_onPackaged(buf_,len_);
    }
    ...
  }
private:
  std::function<void(byte*,int)> m_onPackaged = nullptr;
};

void OnPackaged(byte*buf,int len){
  ......
}
int main(){
  SerialCom scom;
  scom.initSerialCom(OnPackaged);
  return 0;
}

这个例程很简单,是一个串口通信的处理过程。在完成组包后将完整包回调给上层应用。

六、总结

之所以把回调函数放到高级篇,不是说回调函数本身有多么高级而是回调函数的应用是非常灵活的,用得精妙之处,完全可以放到高级的应用中。一个简单的回调函数,最大的优势是可控和安全。但回调函数不是没病的山梨儿,大家还是要斟酌考虑,不要滥用。

  • 20
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
回调函数是指在程序执行过程中,将某个函数的地址作为参数传递给另一个函数,在特定条件下由后者调用执行。在C语言中,回调函数通常是通过函数指针来实现的。 以下是一个简单的C语言例程,演示了回调函数的使用: ```c #include <stdio.h> // 回调函数的定义 typedef int (*CompareFunc)(int, int); // 排序函数,使用回调函数来比较两个数的大小 void sort(int arr[], int size, CompareFunc compare) { for (int i = 0; i < size - 1; i++) { for (int j = 0; j < size - i - 1; j++) { if (compare(arr[j], arr[j + 1]) > 0) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } // 比较函数,用于从小到大排序 int ascending(int a, int b) { return a - b; } // 比较函数,用于从大到小排序 int descending(int a, int b) { return b - a; } int main() { int arr[] = {5, 2, 8, 1, 9}; int size = sizeof(arr) / sizeof(arr[0]); // 使用回调函数进行从小到大排序 sort(arr, size, ascending); printf("从小到大排序结果: "); for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } printf("\n"); // 使用回调函数进行从大到小排序 sort(arr, size, descending); printf("从大到小排序结果: "); for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; } ``` 在上面的例程中,定义了一个`sort`函数,它接受一个整数数组、数组大小和一个回调函数作为参数。`sort`函数根据回调函数的比较结果来进行排序。然后,通过定义两个不同的比较函数`ascending`和`descending`,分别实现了从小到大和从大到小的排序。 在`main`函数中,首先使用回调函数`ascending`进行排序,并输出结果,然后使用回调函数`descending`进行排序,并再次输出结果。 通过回调函数,我们可以在不修改原始函数的情况下,动态地改变其行为,提高代码的灵活性和复用性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值