您现在的位置是:首页 >其他 >C生万物 | 一探指针函数与函数指针的奥秘网站首页其他

C生万物 | 一探指针函数与函数指针的奥秘

烽起黎明 2023-05-13 08:00:04
简介C生万物 | 一探指针函数与函数指针的奥秘

在这里插入图片描述

在这里插入图片描述

一、指针函数

1、定义

指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针

格式】:返回类型* 函数名(参数表)

  • 指针函数还是很好理解的,通过基本的函数来做个对比
int func(int x, int y)
int* func(int x, int y)
  • 很清楚地可以看出,【指针函数】就是普通的一个函数,只是它的返回值类型为一个指针罢了

2、示例

下面展示一个指针函数的相关案例

  • Open()函数从外界接收一个值,用于在函数内部开辟出一块大小为n的空间,然后return返回,返回类型为int*,此时外界使用int*来进行接收,就获取到了函数内部开辟出这个数组的首元素地址,然后通过循环为数组中n个元素初始化
  • 这里无需担心在函数内部开辟的这块空间的地址,因为它存放在堆上,而不是在栈上,所以不会随着函数栈帧的销毁而消亡,所以这里在举例的时候我专门去堆上面申请空间然后返回,若是返回函数中局部变量的地址,就会有很大的风险!
int* Open(int n)
{
	int* a = (int*)malloc(sizeof(int) * n);
	if (NULL == a)
	{
		perror("fail malloc");
		exit(-1);
	}
	return a;
}

int main(void)
{
	int n = 10;
	int* arr = Open(n);

	memset(arr, 0, sizeof(int) * n);

	for (int i = 0; i < n; ++i)
	{
		*(arr + i) = i + 1;
	}
	printf("Initialized Successfully
");
	return 0;
}

通过运行结果可以看出确实可以起到初始化数组的效果

在这里插入图片描述

二、函数指针

讲完指针函数,我们也来说说它的双胞胎兄弟 —— 函数指针

1、概念理清

经过上面所讲的字符指针、数组指针,相信你马上就能类比出函数指针:没错,它就是一个指针,所指向的就是一个数组

  • 在【数组指针】中我有讲到过数组名&数组名的区别,虽然它们都指向数组的首元素地址,但是在它们往后偏移时,访问的字节数却不同;既然一个数组可以取出它的地址,那么函数是否可以取出它的地址呢?一起来看看

在这里插入图片描述

  • 从打印结果可以看出无论是函数名还是&函数名,它们的地址都是相同的,这是为什么呢?这就是语法规定的,一个函数名取不取地址都是这个函数的地址,因为对于函数来说也没有什么首函数的地址,是吧

对于数组的地址,我们可以用数组指针保存起来,那函数可以吗?当然可以,使用到的就是【函数指针】

  • 那我现在想问,下面那种形式可以将函数的地址存放起来呢
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();

💡答案揭晓,就是第二个,解析如下

  • 回忆我们数组指针的写法,为了不让指针变量和[]先结合,所以在*和指针变量外加了一(),其实对于函数指针也是一样的, 若是不加这个括号的话,就会变成* pf(),pf就会优先和后面的()结合,那么这会被编译器当成是一个函数的声明
  • 加上括号后,(*pf)就会是一个指针,向外一看有个(),说明它指向一个函数,这个函数的参数就是Add形参部分两个参数的类型
  • 最后是它的返回类型,也就是这个函数的返回类型int

所以Add函数的函数指针应该写成下面这种形式

int (*pf)(int, int) = &Add;

2、如何调用函数指针?

清楚了函数该如何去声明后,那既然有了这个指针,而且它指向一个函数,是否可以通过这个指针去调用这个函数呢?

  • 调用函数肯定得传参,那我们为刚才声明的形参部分传入两个参数试试,然后再拿返回值接收一下
  • 可以看到确实可以调用Add函数进行求和计算

在这里插入图片描述

  • 不过这个编译器到底是怎么根据这个函数指针来判断去调用的Add函数,我们来对比一下
int ret = (*pf)(3, 4);
printf("ret = %d
", ret);

int ret2 = Add(1, 2);
printf("ret = %d
", ret2);

通过调试来观察可以发现,编译器很智能,确实是通过函数指针的指向去找到函数的地址

在这里插入图片描述
也可以通过汇编来看,很清晰地看出它们都去call了这个函数的地址

在这里插入图片描述


  • 上面说到无论是函数名还是&函数名,它们所取到的地址都是一样的,所以我们可以将函数指针的声明写成下面这种形式,读者可以自己去试一下,效果也是一样的
int (*pf)(int, int) = Add;
  • 那观察上面这样的声明形式,把指针变量单独抽离出来其实就是把Add赋给了pf,然后调用的时候在前面加上一个*作为解引用,取到这个函数,那其实Add和pf就是一样的,所以我们可以像pf(1, 2)这样去调用函数,具体如下
//int ret = (*pf)(3, 4);
int ret = pf(3, 4);

int ret2 = Add(1, 2);

通过运行可以发现效果也是一样的,所以前面的*其实是可以省略的,甚至你多加几个像(****pf)(3, 4)都是可以的

在这里插入图片描述

3、两道“有趣”的代码题O(∩_∩)O

通过函数指针的学习,我们来看看下面两道很有趣的代码

下面两题均来自《C陷阱与缺陷》

< 第一题 >

代码:

(*(void (*)())0)();

解析:

💬如果你是头一次看上面这段代码的话,心里一定是一个大大的问号???现在我就来解释一下

  • 本题的突破口在于这个0,仔细观察可以发现,0前面有一个括号(),括号里面的这种形式若是你自己去看的话就是一个函数指针,那相当于就是对0进行一个强制类型转换,把它变成一个函数地址,然后前面的*我们刚才讲过,就是对这个函数进行解引用,获取到这个函数。那么最后一步便是去调用这个函数

具体的分解可以看看下图👇

在这里插入图片描述
分步细说:

  1. void (*)() —— 》一个没有形参,返回类型为void的函数指针

  1. (void (*)())0 ——》 对0进行强制类型转换,使其被解释成为一个函数的地址

  1. *(void (*)())0 ——》对0地址处的函数进行解引用,获取到这个函数

  1. (*(void (*)())0)() ——》调用0地址处的函数

原文现身:
在这里插入图片描述

< 第二题 >

代码:

void (*signal(int, void(*)(int)))(int);

解析:

💬同理,若是第一次见一定会被它绕晕了😵了

  • 本题真的可以说是在套娃了,首先你看到的一定是signal,它呢是C语言中的一个信号函数,有兴趣可以去了解一下,我们知道()的优先级高于*,所以signal会和后面的内容先结合,那其实已经可以看出这是一个函数声明了。进到里面再来看看这个函数有两个参数,一个是int,一个是函数指针,那么外层的又是什么呢?
  • 仔细看下图,我将内部的signal()函数声明抽离了出来,只剩下了头和尾,你可以做一个视觉上的合并,那其实又是一个void (*)(int)的函数指针,其实这就是signal函数的返回类型,是一个函数指针

在这里插入图片描述

同样地,我们再来捋一遍

分步细说:

  1. void (*)(int) —— 》是一个函数指针,为signal函数的形参

  1. signal(int, void(*)(int)) ——》 是一个函数声明,signal与右侧的()率先结合,内部有两个形参

  1. void (*)(int) ——》也是一个函数指针,不过是作为signal函数返回类型

优化:

对于上面的这种写法你是否觉得很冗余,其实可以再度进行一个优化,那么你可能很快就看得懂了

  • 因为 void (*)(int) 是出现了两次,之前我们在C语言中有学习过typedef这个关键字,可以用来对一个很长的数据类型或者变量进行重命名,那么在这里我们也可以这样做
  • 不过呢,你要把重命名后的名字放在(*)里面,因为语法这么规定了,去掉变量名后就是它的类型
typedef void(*ptr_t)(int);
  • 于是这句代码就可以简化为下面这种形式👇注意解引用那个*不要了,函数指针这里是可以省略的
//void (*signal(int, void(*)(int)))(int);
ptr_t signal(int, ptr_t);

原文现身:

在这里插入图片描述

4、函数指针数组

指针可以存放在一个数组中,那函数指针可以吗?来看看【函数指针数组】吧,这一块可以先看看指针数组与数组指针的辨析

概念明细

  • 还记得我们学习完【数组指针】后的这道练习题吗,最后我们判定它的类型为数组指针数组,它是一个数组,里面存放的都是数组指针
int (*parr3[10])[5];
  • 那对于函数指针来说,和这个其实存在异曲同工之妙,只需要把后面的[]改为()即可,当然你也可以改个名字
int (*pfArr[10]();
  • 再来对比我们前面学习过的【函数指针】,你有发现区别在哪吗?没错,就是多了个[10],因为[]的优先级较高,所以pArr会和它先结合,那其实就可以肯定它为一个数组了
int (*pfArr)();

声明知道了,那具体怎么使用呢?怎么去接收多个函数的地址呢?再来看看

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

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

int main(void)
{
	int (* pfArr[2])(int, int) = {Add, Sub};
	
	int ret1 = pfArr[0](5, 3);
	int ret2 = pfArr[1](5, 3);

	printf("ret1 = %d
", ret1);
	printf("ret2 = %d
", ret2);
	return 0;
}
  • 很简单,上面有Add和Sub两个加与减的函数,那将它们存放到一个数组中,首先用花括号把它们括起来{Add, Sub},然后还是和函数指针一样的声明,只需要在指针变量后加上一个[2]即可,那么这就是一个【函数指针数组】
  • 接着去调用的话其实和要结合函数和数组的调用形式,既要控制数组的下标,还要考虑调用函数时传入相应的参数,如下所示👇

在这里插入图片描述
对于【函数指针数组】,我想你应该感受到了它的强大,竟然可以存放多个数组的地址然后根据不同的下标索引找到不同的函数进行调用,如果使用得当,那一定可以事半而功倍

具体应用:转移表✔

对于函数指针数组而言,有一个很经典的应用就是转移表,简单来说就是计算器

  • 首先我使用分支循环实现了简易的功能计算,代码如下
void menu()
{
	printf("**************************
");
	printf("***** 1.Add    2.Sub *****
");
	printf("***** 3.Mul    4.Div *****
");
	printf("***** 5.Cls    0.Exit*****
");
	printf("**************************
");
}

int main(void)
{
	int input = 0;
	int x = 0, y = 0;
	int ret = 0;

	do {
		menu();
		printf("请输入你的选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入两个运算数:>");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("结果为:%d
", ret);
			break;
		case 2:
			printf("请输入两个运算数:>");
			scanf("%d %d", &x, &y);		
			ret = Sub(x, y);
			printf("结果为:%d
", ret);
			break;
		case 3:
			printf("请输入两个运算数:>");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("结果为:%d
", ret);
			break;
		case 4:
			printf("请输入两个运算数:>");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("结果为:%d
", ret);
			break;
		case 5:
			system("cls");
			break;
		case 0:
			break;
		default:
			printf("请输入正确的内容:");
			break;
		}

	} while (input);
	return 0;
}

但是仔细观察可以发现,每一条case语句中,都有重复的工作,就显得很冗余,为什么每个case里都要放一个输入呢,这是我后来发现的问题,若是把这个输入放在外面的话,就会造成按下0想要退出的时候还会出现输入运算数的情况,因为这是处于一个do…while的循环之中

在这里插入图片描述

但是此处我若是利用函数指针数组的话,就会很方便了

  • 函数声明如下,将这四个加、减、乘、除的函数的地址放到数组中存起来,通过下标的方式来进行访问
int (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div};
  • 于是内部的逻辑就可以写成下面这样,通过去判断输入的input来实现不同的功能,只有当input >= 1 && input <= 4时,才进行运算,此时把输入操作符的逻辑放在这里即可,便不会影响其他功能了
do {
	menu();
	printf("请输入你的选择:>");
	scanf("%d", &input);
	
	int (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div};
	
	if (input == 0)
	{
		break;
	}
	
	if (input >= 1 && input <= 4){
		printf("请输入两个运算数:>");
		scanf("%d %d", &x, &y);
		int ret = pfArr[input](x, y);
		printf("结果为:%d
", ret);
	}
	else if (input == 5) {
		system("cls");
	}
	else {
		printf("输入有误,请重新输入
");
	}
} while (input);

5、指向函数指针数组的指针

学习了函数指针数组后,你是否有联想到取出这个数组的地址再存放到指针里去呢?这不,它来了

  • 仿照前面的写法,若现在要是一个指针的话,那你应该想要又需要*()了,因为存在优先级的问题,指针变量会和[]相结合,所以我们可以取出函数指针数组的地址,给到一个指针作为接收,这个指针即为ptr

在这里插入图片描述

  • 分解着来细说一下,首先说明一下,有些同学直接拿函数指针的*作为指针符,这是不对的, 那是用来对函数指针所指向函数的地址进行解引用的,可不能混淆,所以我们要另外再加一个*,与ptr进行结合
  • 那么此时ptr就一定是一个指针,然后朝外一看有一个数组,那它便指向一个数组,这个数组的有5个元素,每个元素的类型我们只需要拿到数组名即(*ptr)[5]即可,便发现里面存放的都是函数指针。这么分析下来这个【ptr】确实是一个指向函数指针数组的指针

在这里插入图片描述


再来看一组练习巩固一下

  • 【pfun】是一个指针,它指向一个形参类型为const char*,返回类型为void的函数
  • 【pfunArr】是一个数组,数组大小为5,里面存放的均是指向指向一个形参类型为const char*,返回类型为void的函数指针
  • 【ppfunArr】是一个指针,它指向一个数组,数组里面的都是函数指针。。。同上
void test(const char* str)
{
	printf("%s
", str);
}
int main()
{
	//函数指针pfun
	void (*pfun)(const char*) = test;
	//函数指针的数组pfunArr
	void (*pfunArr[5])(const char* str);
	pfunArr[0] = test;
	//指向函数指针数组pfunArr的指针ppfunArr
	void (*(*ppfunArr)[5])(const char*) = &pfunArr;
	return 0;
}

三、实战训练 —— 回调函数

1、回调函数的概念

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

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

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

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

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


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

3、回调函数使用场景

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

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

功能与菜单

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("**************************
");
	printf("***** 1.Add    2.Sub *****
");
	printf("***** 3.Mul    4.Div *****
");
	printf("***** 5.Cls    0.Exit*****
");
	printf("**************************
");
}

主程序与回调函数

void calc(int (*p)(int, int))
{
	int x = 0, y = 0;
	printf("请输入两个运算数:>");
	scanf("%d %d", &x, &y);
	int ret = p(x, y);
	printf("结果为:%d
", 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("请输入正确的内容:
");
			break;
		}

	} while (input);
	return 0;
}

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

在这里插入图片描述

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

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

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

① qsort函数解读

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

在这里插入图片描述

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

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

③ 使用冒泡排序模拟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++;
	}
}

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

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

在这里插入图片描述

  • 再看看内置类型

在这里插入图片描述

在这里插入图片描述

④ 原理分析

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

在这里插入图片描述

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

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

记得在一次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");
}

四、总结与提炼

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

  • 首先我们学习了什么是【指针函数】,它本质上是一个函数,只是返回值是一个指针,我通过一个示例向你演示了如何在函数内部申请出一块数组空间然后返回,给到外界做接收,再让一个指针也指向那块被申请出来的空间,这就是指针函数的一种使用场景,理解了这个遇到其他的也是换汤不换药🥄
  • 然后我们又学习指针函数的兄弟【函数指针】,虽然是两兄弟,但是却长得一点也不像,因为函数指针是一个指针,它指向了一个函数的地址,因为其调用的方式和函数类似,直接将函数名改为指针名调用即可,为了更好地理解函数指针,我带着你做了两道“很有意思”的代码题,不知道你学会(废)了没有呢。既然指针可以存放在数组中,那函数指针可以吗?当然也是可以的,那就是【函数指针数组】,它的作用可不小,还记得我们用它实现了一个转移表吗,简直就是事半而功倍
  • 在学习了基础知识后,还要进行大量的练习,我请来了函数指针的师傅 —— 回调函数,它可以被广泛应用在C/C++的实际应用开发中,通过模拟计算器的加减乘除、模拟qsort函数以及模拟文件下载模块,我们深刻地理解了回调函数的使用场景,原来实现解耦就这么容易

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

在这里插入图片描述

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。