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

一、回调函数

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

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;
}

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

六、总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/24395.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity中的CanvasScaler组件讲解

Unity中的CanvasScaler组件是UGUI系统中的一个关键组件&#xff0c;主要用于控制画布的缩放和适配&#xff0c;以确保UI在不同屏幕分辨率下的显示效果一致。以下是关于CanvasScaler组件的详细讲解&#xff1a; 一、CanvasScaler组件的作用 调整UI画布的缩放和分辨率适配&…

数据挖掘--数据仓库与联机分析处理

什么是数据仓库 &#xff08;面集时非&#xff09; 面向主题的&#xff1a;围绕某一主题来构建集成的&#xff1a;图片文字杂糅在一起时变的&#xff1a;随时间变化的数据非易失的&#xff1a;硬盘存放&#xff0c;不易丢失 操作数据库系统&#xff08;OLTP)与数据仓库(OLAP…

MySQL将错乱的水果信息,截取展示为 品名 英文名 价格 三列展示

将错乱的水果信息&#xff0c;截取展示为 品名 英文名 价格 三列展示 idname1苹果Apple72Plum6李子3Pineapple8菠萝4Mango5芒果5龙吐珠5Buddha’sHand6Olive9橄榄7Raspberry4树莓8Apricot5杏子9Grapefruit9柚子10火龙果Dragonfruit911倒挂金钟Hanging6LobsterClaw12巨峰葡萄Co…

AI办公自动化:批量把docx文档转换为txt文本

任务&#xff1a;把docx文档批量转换成txt&#xff0c;首先让deepseek写了一段代码&#xff0c;但是转换失败。用的是最流行的python-docx库来读取docx文档&#xff0c;但是始终无法读取成功&#xff0c;换成pywin32库就解决问题了。 在deepseek中输入提示词&#xff1a; 写一…

「前端+鸿蒙」鸿蒙应用开发-真机运行

在鸿蒙应用开发中&#xff0c;真机运行是验证应用在实际硬件上表现的重要步骤。以下是如何在华为DevEco Studio中配置真机运行的详细步骤&#xff0c;以及相应的示例代码。 快速体验-真机运行 准备工作&#xff1a; 确保您的鸿蒙设备已开启开发者模式&#xff0c;并启用USB调试…

部件库(Widget Factory)

部件库(Widget Factory) 部件库,也被称为Widget Factory,是一个强大的工具,用于创建、存储和管理可重用的软件组件。在本文中,我们将深入探讨部件库的概念、重要性、以及如何在现代软件开发中使用它。 什么是部件库? 部件库是一个集合,其中包含了各种预先构建的软件…

c++ 简单的日志类 CCLog

此日志类&#xff0c;简单地实现了向标准输出控制台和文件输出日志信息的功能&#xff0c;并能在这两者之间进行切换输出&#xff0c;满足输出日志的不同需求。 代码如下&#xff1a; /** CCLog.h* c_common_codes** Created by xichen on 12-1-12.* Copyright 2012 cc_te…

40.任务调度线程池

Timer(废弃) 在任务调度线程池功能加入之前,可以使用java.util.Timer来实现定时功能,Timer优点在于简单易用,缺点是由于所有的任务都是由同一个线程来调度,因此所有的任务都是串行执行,同一时间只能有一个任务在执行,前一个任务的延迟和异常都将会影响之后的任务。 T…

【背包-BM70 兑换零钱(一)】

题目 BM70 兑换零钱(一) 描述 给定数组arr&#xff0c;arr中所有的值都为正整数且不重复。每个值代表一种面值的货币&#xff0c;每种面值的货币可以使用任意张&#xff0c;再给定一个aim&#xff0c;代表要找的钱数&#xff0c;求组成aim的最少货币数。 如果无解&#xff0c;…

docker 命令 ps,inspect,top,logs详解

docker常用命令教程-4 docker ps docker ps 命令用于列出当前正在运行的容器。默认情况下&#xff0c;它只显示正在运行的容器&#xff0c;但你可以使用 -a 或 --all 选项来显示所有容器&#xff08;包括已停止的容器&#xff09;。 常用的选项和示例&#xff1a; -a 或 --…

【C语言题解】1、写一个宏来计算结构体中某成员相对于首地址的偏移量;2、写一个宏来交换一个整数二进制的奇偶位

&#x1f970;欢迎关注 轻松拿捏C语言系列&#xff0c;来和 小哇 一起进步&#xff01;✊ &#x1f308;感谢大家的阅读、点赞、收藏和关注 &#x1f495;希望大家喜欢我本次的讲解&#x1f495; 目录&#x1f451; 1、写一个宏&#xff0c;计算结构体中某变量相对于首地址的偏…

UE4获取动画序列资产的动画时长

谢谢”朝闻道“大佬的指点~

(UE4.26)UE4的FArchive序列化入门

前言 序列化(Serialize)和反序列化(UnSerialize)是程序领域常见的概念。对于这两个词汇我理解的是 序列化(Serialize): 变量值(int, float, string等基本类型, 或者Array&#xff0c;Map&#xff0c;或者更复杂的复合体)存储为一个文件(二进制流, 二进制文件, json, xml等格式…

C++并发之互斥(std::mutex)

目录 1 概述2 使用实例3 接口使用3.1 mutex3.2 lock3.3 try_lock3.4 unlock1 概述 互斥锁是一个可锁定的对象,用于在代码的关键部分需要独占访问时发出信号,防止具有相同保护的其他线程同时执行并访问相同的内存位置。   互斥对象提供独占所有权,不支持递归性(即,线程不…

Windows 找不到文件‘shell:sendto‘。请确定文件名是否正确后,再试一次

执行“shell:sendto”命令的时候&#xff0c;报错&#xff1a;Windows 找不到文件’shell:sendto’。请确定文件名是否正确后&#xff0c;再试一次 解决办法&#xff1a; 在桌面新建一个记事本文件命名为fix.reg&#xff0c;注意后缀是reg&#xff0c;文件中填写以下内容&…

快速入门和学习Qt 6.3的技巧

#qt6.3 Qt 6.3 是一个强大且多功能的跨平台应用开发框架&#xff0c;适用于开发桌面、移动和嵌入式系统应用。本文将介绍一些学习Qt 6.3的技巧和快速入门的方法&#xff0c;帮助你高效掌握这一技术。 一、理解Qt 6.3的基础架构 模块结构&#xff1a;Qt 6.3 由多个模块组成&a…

若依 Spring Security 短信,扫码登录

1. 修改 LoginBody&#xff0c;添加登录类型字段 Data public class LoginBody {/*** 用户名*/private String username;/*** 用户密码*/private String password;/*** 验证码*/private String code;/*** 唯一标识*/private String uuid;/*** 登录类型*/private String logi…

应对转租、混租、群租,天诚人脸物联网智能门锁与公租房管理系统有一套!

“住房”关系着老百姓的切身利益和幸福指数。近年来&#xff0c;全国各地掀起了保障性住房建设热潮&#xff0c;积极有序推进智慧公租房小区打造&#xff0c;助力共同富裕。 一、公租房入住对象界定 公租房的受众群体各地标准不一&#xff0c;却也大同小异。重庆市公共租赁房…

【安装笔记-20240607-Linux-在 OpenWrt-23.05 上安装配置域名服务器】

安装笔记-系列文章目录 安装笔记-20240607-Linux-在 OpenWrt-23.05 上安装配置域名服务器 文章目录 安装笔记-系列文章目录安装笔记-20240607-Linux-在 OpenWrt-23.05 上安装配置域名服务器 前言一、软件介绍名称&#xff1a;Bind9主页官方介绍 二、安装步骤测试版本&#xff…

Liunx环境下redis主从集群搭建(保姆级教学)01

Linux 环境安装redis 准备一台linux虚拟机 我使用基于Linux的开源类服务器操作系统CentOS7。 打开虚拟机&#xff0c;输入密码登录 下载linux版本的redis安装包 已经下载redis-5.0.10.tar.gz 创建一个文件夹用来安装redis,我在/opt目录下创建redis文件夹 将下载好的redis…