C语言中的回调函数(函数指针的应用,qsort的理解和用法)

1. 回调函数

回调函数通俗的讲就是通过函数指针(作为另一个函数形参的)调用的函数。
一般我们是把回调函数名(地址)作为参数,传递给另一个函数,在另一个函数中通过该回调函数指针,去调用它所指向的函数。

其实是什么不重要,主要是一个函数指针的应用而已。


2. void* 指针类型的基本认识

首先 void* 指针类型是一个可以接受任意类型的指针类型,其通用性非常广,
比如:

int *p_int;
void* p1 = p_int; //void 接收 int*类型的指针;
double *p_double;
void* p2 = p_double; //void*接收double*类型的指针;
char *p_char;
void* p3 = p_char; //void*接收 char*类型的指针

int arr[] = {0};
void * p4 = arr; //void*接收数组类型的指针

struct person
{
	char [20];
	int age;
}* p_struct;

void *p5 = p_struct; //void*接收结构体类型的指针

虽然 void* 可以接收任意类型的指针,但是 ,void * 类型的指针不可以加减整数,其实很好理解,指针加减整数,表示跳过指针指向的变量类型单位;而 void* 并不知道,具体指针指向什么类型,所以不可以加减整数;
void * 类型的指针还不可以解引用指针,访问指针指向的变量,也很容易理解,解引用指针得到的是指针指向变量的内容,void* 是任意类型,不知道具体指向什么类型;


3. qsort 的基本使用

qsort 是一个C语言的库函数,英文全称为 quick sort,快速排序,是一种可以快速排序任何类型数据一种排序算法。

我们首先看看,qsort 函数的原型:

void qsort(void* base, 
		size_t num, 
		size_t width,
		int(*cmp)(const void* e1,const void* e2))

解释一下,函数的参数:

1. void * base ,这个base表示数组的首地址,接收任意类型的数组类型;
2. size_t num, 这个num表示数组元素个数;
3. size_t width, 这个表示数组每个元素的大小;
4. int(*cmp)(const void* e1,const void* e2))cmp表示函数指针,指向的函数返回类型为 int ,参数类型为 两个 const void* , 其中,e1 ,e2, 表示要比较的数据, const void* 说明这两个数据是任意类型的,且通过函数指针cmp调用的函数里面不可以修改e1e2的值。
5. 第4 个形参,当函数返回结果大于 0 ,表示 e1>e2, 等于 0 ,表示相等,小于 0 ,表示 e1 < e2;


这个函数如何使用呢?
记得包含头文件 # include<stdlib.h>
就是一个排序函数,
来看看几个排序的案例:
假如我们排序 int 类型的一维数组数据

//qsort的第四个参数,必须要一个比较函数(也是回调函数),
//所以是需要自己实现的,这个函数是需要配合qsort使用,
int int_cmp(const void * p1, const void * p2)
{
 return (*( int *)p1 - *(int *) p2);
}
# include<stdio.h>
# include<stdlib.h>
int main()
{
  int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
  int i = 0;
 
 //qsort的使用
  qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
  
  for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
 {
   printf( "%d ", arr[i]);
 }
  printf("\n");
  return 0;
}
//最终打印结果 :0 1 2 3 4 5 6 7 8 9

其实代码很好理解:重点我们关注的是 int int_cmp(const void * p1, const void * p2) 这个函数,
我们在设计这个比较函数(也是回调函数),通过 qsort的第四个形参int(*cmp)(const void* e1,const void* e2)
可以知道我们需要设置一个函数的返回类型 为 int ,形参参数类型为 两个 const void*,
所以就得到了这个函数,那么如何比较呢?也就是函数体如何实现呢?
因为我们知道,void* 类型指针,是无法指针相减的,所以我们需要把 传进来给 int_cmp的函数形参在函数体里面转化一下类型,由于要比较的的是整数,所以转化为整数的指针类型。即( int *)p1,和(int *) p2,由于,这个函数完成的功能是比较大小,并且需要返回值,只要通过两个指针再解引用,就可以得到大小了,所以return (*( int *)p1 - *(int *) p2);


排序 结构体 看看

sturct Person
{
	char name[20];
	int age;
};

//通过年龄比较
int cmp_struct_age(const void* e1,const void* e2)
{
	return ( (struct Person*)e1 -> age - (struct Person*)e2 -> age );
}
//通过年龄比较
int cmp-struct_name(const void* e1,const void* e2)
{
	//比较名字,是字符串,需要调用strcmp()函数。
	return strcmp( (struct Preson*)e1->name,(struct Person*)e2->name);
}
int main 
{
	struct Person p[3] = {{"zhangsan",20},{“lisi”,38},{"wangwu",27}};
	//通过年龄排序
	qsort(p,sizeof(p) / sizeof(p[0]),sizeof(p[0]),cmp_strcut_age );
	//通过名字排序
	qsort(p,sizeof(p) / sizeof(p[0]),sizeof(p[0]),cmp_strcut_name);
	return 0;
}

总结:其实,qsort的基本使用也不是很难,最主要的是,要处理好回调函数的书写。


4. 利用函数指针书写排序任意类型的冒泡排序

其实也是函数指针的应用,在这里带大家分析一下如何设计任意类型的冒泡排序算法。
普通版本的冒泡排序相信大家见过很多了,即就是排序int类型的数据。
但,就这里我想利用void* 和 函数指针的方法来实现一种任意排序的冒泡排序;
首先简单回忆一下冒泡排序的普通写法:

void swap(int*x,int*y)
{
	int temp = *x;
	*x = *y;
	*y = temp;
}
void bubble_sort(int * arr,int size)
{
	for(int i = 0;i < size -1;i++)
	{
		for(int j = 0;j< size - 1 -i;j++)
		{
			if(arr[j] > arr[i])//升序
				swap(&arr[j],&arr[j+1]);
		}
	}
}

这就是简单版本的冒泡排序啦。

那任意类型版本的呢?
首先我们要知道,既然是任意类型的冒泡排序,就参数来说,肯定不一样,那到底怎么设计呢?
我们可以参照一下 qsort 函数的设计,虽然 qsort 函数的源码我们没见过,但这不重要,重要的是我们可借鉴一下它的参数设计

  • 既然是任意类型的排序,那么必须有一个接收任意类型的指针,void* base 指向数组的起始地址;
  • 其次,我们需要知道排序的元素个数,即数组的元素个数,由于个数肯定不为负数,所以把 参数设置为size_t num,表示数组元素的个数;
  • 还有我们参照qsort,自然而然也会设计一个参数为 size_t width表示每个元素的大小,至于这个具体怎么用,等下分析设计时候,就会派上用场;
  • 最后,就是需要一个函数指针了,指向回调函数,这个函数指针很关键,因为在函数体内部,我们需要借助函数指针,去调用函数, 该函数指针 设计 为 int (*cmp)(cosnt void*e1,cosnt void* e2);
开始行动,首先我们先把设计参数写上:
void bubble_sort(void* base,size_t num,size_t width,int(*cmp)(const* e1,const* e2))
{
	//排序的框架逻辑
}

其次,如何思考呢? 冒泡排序的框架总体还是不变的,
所以我们在函数体内还是需要两个for循环,外循环控制
排序的趟数,内循环控制排序一趟需要排多少次;
即
void bubble_sort(void* base,size_t num,size_t width,int(*cmp)(const* e1,const* e2))
{
	//排序的框架逻辑
	for(int i = 0;i<num - 1;i++)
	{
		for(int j = 0;j<num - 1 -i;j++)
		{
			//交换逻辑,任意类型的交换逻辑
			//1. 比较
			//2. 交换
			if()
		}
	}
}

关键步骤来了,就是这个任意类型数据的交换逻辑,到底怎么实现比较好呢?

还记得传参进来的函数指针 cmp吗?就是在这里发生作用:
cmp要做的事情是就是:通过函数指针cmp去调用一个函数,
该函数的返回类型为int ,参数列表 为 const* ,
调用的这个函数做什么事情呢?要做的事情就是在里比较任意类型数据的大小,
这不再是直接获得数组元素的下标,就可以比较大小的了,因为这是任意类型的比较,
我们并不清楚,你传过来到底是什么类型的数组,即相邻两个元素相差多数个字节,
所以比较时候,并不是获得数组下标元素就可以比较的,说了那么多,到底如何比较呢?
首先通过 cmp 调用,需要传参为任意类型的实参,比较任意类型数组的相邻两个元素,
那么这相邻的两个元素如何表示呢?
有人说,直接传参给base, base+1两个实参 cmp,不就可以表示相邻两个元素了嘛。
错,这是不行的,因为,base 的类型为 void* ,不可以加整数。
那么必须表示相邻的,有一种写法 把 base转换为 char* ,然后再+每个元素的字节大小,就可以了。
这样就可以精准定位到相邻元素相差的字节个数了。
那么就可以表示 相邻元素了,即传参 base,(char*)base+width给cmp的函数指针调用;
但是,我们不是比较一次,是需要比较很多次,那如何表示很多次的呢? 即 传参 (char*)base + j*width,(char*)base+(j+1)*width;给 cmp函数指针调用。
好好体会一下这个实参传入的写法;
所以继续完善代码:
void bubble_sort(void* base,size_t num,size_t width,int(*cmp)(const* e1,const* e2))
{
	//排序的框架逻辑
	for(int i = 0;i<num - 1;i++)
	{
		for(int j = 0;j<num - 1 -i;j++)
		{
			//交换逻辑,任意类型的交换逻辑
			//1. 比较
			//通过函数指针调用回调函数,返回值 > 0 即要交换
			if(cmp( (char*)base + j*width,(char*)base+(j+1)*width) > 0) 
			//2. 交换
			
		}
	}
}
最后,来到交换逻辑,当我们交换数据后,如何交换呢?
我们知道在上面的比较时候,是转化为 char*类型比较,
因为这样是可以精确得到每个字节,再加上宽度width就可以
准确的清楚知道每个元素的字节宽度,这样就可以找到相邻的元素了,
在交换函数中,传入要交换两个相邻数的地址,也要设计为 char*,
实际在交换函数里面,就是交换两个相邻元素地址对应的元素,
但是我们传入的是 char* 啊,最多也是交换 一个字节。所以这个时候,还是需要一个参数
宽度, size_t width,这样 for循环,一个一个字符交换,到 width的字节个数停止,
等循环结束,也是字符交换完毕,也就是元素交换成功。

void swap(char* buff1,char* buff2,size_t width)
{
	for(int i = 0;i<width;i++)
	{
		char temp = *buff1;
		*buff1 = *buff2;
		*buff2 = temp;
		buff1++;
		buff2++;
	}
}

所以我们最终写出了任意类型的冒泡排序
整理最终代码

void swap(char* buff1,char* buff2,size_t width)
{
	for(int i = 0;i<width;i++)
	{
		char temp = *buff1;
		*buff1 = *buff2;
		*buff2 = temp;
		buff1++;
		buff2++;
	}
}

void bubble_sort(void* base,size_t num,size_t width,int(*cmp)(const* e1,const* e2))
{
	//排序的框架逻辑
	for(int i = 0;i<num - 1;i++)
	{
		for(int j = 0;j<num - 1 -i;j++)
		{
			//交换逻辑,任意类型的交换逻辑
			//1. 比较
			//通过函数指针调用回调函数,返回值 > 0 即要交换
			if(cmp( (char*)base + j*width,(char*)base+(j+1)*width) > 0) 
			//2. 交换
			swap((char*)base + j*width,(char*)base+(j+1)*width));
			}	
		}
	}
}
  • 11
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呋喃吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值