热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

关于C++回调函数(callback)精简且实用

1关于回调函数1.1定义回调函数的定义,可以很严(复)谨(杂),也可以很简(随

1 关于回调函数


1.1 定义

回调函数的定义,可以很严(复)谨(杂),也可以很简(随)单(意)。其实与其研究定义,还不如讨论为什么需要回调函数,回调函数能干些啥。
在我看来,回调函数不是在函数的定义上区别于普通函数,而是在调用的方式有区别,因为归根到底,他们都是代码中芸芸众生的普普通通的函数,即“回调函数”的重点在“回调”这两个字。
以花钱为例,花钱买衣服叫消费,花钱买股票叫投资,都是花钱,但是方式不同,意义也不同。

下图列举了普通函数执行和回调函数调用的区别。


  • 对于普通函数,就是按照实现设定的逻辑和顺序执行。
  • 对于回调函数,假设Program A和Program B分别有两个人独立开发。回调函数Fun A2它是由Program A定义,但是由Program B调用。Program B只负责取调用Fun A2,但是不管Fun A2函数具体的功能实现。
    image.png

1.2 为什么需要回调函数

因为有这样的使用场景,Fun A2只有在 Fun B1调用时才能执行,有点像中断函数的概念。那可能会有人问,在Program A中不断去查询Fun B1的状态,一旦它被执行了,就让Program A自己去执行Fun A2不行吗?如果你有这样的疑问,说明你已经入门了。
答案是“可以”,但是这样实现的方案不好。因为整个过程中Program A一直都在查询状态,非常耗资源,查询频率高了费CPU,查询频率低了实时性保证不了,Fun B1都执行好久了你才反应过来,Fun A2的执行就会明显晚于Fun B1了。
正因为如此,回调函数才登上了舞台。

如果依然一知半解,可以看这位知乎答主的回答。回调函数(callback)是什么?


2 如何实现函数回调

函数的回调并不复杂,把 Fun A2的函数的地址/指针告诉Program B就可以了。
其实我们在这里要讨论的是在C++中,常用回调函数的类型。
image.png

得到函数的地址是其中一个关键步骤。
普通和函数和类的静态成员函数都分配有确定的函数地址,但是类的普通函数是类共用的,并不会给类的每一个实例都分配一个独立的成员函数,这样就太浪费存储资源了。所以类的非静态函数作为回调函数是有区别的,也是这篇文章想要讨论的重点。

不过我们还以一步一步来,从简单的开始。


2.1 普通函数作为回调函数

#include void programA_FunA1() { printf("I'am ProgramA_FunA1 and be called..\n"); }void programA_FunA2() { printf("I'am ProgramA_FunA2 and be called..\n"); }void programB_FunB1(void (*callback)()) {printf("I'am programB_FunB1 and be called..\n");callback();
}int main(int argc, char **argv) {programA_FunA1();programB_FunB1(programA_FunA2);
}

执行结果:
image.png
没有什么可说的,非常简单。


2.2 类的静态函数作为回调函数

#include class ProgramA {public:void FunA1() { printf("I'am ProgramA.FunA1() and be called..\n"); }static void FunA2() { printf("I'am ProgramA.FunA2() and be called..\n"); }
};class ProgramB {public:void FunB1(void (*callback)()) {printf("I'am ProgramB.FunB1() and be called..\n");callback();}
};int main(int argc, char **argv) {ProgramA PA;PA.FunA1();ProgramB PB;PB.FunB1(ProgramA::FunA2);
}

执行结果
image.png

可以看出,以上两种方式没有什么本质的区别。
但这种实现有一个很明显的缺点:static 函数不能访问非static 成员变量或函数,会严重限制回调函数可以实现的功能。


2.3 类的非静态函数作为回调函数

#include class ProgramA {public:void FunA1() { printf("I'am ProgramA.FunA1() and be called..\n"); }void FunA2() { printf("I'am ProgramA.FunA2() and be called..\n"); }
};class ProgramB {public:void FunB1(void (ProgramA::*callback)(), void *context) {printf("I'am ProgramB.FunB1() and be called..\n");((ProgramA *)context->*callback)();}
};int main(int argc, char **argv) {ProgramA PA;PA.FunA1();ProgramB PB;PB.FunB1(&ProgramA::FunA2, &PA); // 此处都要加&
}

执行结果
image.png
这种方法可以得到预期的结果,看似完美,但是也存在明显不足。
比如在programB中FunB1还使用 programA的类型,也就我预先还要知道回调函数所属的类定义,当programB想独立封装时就不好用了。

这里还有一种方法可以避免这样的问题,可以把非static的回调函数 包装为另一个static函数,这种方式也是一种应用比较广的方法。

#include class ProgramA {public:void FunA1() { printf("I'am ProgramA.FunA1() and be called..\n"); }void FunA2() { printf("I'am ProgramA.FunA2() and be called..\n"); }static void FunA2Wrapper(void *context) {printf("I'am ProgramA.FunA2Wrapper() and be called..\n");((ProgramA *)context)->FunA2(); // 此处调用的FunA2()是context的函数, 不是this->FunA2()}
};class ProgramB {public:void FunB1(void (ProgramA::*callback)(), void *context) {printf("I'am ProgramB.FunB1() and be called..\n");((ProgramA *)context->*callback)();}void FunB2(void (*callback)(void *), void *context) {printf("I'am ProgramB.FunB2() and be called..\n");callback(context);}
};int main(int argc, char **argv) {ProgramA PA;PA.FunA1();ProgramB PB;PB.FunB1(&ProgramA::FunA2, &PA); // 此处都要加&printf("\n");PB.FunB2(ProgramA::FunA2Wrapper, &PA);
}

执行结果:
image.png
这种方法相对于上一种,ProgramB中没有ProgramA的任何信息了,是一种更独立的实现方式。
FunB2()通过调用FunA2Wrapper(),实现间接的对FunA2()的调用。FunA2()可以访问和调用对类内的任何函数和变量。多了一个wrapper函数,也多了一些灵活性。

上面借助wrapper函数实现回调,虽然很灵活,但是还是不够优秀,比如:
1)多了一个不是太有实际用处的wrapper函数。
2)wrapper中还要对传入的指针进行强制转换。
3)FunB2调用时,不但要指定wrapper函数的地址,还要传入PA的地址。

那是否有更灵活、直接的方式呢?有,可以继续往下看。


3 std::funtion和std::bind的使用

std::funtion和std::bind可以登场了。
std::function是一种通用、多态的函数封装。std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等[1]。
std::bind()函数的意义就像它的函数名一样,是用来绑定函数调用的某些参数的[2]。
关于他们的详细用法可以自行百度,如果有需要的以后出一期单独写,这里直接上代码,看std::funtion和std::bind如何在回调中使用。

#include #include // fucntion/bindclass ProgramA {public:void FunA1() { printf("I'am ProgramA.FunA1() and be called..\n"); }void FunA2() { printf("I'am ProgramA.FunA2() and be called..\n"); }static void FunA3() { printf("I'am ProgramA.FunA3() and be called..\n"); }
};class ProgramB {typedef std::function<void ()> CallbackFun;public:void FunB1(CallbackFun callback) {printf("I&#39;am ProgramB.FunB2() and be called..\n");callback();}
};void normFun() { printf("I&#39;am normFun() and be called..\n"); }int main(int argc, char **argv) {ProgramA PA;PA.FunA1();printf("\n");ProgramB PB;PB.FunB1(normFun);printf("\n");PB.FunB1(ProgramA::FunA3);printf("\n");PB.FunB1(std::bind(&ProgramA::FunA2, &PA));
}

执行输出&#xff1a;
image.png
std::funtion支持直接传入函数地址&#xff0c;或者通过std::bind指定。
简而言之&#xff0c;std::funtion是定义函数类型(输入、输出)&#xff0c;std::bind是绑定特定的函数&#xff08;具体的要调用的函数&#xff09;。

相比于wrapper方法&#xff0c;这个方式要更直接、简洁很多。


Ref:

[1] https://blog.csdn.net/u013654125/article/details/100140328
[2] https://blog.csdn.net/u013654125/article/details/100140547


推荐阅读
author-avatar
木又的思念_740
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有