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

C语言学习之路

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



一、什么是回调函数:

1、通俗解释

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

2、示例展示:

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

#include<stdio.h>
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<stdio.h>
#include<string.h>
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指针。并且我们将二者结合模拟实现了一下一个排列大小的函数。希望对大家有所帮助

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值