回调函数的实质 —— 什么是回调函数,为什么要使用回调函数

在 C/C++ 里面,函数的概念很好理解,就是把某个任务独立出来,封装在一起,然后给它取个名字,它可以有参数和返回值。那么,回调函数是个什么鬼呢?它和函数到底有何异同?既然已经有了函数,为啥还非要生出个回调函数来?想必,小伙伴们在刚碰到这个概念的时候,都会被这些问题困扰。网上搜一搜,有很多相关的材料,但是未必透彻。我觉得要真正理解一个概念,必须要先理解它存在的意义,也就是它为什么要存在,它能带来什么方便之处。在这一点上 C++ Primer 这本书写的还是比较到位的。仔细阅读之后,我把自己的心得写下来,供大家参考。

首先,回调函数也是函数,就像白马也是马一样。它具有函数的所有特征,它可以有参数和返回值。其实,单独给出一个函数是看不出来它是不是回调函数的。回调函数区别于普通函数在于它的调用方式。只有当某个函数(更确切的说是函数的指针)被作为参数,被另一个函数调用时,它才是回调函数。就像给你一碗饭,你并不能说它是中饭还是晚饭一样,只有当你在某个时候把它吃掉了你才明确它是中饭还是晚饭(这个比喻貌似有点挫。领会精神就好,哈哈)。

那么问题来了,为什么我们要把函数作为参数来调用呢,直接在函数体里面调用不好吗?这个问题问的好。在这个意义上,“把函数做成参数” 和 “把变量做成参数” 目的是一致的,就是以不变应万变。形参是不变的,而实参是变的。唯一不同的是,普通的实参可以由计算机程序自动产生,而函数这种参数计算机程序是无法自己写出来的,因为函数本身就是程序(要是程序可以写程序的话那就是超级人工智能了),它必须由人来写。所以对于回调函数这种参数而言,它的 “变” 在于人有变或者人的需求有变。

C++ Primer 里面举了个例子就是排序算法。为了使排序算法适应不同类型的数据,并且能够按各种要求进行排序,机智的人类把排序算法做成了一个模版(在标准模版库 STL 里),并且把判断两个数据之间的 “大小”(也可以是 “字节数”,或者其他某种可以比较的属性)这个任务(即函数)当成一个参数放在排序算法这个函数的参数列表里,而把它的具体实现就交给了使用排序算法的人。这个判断大小的函数就是一个回调函数。比如我们要给某个 vector 容器里面的单词进行排序,我们就可以声明一个排序算法:

 
  1. void stable_sort(vector<string>::iterator iterBegin, vector<string>::iterator iterEnd,

  2. bool (*isShorter)(const string &, const string &));

其中前面两个是普通参数,即迭代器(用于标记 vector 容器里面元素的位置),而第三个参数 isShorter 就是回调函数。根据不同需求 isShorter 可以有不同的实现,包括函数名。比如:

 
  1. bool myIsShorter(const string &s1, const string &s2)

  2. {

  3. return s1.size()<s2.size();

  4. }

  5. stable_sort(words.begin(),words.end(),myIsShorter);

根据需求你也可以换一种方式来实现。注意,在传递 myIsShorter 这个参数时,只需写函数名,它代表函数指针。后面绝对不能加 () 和参数,绝对不能加 () 和参数,绝对不能加 () 和参数!因为那样是调用函数的返回值!两者天壤之别!在 stable_sort 运行时,当遇到需要比较两个单词的长短时,就会对 myIsShorter 进行调用,得到一个判断。在调用时,还必须把两个单词传递给 isShorter 供 isShorter 调用。所以说 stable_sort 调用了 myIsShorter,而 myIsShorter 又调用了 stable_sort 给它的单词。它们相互调用。这就是 “回调” 这两个字的含义!

虽然说形参不变,实参可变,以不变应万变。但是作为实参有一点还是不能变的,那就是实参的数据类型不能变。比如 void foo (int i) 这个函数里的参数 i 可以取 1 也可以取 2,但是它必须是整型的。同样的,回调函数这种参数的类型也不能变。而函数的类型是由函数的参数类型和返回值类型决定的。比如前面提到的排序算法里面,isShorter 这个回调函数的参数必须是两个 const string 类型,返回值必须是 bool 类型。所以在写回调函数时还是不能太任性,必须要查看一下调用该回调函数的函数的声明。[这里插播一段广告。假如回调函数本应该只有两个参数的,但是我想让它更普适一点,我想传三个参数给它,那怎么办呢?为了解决这个问题,C++ 标委又发明了 lambda 表达式和 bind 函数这两种方法。欲知详情请戳这里和这里。]

总之,所谓回调函数就是把函数当作参数使用。目的是使程序更加普适(正如活字印刷,把可能会 “变” 的字一个个分离开来,这样就可以任意组合,重复利用)。一般情况下,一个人的小规模程序用不着这种普适性,除非你想把它做成工具箱(比如游戏引擎),供他人使用。

其实实现这种普适性还有其他方法,比如对虚函数进行重写(或者用纯虚函数。Objective C 里面所有函数都是虚函数,而协议相当于纯虚函数)。这样同一个函数就可以有不同的实现。不同的合作者之间就可以通过这种虚函数 “协议” 进行合作。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注