第二十章 指针进阶(4)(回调函数剖析与应用)

第二十章 指针进阶(4)(回调函数剖析与应用),第1张

C语言学习之路

第一章 初识C语言
第二章 变量
第三章 常量
第四章 字符串与转义字符
第五章 数组
第六章 *** 作符
第七章 指针
第八章 结构体
第九章 控制语句之条件语句
第十章 控制语句之循环语句
第十一章 控制语句之转向语句
第十二章 函数基础
第十三章 函数进阶(一)(嵌套使用与链式访问)
第十四章 函数进阶(二)(函数递归)
第十五章 数组进阶
第十六章 *** 作符(详解及注意事项)
第十七章 指针进阶(1)
第十八章 指针进阶(2)
第十九章 指针进阶(3)
第二十章 指针进阶(4)


文章目录
  • C语言学习之路
  • 一、什么是回调函数:
    • 1、通俗解释
    • 2、示例展示:
  • 二、利用回调函数实现排序函数:
    • 1、铺垫:
      • (1)冒泡排序:
      • (2)void指针
      • (3)qsort库函数
    • 2、自我实现my_qsort库函数
      • 代码展示:
      • 代码拆分详解
        • (1)排序函数的形参列表:
        • (2)排序函数的函数主体:
        • (3)交换两个变量的内容
  • 总结


一、什么是回调函数: 1、通俗解释

假设我们创建了一个函数A,然后这个函数A的形参列表中包含一个函数指针,调用函数A的时候,将这个函数指针指向了函数B。在我们执行函数A的过程中,在函数A的内部通过函数指针调用了函数B。那么此时被函数A调用的函数B,就叫做回调函数。

2、示例展示:

我们在上一章中讲解函数指针数组的时候,曾经写过一个实例,即简易计算器的实现。那么现在我们用回调函数的形式,再次实现一个简单计算器。

#include
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***\n");
	printf("***2、sub***\n");
	printf("***3、mul***\n");
	printf("***4、div***\n");
	printf("***0、exit**\n");
	printf("************\n");


}
void calc(int (*p)(int x, int y))
{
	int x = 0;
	int y = 0;
	printf("请输入两个 *** 作数:>\n");
	scanf("%d %d",&x,&y);
	int ret = p(x, y);
	printf("%d\n",ret);
	system("pause");
	system("cls");
}

int main()
{
	int choice = 0;
	do
	{
		menu();
		printf("请输入您的选择:>");
		scanf("%d", &choice);
		switch (choice)
		{
		case 1:
			calc(add);
			break;
		case 2:
			calc(sub);
			break;
		case 3:
			calc(mul);
			break;
		case 4:
			calc(div);
			break;
		case 0:
			printf("欢迎下次使用!\n");
			break;

		default:
			printf("您的输入有误,请重新输入!\n");
			break;
		}
	} while (choice);
	
	return 0;
}

上述代码中的clac函数就是刚才定义中的函数A,在我们调用函数A的时候,我们传入了其余实现加减乘除的函数名(即函数的地址),这些函数就是刚才定义中的函数B。然后我们在函数A的内部,通过函数指针,调用了函数B(实现各种数学运算的函数),那么这些实现数学运算的函数就叫做回调函数。

二、利用回调函数实现排序函数: 1、铺垫: (1)冒泡排序:

我们先来回顾一下之前学过的冒泡排序(升序排列)。

int main()
{
	int arr[10]={2,4,5,7,8,1,0,9,3,6};
	int sz=sizeof(arr)/sizeof(arr[0]);
	for(int i=0;i<sz-1;i++)
	{
		for(int j=0;j<sz-1-i;j++)
		{
			if(arr[j]>arr[j+1])
			{
				int temp=arr[j];
				arr[j]=arr[j+1];
				arr[j+1]=temp;
			}
		}
	}
	return 0;
}
(2)void指针

当一个新的语法出现的时候,它肯定是为了解决某些问题的。那么void指针是为了解决什么问题的呢?假设我们想在传入一个指针作为实参,但是这个指针的类型在不同的使用场景下,所需要的指针类型是不确定的。那么此时就出现了void指针,这个指针能够指向任意类型的数据的内存地址。倘若我们不使用void指针,代码的灵活性就会变得很差。

int a=0;
char b=0;
void*p=&a;
p=&b;

但是void指针并不是万能的,虽然void指针可以记录任何数据类型的内存地址,但是却不能访问或者偏移,其实原因很简单,由于其本身的数据类型不确定,所以访问空间的内存大小是不确定的,每次的偏移量也是不确定的。故void指针无法进行访问和偏移。倘若我们想让其进行偏移,就需要对void指针进行强制类型转换,将其转化为另外一种数据类型明确的指针。

(3)qsort库函数

qsort是库函数中用来排序的一个函数,其底层是用快速排序的算法实现的,但这并不是我们本章学习的重点。我们来观察一下这个函数的使用:

void qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*));

我们来分析一下这个函数的形参列表:

以上是cpp官方网站中对于这个排序函数的描述:

  • base 是一个void类型的指针,它指向的是一个数组的首元素的地址。
  • num 记录的是需要排序的数组中的元素个数。
  • size 记录的是这个数组中,每个元素所占的字节数。
  • comar 是一个函数指针,这个指针指向的是记录比较大小规则的函数。
2、自我实现my_qsort库函数

我们实现的这个函数是基于冒泡排序算法的,但是我们会借鉴库函数中的qsort的形参列表。

代码展示:
#include
#include
struct stu
{
	int age;
	char name[30];
};

//元素大小的比较规则
int int_cmp_ascend(const void* num1, const void* num2)//整型升序比较函数
{
	return(*((int*)num1)-*((int*)num2));
}


int char_cmp_ascend(const void* num1, const void* num2)//字符串型升序比较函数
{
	return strcmp((char*)num1,(char*)num2);
}


int struct_cmp_ascend_by_age(const void* num1, const void* num2)//按结构体中的姓名升序比较函数
{
	return (((struct stu*)num1)->age-((struct stu*)num2)->age);
}


int struct_cmp_ascend_by_name(const void* num1, const void* num2)//按结构体中的年龄升序比较函数
{
	return strcmp(((struct stu*)num1)->name , ((struct stu*)num2)->name);
}

//排序函数主体
void my_qsort(void*base,int num,int size,int(*cmp)(const void*num1,const void*num2))
{
	for (int i = 0; i < num-1; i++)
	{
		for (int j = 0; j < num - 1 - i; j++)
		{
			int ret=cmp((char*)base+j*size,(char*)base+(j+1)*size);
			if (ret > 0)
			{
				for (int k = 0; k < size; k++)
				{
					char temp = *((char*)base + j * size+k);
					*((char*)base + j * size + k) = *((char*)base + (j + 1) * size + k);
					*((char*)base + (j + 1) * size + k) = temp;

				}
			}
		}
	}
	return;
}
代码拆分详解 (1)排序函数的形参列表:

我们先来看函数的形参列表:

  • void* base这是一个void类型的指针,这个指针的作用是指向需要排序数组的起始元素,便于后续地比较。
  • int num是为了记录这个数组中一共有多少个元素。这是为了方便书写for循环中的循环次数。
  • int size是为了记录每个元素所占的字节数。
  • int(*cmp)(const void*num1,const void*num2)是一个函数指针,这个指针所指向的函数的返回值是int类型,这个函数的形参列表是两个const修饰的void类型的指针。
(2)排序函数的函数主体:

接着我们来分析一下这个函数主体中的代码段的背后逻辑:首先我们这个排序函数所用的算法是比较简单的冒泡排序算法。我们根据形参列表中传入的数组中的元素个数,能够写出循环的次数。然后我们将比较函数的返回值作为if条件语句中的条件。
我相信大家最难理解的其实是二者比较的逻辑。我们传入cmp函数中的两个参数肯定是指向被比较的两个元素的地址。而这两个地址肯定是通过数组首地址偏移得到的。但是我们知道void指针是无法进行访问和偏移的。所以我们需要对该指针进行强制类型转换,这个时候我们形参列表中的size就可以派上用场了。我们将void类型的指针转换成char类型后,每次指针的偏移量就是一个字节。然后我们通过(j*size)就能够确定指针需要移动的字节数。这样我们就解决了cmp函数中的形参问题。然后我们来看一下当两个元素符合条件后,如何交换这两个元素。

(3)交换两个变量的内容

我们平常交换两个数值使用的方法是创建一个临时变量,但是这种方法适用于已知数据类型的变量。但是我们现在所写的函数内部的数值交换,是不知道两个数值的数据类型的。那我们应该怎么办呢?我们知道,每一个数据类型的变量最后本质上在存储的时候都是二进制数。那么我们只要交换两个变量中的每一个字节所对的二进制序列即可。那么想要交换每个字节的二进制位,我们就需要写一个循环,而这个循环中的指针还需要是char类型的才行,因为char类型的单位偏移量是一个字节。而循环的次数即size。


总结

本章重点讲解了两个问题,一个是回调函数一个是void指针。并且我们将二者结合模拟实现了一下一个排列大小的函数。希望对大家有所帮助

欢迎分享,转载请注明来源:内存溢出

原文地址:https://www.54852.com/langs/2889394.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-09-14
下一篇2022-09-14

发表评论

登录后才能评论

评论列表(0条)

    保存