C生万物 | 剖析函数指针经典应用 —— 回调函数

news2024/12/28 22:01:31

在这里插入图片描述

在这里插入图片描述

不懂函数指针的老铁可以先看看这篇文章【指针函数与函数指针】,上车,准备出发🚗

文章目录

  • 一、回调函数的概念
  • 二、为什么要使用回调函数?
  • 三、回调函数使用场景
    • 场景一:模拟计算器的加减乘除
    • 场景二:模拟qsort函数【⭐】
      • 1、qsort函数解读
      • 2、用用qsort
      • 3、使用冒泡排序模拟qsort
      • 4、原理分析
    • 场景三:模拟文件下载模块
  • 四、语言对比
    • 1、JavaScript回调函数
    • 2、C#委托
  • 五、总结与提炼

一、回调函数的概念

回调函数就是一个通过【函数指针】调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

二、为什么要使用回调函数?

👉最大的一个目的,就是为了实现:解耦!

  1. 在主入口程序中,把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,且不需要修改库函数的实现,变的很灵活,这就是解耦

  2. 主函数和回调函数是在同一层的,而库函数在另外一层。如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数了,这也就是在日常工作中常见的情况


注:使用回调函数会有间接调用,因此,会有一些额外的传参与访存开销,对于MCU代码中对时间要求较高的代码要慎用

三、回调函数使用场景

场景一:模拟计算器的加减乘除

  • 在函数指针章节,我有介绍了如何使用【函数指针数组】去模拟计算器的加减乘除,现在我们使用回调函数来试试

功能与菜单

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int Mul(int x, int y)
{
	return x * y;
}

int Div(int x, int y)
{
	return x / y;
}

void menu()
{
	printf("**************************\n");
	printf("***** 1.Add    2.Sub *****\n");
	printf("***** 3.Mul    4.Div *****\n");
	printf("***** 5.Cls    0.Exit*****\n");
	printf("**************************\n");
}

主程序与回调函数

void calc(int (*p)(int, int))
{
	int x = 0, y = 0;
	printf("请输入两个运算数:>");
	scanf("%d %d", &x, &y);
	int ret = p(x, y);
	printf("结果为:%d\n", ret);
}

int main(void)
{
	int input = 0;
	do {
		menu();
		printf("请输入你的选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			calc(Add);
			break;
		case 2:
			calc(Sub);
			break;
		case 3:
			calc(Mul);
			break;
		case 4:
			calc(Div);
			break;
		case 5:
			system("cls");
			break;
		case 0:
			break;
		default:
			printf("请输入正确的内容:\n");
			break;
		}

	} while (input);
	return 0;
}

通过画图来看一下是如何通过函数指针来实现的回调

在这里插入图片描述

  • 可以看出,回调函数它不会自己调用,而是将自己的函数名传递给到另一个函数(此处的Add和Sub即为回调函数),然后在这个函数内部通过函数指针去调用这个函数。就是这样函数指针会接收来自不同函数的地址,继而实现计算器的加、减、乘、除各种功能

场景二:模拟qsort函数【⭐】

学习过数据结构的同学一定接触过【快速排序】,即QuickSort。不了解的可以看看 数据结构 | 十大排序超硬核八万字详解

1、qsort函数解读

  • 在C语言中,也有一个关于快速排序的库函数,叫做qsort,来看一下官方文档是怎么说的

在这里插入图片描述

  • 清楚了这个函数的基本作用后,那最想知道的就是它如何使用,既然是函数的话就需要传递参数,给个特写📷
    • base —— 待排序元素的起始地址,类型为【void】表示可以传递任何类型的数组
    • num —— 表示待排序数据的元素个数
    • size —— 表示数组中每个元素所占的字节数
    • int (*compar)(const void*, const void*) —— 函数指针,用于接收回调函数

在这里插入图片描述

2、用用qsort

💬首先我们用它来排下整型数组试试

cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}
void test1()
{
	int arr[10] = { 2,3,6,7,5,1,4,9,10,8 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	printarray(arr, sz);
	
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	
	printarray(arr, sz);
}

运行结果:

在这里插入图片描述
解析:

cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}
  • 主要来讲一下这个函数,这就是本文要讲解的回调函数,为什么它的形参是一个void*的指针呢?这种类型的指针一般被我们称作为【垃圾桶】,那垃圾桶我们平常都在用,不考虑垃圾分类的话,可以接收任何种类的垃圾,那么在这里就是可以接收任何类型的数据,即整型、字符型、浮点型,甚至是自定义类型它都可以接受
  • 但是呢我们在使用的时候还是要去进行一个转换,此处就要使用到【强制类型转换】,将其转换为int *的指针,那么它就指向了我们要待排序的数组。但是要怎么比较和交换两个数据呢,这就要看qsort()函数内部的实现了,它是基于快速排序的思想,如果你懂快速排序的话,脑海里立马就能浮现出它们的比较的场景
  • 还是来看一下官方文档,其实下面的这种比较思路很常见,像字符串函数[strcmp]也是这样的:
    • 前一个比后一个小,返回-1
    • 前一个和后一个相等返回,返回0
    • 前一个比后一个大,返回1

在这里插入图片描述


当然,除了上面这种内置类型外,自定义类型的数据也是可以比较的,接下去我们来比较一下两个学生的信息

  • 下面是结构体的初始化和定义,以及qsort函数的调用
typedef struct stu {
	char name[20];
	int age;
}stu;
void test2()
{
	stu ss[3] = { {"zhangsan", 22}, {"lisi", 55}, {"wangwu", 33} };

	qsort(ss, 3, sizeof(ss[0]), cmp_byname);
	//qsort(ss, 3, sizeof(ss[0]), cmp_byage);
}
  • 下面是两个回调函数的实现,在看了第一个后相信你已经很熟悉了,形参还是void*类型的指针,但是在比较的时候要转换为结构体指针,否则就无法访问到成员了。对于【姓名】的比较是按照首字母的ASCLL码值来的,这里我们直接使用库函数strcmp即可,比较的规则和qsort()是一致的
Cmp_ByName(const void* e1, const void* e2)
{
	return strcmp(((stu*)e1)->name, ((stu*)e2)->name);
}

Cmp_ByAge(const void* e1, const void* e2)
{
	return ((stu*)e1)->age - ((stu*)e2)->age;
}

首先来看按照名字排序的结果

在这里插入图片描述

然后是按照年龄排序的结果

在这里插入图片描述

3、使用冒泡排序模拟qsort

  • 普通的冒泡排序的话相信是个大学生应该都会写,这里就不解释了,如果不会的话看看我的排序文章
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; ++i)
	{
		for (int j = 0; j < n - 1 - i; ++j)
		{
			if (a[j] > a[j + 1])
			{
				int t = a[j];
				a[j] = a[j + 1];
				a[j + 1] = t;
			}
		}
	}
}

在这里插入图片描述

但此时我若是要用这个冒泡排序去排任意类型的数据呢?该如何进行修改

  • 此时就需要使用到刚才所学习的qsort()函数了。我们可以仿照着它的参数来写写看
void bubblesort(void* base, int num, int sz, int(*cmp)(const void* e1, const void* e2))
  • 既然参数做了,那么函数体内部我们也需要做一个大改动。例如对数组中的两个数据进行比较的时候,就不能单纯地使用关系运算符>>==了,此处函数指针就派上了用场,我们可是使用函数指针去接收不同的回调函数,继而去实现不同的类型数据的比较,也就是上面所写的Cmp_intCmp_ByNameCmp_ByAge
  • 而且对于内部的交换逻辑我们也要单独去实现,不同数据的交换方式是不一样的

在这里插入图片描述

那现在,我们就来实现一下上面说到的这两块内部逻辑

  • 首先就是jj + 1这两个位置上的值要如何进行比较的问题,那既然base指向首元素地址,那有同学说不妨让它进行偏移,但是它的类型是void*,虽然这种类型的指针可以接收各种各样的数据地址, 但是却无法进行偏移,因为它也不知道要偏移多少字节,所以我上面在回调函数内部对两个形参进行了强转才可以进行比较

在这里插入图片描述

  • 我们知道,对于char类型的字符,在内存中只占有1个字节的大小,那么char*的指针每次后移便会偏移一个字节,那既然在形参我们传入了数组中每个元素在内存中所占字节数的话,就可以使用起来了,和char*的指针去做一个配合
    在这里插入图片描述
  • 所以两数比较的逻辑就可以写成下面这样
//判断两数是否需要交换
if (cmp((char*)base + j * sz, (char*)base + (j + 1) * sz) > 0)
{
	//两数据交换的逻辑
}

接下去就来实现两数交换的逻辑

  • 因为我们是使用的char*指针一个字节一个字节去访问数据的,所以交换的时候也需要按照字节来交换。单独封装一个Swap()函数,把要交换两个数的地址和单个数据所占的字节数传入

声明:

void Swap(char* buf1, char* buf2, int sz)

调用:

Swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz);

内部逻辑就是单个数据的交换【记住,这只是单个数据,所以循环sz次】

void Swap(char* buf1, char* buf2, int sz)
{
	//两个数据按照字节一一交换
	for (int i = 0; i < sz; ++i)
	{
		int t = *buf1;
		*buf1 = *buf2;
		*buf2 = t;

		buf1++;
		buf2++;
	}
}

具体交换细节可以看下图
在这里插入图片描述
测试一下:

  • 可以看到,整数类型的数据排序成功了

在这里插入图片描述

  • 再看看内置类型

在这里插入图片描述

在这里插入图片描述

4、原理分析

仔细看一下这张图,你就清楚整个调用过程了

在这里插入图片描述

场景三:模拟文件下载模块

我们为什么要用回调函数呢?

记得在一次C++开发面试的时候被被一位主面官问到过这个问题,现在再回答一遍。

  • 我们对回调函数的使用无非是对函数指针的应用,函数指针的概念本身很简单,但是把函数指针应用于回调函数就体现了一种解决问题的策略,一种设计系统的思想。

  • 在解释这种思想前我想先说明一下,回调函数固然能解决一部分系统架构问题但是绝不能再系统内到处都是,如果你发现你的系统内到处都是回调函数,那么你一定要重构你的系统。回调函数本身是一种破坏系统结构的设计思路,回调函数会绝对的变化系统的运行轨迹,执行顺序,调用顺序。回调函数的出现会让读到你的代码的人非常的懵头转向。

  • 那么什么是回调函数呢,那是不得以而为之的设计策略,想象一种系统实现:在一个下载系统中有一个文件下载模块和一个下载文件当前进度显示模块,系统要求实时的显示文件的下载进度,想想很简单在面向对象的世界里无非是实现两个类而已。但是问题恰恰出在这里,显示模块如何驱动下载进度条?显示模块不知道也不应该知道下载模块所知道的文件下载进度(面向对象设计的封装性,模块间要解耦,模块内要内聚),文件下载进度是只有下载模块才知道的事情,解决方案很简单给下载模块传递一个函数指针作为回调函数驱动显示模块的显示进度。

下面是模拟实现这个文件下载模块的代码,仅供参考

#include <iostream>
#include <random>
#include <ctime>

typedef void(*on_process_callback)(std::string data);

//处理完成的回调
void on_process_result(std::string data)
{
   //根据返回消息进行处理
   std::cout << data.c_str() << std::endl;
};

class TaskProcessing
{
public:
   TaskProcessing(on_process_callback callback) : _callback(callback)
   {};

   void set_callback(on_process_callback callback)
   {
   	_callback = callback;
   };

   void do_task()
   {
   	//当文件传输完成
   	if (_callback)
   	{
   		srand((int)time(NULL));
   		if (rand() & 1)
   		{
   			(*_callback)(std::string("ftp succeed"));
   		}
   		else
   		{
   			(*_callback)(std::string("ftp failed"));
   		}
   	}
   };
private:
   on_process_callback _callback;
};

int main()
{
   TaskProcessing* process = new TaskProcessing(on_process_result);
   process->do_task();
   system("pause");
}

四、语言对比

在看这个回调函数的时候,我也联想到了JS和C#中似乎也有类似的身影,这里对比分析一下

1、JavaScript回调函数

  • 在JavaScrip中, function 是内置的类对象,也就是说它是一种类型的对象,可以和其它String、Array、Number、Object类的对象一样用于内置对象的管理。因为function实际上是一种对象,它可以“存储在变量中,通过参数传递给(别一个)函数(function),在函数内部创建,从函数中返回结果值”。
  • 因为function是内置对象,我们可以将它作为参数传递给另一个函数,延迟到函数中执行,甚至执行后将它返回。这是在JavaScript中使用回调函数的精髓

例如在下面,有一个add函数,通过外界传入要运算的两个操作符以及一个回调函数的地址,就可以起到在add函数内部去调用print()函数的作用

  • 可以看到我传递了print作为add()函数的形参,其为函数名,函数名即为函数的地址,此时add函数内部就获取到printf()函数的地址那便可以通过一定的条件去调用这个函数
<script>
    function add(num1, num2, callback) {
        var sum = num1 + num2;
        callback(sum);
    }

    function print(num) {
        console.log(num);
    }

    add(1, 2, print); //3
</script>

2、C#委托

如果有学习过C#的同学,说到【回调函数】的话,应该可以很快联想到委托,真的是异曲同工之妙

不清楚的同学可以先看看这个视频,讲得还可以

C#基础教程 delegate 帮你理解委托,知道委托的好处, 不懂委托一定要看下!

  • C#里面有命名方法委托、多播委托、匿名委托,这里举一个简单点的小例子
class Program
{
    public delegate void MyDelegate();
    static void Main(string[] args)
    {
        MyDelegate myDelegate = new MyDelegate(new Test().SayHello);
        myDelegate();
    }
}
class Test
{
    public void SayHello()
    {
        Console.WriteLine("Hello Delegate!");
    }
}

💬 举了两个小小的例子,为了让读者了解到除了C语言其实还有其他语言中也有【回调函数】的声音,了解到什么叫做 ⇒ C生万物

五、总结与提炼

好,最后来总结一下本文所学习的内容📖

  • 在本文中,我们重点讲解了什么叫做【回调函数】,以及为什么要使用【回调函数】,它有什么用途?清楚了基本的概念后我们就去真正地接触了这个回调函数,模拟实现了三种回调函数的应用场景,分别是计算器的加减乘除、qsort函数、还有文件下载模块,其中qsort函数的模拟实现是我们本文的重点所在
  • 除了C语言里面有回调函数之外,其实其他语言里面也存在这个东西,像JS中的回调函数、C#中的委托,如果有兴趣的老铁可以再去研究研究,回调函数这个东西在有些场景确实很管用

以上就是本文要介绍的所有内容,感谢您的阅读🌹

在这里插入图片描述

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

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

相关文章

MySQL集群方案总结与方案原理

前言 在给自己做着玩的一个项目准备数据库集群&#xff0c;顺带自己大致系统复习并记录一下。 单节点mysql存在的常见问题 当数据量和并发量上去后&#xff0c;单节点数据库无法满足大并发时性能上的要求。单节点的MySQL无法满足高可用&#xff0c;数据库宕机或者意外中断等故障…

数据结构学习分享之双向链表详解

数据结构第四课 1.前言2. 结构分析3. 双链表的实现3.1 初始化结构3.2 初始化函数3.3 尾插函数3.4 尾删函数3.5 头插函数3.6 头删函数3.7 销毁链表3.8 其他函数 4. 缓存利用率5. 总结 1.前言 &#x1f493;博主CSDN:杭电码农-NEO&#x1f493;&#x1f389;&#x1f389;&#x…

Python(一) 基础二(语句、文件读写)

1.语句 1.1.if…elif…else 类似于java的if…else if…else语句 1.1.1.判断条件 比较运算符: 、>、<、<、>、!、is、is not、in、not in 1.1.2.和is的区别 list_1 [aaa, bbb] list_2 [aaa, bbb] print(list_1 list_2) #结果:True print(list_1 is list_2)…

async await

async await async await 都是修饰符&#xff0c;修饰函数的。 async/await一定是成对出现的。比如用async也没有什么太大意义。只要函数体中出现了await&#xff0c;则当前函数必须用async来修饰。 用async修饰的函数&#xff0c;相当于用promise包裹起来。其实相当于把同步修…

基于jeecgboot的OA日程安排开发(一)

日程安排也是OA里的一项重要功能&#xff0c;所以基于jeecgboot开发这个日程安排。 日程安排主要涉及以下几个方面&#xff1a; 1、数据库方面&#xff0c;主要是分日历与日程 日历可以分个人日历与工作日历&#xff0c;一般情况下&#xff0c;个人日历只给自己查看&#xff0…

2023-05-02 动态规划简介

动态规划简介 1 动态规划的基本概念 阶段、状态、决策、策略、状态转移方程 1) 阶段和阶段变量 将问题的全过程恰当地分成若干个相互联系的阶段闫氏DP分析法&#xff1a;对应f[i][j]的ij遍历时形成的所有f[i][j]阶段的划分一般根据时间和空间的自然特征去划分阶段的划分便于把…

Nginx原理解析

master和worker 当linux启动的时候&#xff0c;会有两个和nginx相关的进程&#xff0c;一个是master,一个是worker。 master如何工作 当客户端发送请求到nginx之后&#xff0c;master会接收到这个请求&#xff0c;然后通知所有的worker进程&#xff0c;此时&#xff0c;work…

【STL十六】函数对象:包装器(std::function)——绑定器(std::bind)——函数适配器

【STL十六】函数对象&#xff1a;包装器(std::function&#xff09;——绑定器&#xff08;std::bind&#xff09;——函数适配器 一、包装器(std::function&#xff09;1、简介2、头文件3、构造函数4、demo5、异常 二、绑定器&#xff08;std::bind&#xff09;1、简介2、头文…

Nessus漏洞扫描以及OpenSSH漏洞修复验证

主机IP地址资源kali192.168.200.1285GB内存/4CPUCentOS7.5192.168.200.1292GB内存/2CPU https://www.tenable.com/downloads/nessus?loginAttemptedtrue curl --request GET \--url https://www.tenable.com/downloads/api/v2/pages/nessus/files/Nessus-10.5.1-ubuntu1404_am…

云原生Istio案例实战

目录 1 Istio监控功能1.1 prometheus和grafana1.2 访问prometheus1.3 访问grafana 2 项目案例&#xff1a;bookinfo2.1 理解什么是bookinfo2.2 sidecar自动注入到微服务2.3 启动bookinfo2.4 通过ingress方式访问2.5 通过istio的ingressgateway访问2.5.1 确定 Ingress 的 IP 和端…

计算机视觉--图像拼接

图像拼接 单应性变换仿射变换图像扭曲实现图像嵌入&#xff08;图中图&#xff09; RANSAC算法算法介绍图片收集无RANSAC优化和有RANSAC优化的代码实现差别 总结 单应性变换 单应性变换是指一个平面上的点通过一个矩阵变换映射到另一个平面上的点&#xff0c;这个变换矩阵是一…

java聊天室的设计与实现代码

聊天室是一个简单的通信应用&#xff0c;可以帮助您与客户和朋友保持联系&#xff0c;并且可以让您更轻松地与其他员工联系。然而&#xff0c;您将不得不确保每个人都知道他们正在做什么。 一旦聊天室开始&#xff0c;它就会变得非常复杂&#xff0c;因为有许多用户可能会同时登…

【三十天精通Vue 3】第二十五天 Vue3 与 Axios 后端数据交互

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: 三十天精通 Vue 3 文章目录 引言一、Vue3 与 Axios 概述二、Axios 安装与基本使用2.1 安装 Ax…

NECCS|全国大学生英语竞赛C类|词汇和语法|语法题|时态|22:30~11:44

15题 10min 10:20&#xff5e;10:25 test2 10:25&#xff5e;10:47 test1订正 10:44&#xff5e;11:47 理论学习 涉及的语法点主要包括&#xff1a; 动词的时态和语态 非谓语动词 虚拟语气 主谓一致 倒装句 强调句 比较级 名词性从句 定语…

【SQL篇】面试篇之子查询

1303 求团队人数 # 写法1 # Write your MySQL query statement below select employee_id, count(*) over(partition by team_id) as team_size from Employee# 写法2 # Write your MySQL query statement below select employee_id, team_size from Employee e join (select t…

优雅编程,从空格、空行、缩进、注释开始

很多初学者的代码其实都不够“漂亮”&#xff0c;那是因为没有养成好的编码习惯。本篇博客以C语言为例&#xff0c;总结一些好习惯。其实&#xff0c;很多习惯都是肌肉记忆&#xff0c;举个例子&#xff1a;请你写一个程序&#xff0c;输入2个整数并输出它们的和。有些朋友可能…

springboot+vue前后端分离项目打包成jar包及运行

将 Spring Boot 和 Vue.js 项目打包成 jar 包需要按照以下步骤操作&#xff1a; 在项目的根目录中&#xff0c;使用命令行进入 Vue.js 项目的根目录&#xff0c;然后运行以下命令&#xff1a; npm run build这个命令将会构建 Vue.js 项目&#xff0c;并在项目的 dist 目录中生…

Rust-Rocket框架笔记

Rust-Rocket框架笔记 Rocket-Learn-docRocket Addr视频地址 What is RocketQuickStart下载Rocket-Rust运行Rust-Rocket-Hello-错误-端口占用解决查看端口占用情况添加Rocket.toml配置文件更改Rocket默认启动端口启动成功 GetStart-Hello world创建项目cargoIDEA 添加依赖添加Ro…

使用eclipse创建一个图书管理系统(2)---------逻辑的实现

就像使用C语言写代码一样。比如要用​​​​​​C语言写一个小游戏的代码&#xff0c;我们的逻辑实现是在哪里实现的啊&#xff1f;是不是在一个test.c源文件里面啊&#xff1f;没错&#xff0c;就是的&#xff01;在之前的文章里&#xff0c;我说过我定义的Main函数就像C语言里…

【《中国工业经济》数据复现】数字化转型与企业分工:专业化还是纵向一体化

一.研究内容 本文使用机器学习方法刻画微观企业数字化水平&#xff0c;并在构建数理模型的基础上实证考察了企业数字化转型对企业分工的影响及其机理。结果表明&#xff0c;企业数字化转型显著提升了中国上市企业专业化分工水平。机制分析表明&#xff0c;数字化转型对企业专业…