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

这篇具有很好参考价值的文章主要介绍了C生万物 | 剖析函数指针经典应用 —— 回调函数。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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

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

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

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

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

1、qsort函数解读

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

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

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

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

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

运行结果:

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

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

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


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

  • 下面是结构体的初始化和定义,以及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;
}

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

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

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

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

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

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

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

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

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

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

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

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

  • 我们知道,对于char类型的字符,在内存中只占有1个字节的大小,那么char*的指针每次后移便会偏移一个字节,那既然在形参我们传入了数组中每个元素在内存中所占字节数的话,就可以使用起来了,和char*的指针去做一个配合
    C生万物 | 剖析函数指针经典应用 —— 回调函数
  • 所以两数比较的逻辑就可以写成下面这样
//判断两数是否需要交换
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++;
	}
}

具体交换细节可以看下图
C生万物 | 剖析函数指针经典应用 —— 回调函数
测试一下:

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

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

  • 再看看内置类型

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

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

4、原理分析

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

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

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

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

记得在一次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#中的委托,如果有兴趣的老铁可以再去研究研究,回调函数这个东西在有些场景确实很管用

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

C生万物 | 剖析函数指针经典应用 —— 回调函数文章来源地址https://www.toymoban.com/news/detail-441698.html

到了这里,关于C生万物 | 剖析函数指针经典应用 —— 回调函数的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C/C++面向对象(OOP)编程-回调函数详解(回调函数、C/C++异步回调、函数指针)

    本文主要介绍回调函数的使用,包括函数指针、异步回调编程、主要通过详细的例子来指导在异步编程和事件编程中如何使用回调函数来实现。 🎬个人简介:一个全栈工程师的升级之路! 📋个人专栏:C/C++精进之路 🎀CSDN主页 发狂的小花 🌄人生秘诀:学习的本质就是极致

    2024年02月03日
    浏览(39)
  • 【再识C进阶2(中)】详细介绍指针的进阶——函数指针数组、回调函数、qsort函数

    💓作者简介: 加油,旭杏,目前大二,正在学习 C++ , 数据结构 等👀 💓作者主页:加油,旭杏的主页👀 ⏩本文收录在:再识C进阶的专栏👀 🚚代码仓库:旭日东升 1👀 🌹欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖        在这一篇博客中,我们要认识并理解 函数指

    2024年02月09日
    浏览(46)
  • 【C语言】——指针五:转移表与回调函数

    1.1、转移表的定义      在之前的学习中,我们学习了 函数指针数组 (详情请看【C语言】——指针四:字符指针与函数指针变量),在最后。我曾问到:函数指针数组有什么用呢?别急,本文给大家细细道来。      函数指针数组常常被用在 转移表 中,那转移表是

    2024年03月26日
    浏览(43)
  • 【C进阶】回调函数(指针进阶2,详解,小白必看)

    目录 6. 函数指针数组 6.1简单计算器 6.2函数指针数组实现计算器 7. 指向函数指针数组的指针(仅作了解即可) 8.回调函数 8.1关于回调函数的理解​编辑 8.1.1用回调函数改良简单计算器 8.2qsort库函数的使用 8.2.1冒泡排序 8.2.2qsort的概念 8.3冒泡排序思想实现qsort          这篇文

    2024年02月14日
    浏览(38)
  • C 语言高级3--函数指针回调函数,预处理,动态库的封装

    目录 1.函数指针和回调函数 1.1 函数指针 1.1.1 函数类型 1.1.2 函数指针(指向函数的指针) 1.1.3 函数指针数组        1.1.4 函数指针做函数参数(回调函数)  2.预处理 2.1 预处理的基本概念 2.2 文件包含指令(#include) 2.2.1 文件包含处理  2.2.2 #incude和#include\\\"\\\"区别 2.3 宏定义 2.3.1 无参

    2024年02月14日
    浏览(38)
  • 【C语言】指针的进阶(二)—— 回调函数的讲解以及qsort函数的使用方式

    目录 1、函数指针数组 1.1、函数指针数组是什么?  1.2、函数指针数组的用途:转移表 2、扩展:指向函数指针的数组的指针 3、回调函数 3.1、回调函数介绍  3.2、回调函数的案例:qsort函数 3.2.1、回顾冒泡排序  3.2.1、什么是qsort函数? 函数指针数组 是什么?首先主语是 数

    2024年02月07日
    浏览(44)
  • Java中的回调函数 (callback) 及其应用

    回调函数在编程中是一种常见的设计模式,它允许一个函数在特定的时刻或条件下调用另一个函数。在Java中,我们可以通过接口和匿名内部类实现回调函数。本文将详细介绍Java中的回调函数,并提供相关代码示例。 回调函数是一种将函数作为参数传递给另一个函数的方法。

    2024年01月24日
    浏览(53)
  • 【数据结构】深度剖析最优建堆及堆的经典应用 - 堆排列与topk问题

    🚩 纸上得来终觉浅, 绝知此事要躬行。 🌟主页:June-Frost 🚀专栏:数据结构 🔥该文章分别探讨了向上建堆和向下建堆的复杂度和一些堆的经典应用 - 堆排列与topk问题。 ❗️该文章内的思想需要用到实现堆结构的一些思想(如向上调整和向下调整等),可以在另一篇文章

    2024年02月08日
    浏览(49)
  • 【C语言】深入解析C语言中的回调函数及其应用

    目录 什么是回调函数? 回调函数有什么作用? 额外的进阶用法? 1. 传递多个参数: 2. 回调函数和数据封装: 3. 函数指针的灵活性: 回调函数的概念可能有些抽象,让我们尝试用一个简单的生活场景来解释它。假设你有一项重要任务需要完成,但任务的一部分要依赖于其他

    2024年02月12日
    浏览(52)
  • Android应用开发-Flutter的LongPressDraggable控件回调函数onDraggableCanceled使用

    以下是如何使用 onDraggableCanceled 的示例: velocity 参数表示拖动被取消时的速度信息。 offset 参数表示拖动被取消时的偏移量信息。 这个回调通常用于在拖动被取消时执行一些清理工作或展示一些反馈。例如,你可能想要将拖动对象返回到原始位置,或者显示一个提示,告诉用

    2024年03月08日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包