【C语言】带你完全理解指针(四)函数指针的应用sqort函数的实现

news2024/11/26 2:42:10

前言:

本文主要是函数指针的重要应用,介绍qsort函数以及模拟实现这样一个不限制使用数据类型的快速排序函数。

回调函数

函数指针有一个非常大的作用就是实现回调函数。非常重要

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

 以上面的计算机的代码为例子观察代码可以发现 ,只有调用函数部分不一样,那么能不能定义一个cacl()函数,通过将加减乘除函数作为参数传入到calc函数中,达到在calc函数中调用加减乘除函数?

int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
 
void menu()
{
	printf("*******************************\n");
	printf("******   1.add   2.sub    *****\n");
	printf("******   3.mul   4.div    *****\n");
	printf("******   0.exit           *****\n");
	printf("*******************************\n");
}
 
void calc(int(*pf)(int, int))
{
	int a = 0;
	int b = 0;
	int ret = 0;
	printf("请输入2个操作数:");
	scanf("%d %d", &a, &b);
	ret = pf(a, b);
	printf("%d\n", ret);
}
 
int main()
{
	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 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

 图解:

回调函数的应用qsort函数

介绍一下冒泡排序

冒泡排序
有一组整数,需要排序为升序
1. 两两相邻的元素比较
2. 如果不满足顺序就交换

当前的代码只适合于整型数据

	void bubble_sort(int arr[], int sz)
	{
		int i = 0;
		//趟数
		for (i = 0; i < sz - 1; i++)
		{
			//一趟比较
			//两两相邻元素比较
			int j = 0;
			for (j = 0; j < sz - 1 - i; j++)
			{
				if (arr[j] > arr[j + 1])
				{
					int tmp = arr[j];
					arr[j] = arr[j + 1];
					arr[j + 1] = tmp;
				}
			}
		}
	}

这个冒泡排序其实是有缺陷的,它的参数是限制为只能是int类型,限制了它只能够排序整型数据!而qsort函数是一个可以用来排序任意类型数据的函数。

qsort函数介绍

qsort函数是C语言中的一个标准库函数,用于快速排序一个数组。它的原型如下:

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

参数解释:

  • void *base:指向待排序数组的起始位置的指针。也就是需要排序的数组的第一个元素
  • size_t nitems:数组中的元素个数。
  • size_t size:每个元素的大小(以字节为单位)。
  • int (*compar)(const void *, const void *):比较函数指针,用于比较两个元素的大小。

qsort函数使用了快速排序算法,它会根据比较函数的返回值对数组进行排序。比较函数的定义如下:

int compar(const void *a, const void *b)

比较函数需要返回一个整数值。如果返回值小于0,表示a应该在b之前;如果返回值大于0,表示a应该在b之后;如果返回值等于0,表示a和b的顺序不变。这个函数需要两个参数两个要比较的值。

快速排序是一种基于比较的排序算法,它的平均时间复杂度为O(nlogn),是常用的排序算法之一。通过不断地分割数组并以一个基准值为中心进行排序,快速排序可以高效地将数组排序。

使用qsort函数时,你只需要提供一个比较函数,qsort函数会自动根据这个比较函数对数组进行排序。这使得排序变得非常简单和方便。你可以使用qsort函数对任何一种数据类型的数组进行排序,只需要提供相应的比较函数。 

 比较函数的形参中为什么用的是void*

void* 是无具体类型的指针,不能直接进行解引用操作符(但是可以强制类型转换之后可以解引用),也不能进行+-整数的操作,它是用来存放任意类型数据的地址(可以理解为一个仓库,什么都能装,当需要用时再强制类型转换为需要的类型)。只有void*被允许存放任意类型数据的地址,如果是其他类型的指针编译器会报错。正是因为定义qsort函数时用的是void*,qsort函数才可以排序任意类型的数据。

 个用于排序整型数据比较函数cmp_int

int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

 做差是因为这个比较函数需要返回值,如果返回值小于0,表示e1应该在e2之前;如果返回值大于0,表示e1应该在e2之后;如果返回值等于0,表示e1和e2的顺序不变

想要排成降序把这个比较函数的两个参数减法的位置变一下就好了

 同理,qsort函数排序结构体类型数据(下面例子以结构体中的年龄来排序)

struct Stu
	{
		char name[20];
		int age;
	};
	
	int cmp_stu_by_age(const void* p1, const void* p2)
	{
		return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
	}
	
	void test2()
	{
		struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 50},{"wangwu", 15} };
		int sz = sizeof(arr) / sizeof(arr[0]); 
		qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
	}
	
	int cmp_stu_by_name(const void* p1, const void* p2)
	{
		return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
	}
	
	void test3()
	{
		struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 50},{"wangwu", 15} };
		int sz = sizeof(arr) / sizeof(arr[0]);
		qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	}
	
	int main()
	{
		//test1();
		//test2();
		test3();
	
		return 0;
	}

按年龄比较的函数中: 同样这里的指针不能直接解引用,先强制类型转换。

按名字比较的那个函数中:两个字符串比较使用strcmp函数(专门用来比较字符串的大小的)

模拟实现qsort函数 

使用【冒泡排序】的算法,模拟实现一个排序函数 bsort ,可以用来排序任意类型的数据。

在冒泡排序的基础上来实现。

冒泡排序
有一组整数,需要排序为升序
1. 两两相邻的元素比较
2. 如果不满足顺序就交换

当前的代码只适合于整型数据

	void bubble_sort(int arr[], int sz)
	{
		int i = 0;
		//趟数
		for (i = 0; i < sz - 1; i++)
		{
			//一趟比较
			//两两相邻元素比较
			int j = 0;
			for (j = 0; j < sz - 1 - i; j++)
			{
				if (arr[j] > arr[j + 1])
				{
					int tmp = arr[j];
					arr[j] = arr[j + 1];
					arr[j + 1] = tmp;
				}
			}
		}
	}

 问题:参数只能接收整型数组

参数一:void*的指针,指向需要排列的数组的第一个元素因为它是用来存放任意类型数据的地址

参数二:对照库函数qsort的定义中,第二个参数为待排序数组的元素个数,因此我们也用个 size_t num 存放待排序数组的元素个数。

当前,我们可以通过参数一和参数二知道起始位置地址(void* base)和元素个数(num),但是仅仅知道起始地址和元素个数是不够的,因为不知道一个元素有多大的,一次需要跳过多少个字节

参数三:因此还需要一个参数记录一个元素的大小 size_t size

函数内部循环的趟数和一趟的次数是不需要改造的

问题:对于不同类型的数据要有不同的比较方式,比如整型可以通过><来比较但是结构体就不行

解决方法·:将2个元素的比较方法以函数参数的形式传递

问题:不同的数据交换方式也有差异

 因为是void*类型不能够直接加减整数来移动所以要强制类型转换才行,转换为char*最为合适因为步长最短,可以满足所有类型的交换。base是下标,想要找到下一个元素的地址应该加上一个元素的大小也就是size。交换第j个元素和第j+1个元素,应该起始地址加上j*size和起始地址加上(j+1)*size

cmp((char*)base + size * j

 完成使用cmp来比较两个元素的大小关系了接下来就是完成交换的代码创建一个函数来实现交换,需要的参数肯定要这两个元素的地址,同时还要这两个元素的大小

交换函数要完成一个字节一个字节的交换因为可以实现任意类型的交换。

void Swap(char* buf1, char* buf2, int size)//交换arr[j],arr[j+1]这两个元素
{
	int i = 0;
	char tmp = 0;
	for (i = 0; i < size; i++)
	{
		tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

 比较函数cmp和主函数是由程序员B来完成的,冒泡函数是由程序员A完成的用于完成排序(任何类型)

 

int cmp_int(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}
void Swap(char* buf1, char* buf2, int size)//交换arr[j],arr[j+1]这两个元素
{
	int i = 0;
	char tmp = 0;
	for (i = 0; i < size; i++)
	{
		tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

void bubble_sort(void* base, int num, int size, int (*cmp)(const void*, const void*))
{
	int  i = 0;
	//趟数
	for (i = 0; i < num - 1; i++)
	{
		int j = 0;
		//一趟内部比较的对数
		for (j = 0; j < num - 1 - i; j++)
		{
			//假设需要升序cmp返回>0,交换
			if (cmp((char*)base+j*size, (char*)base+(j+1)*size)>0)//两个元素比较,需要将arr[j],arr[j+1]的地址要传给cmp
			{
				//交换
				Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}

}

 swap函数中临时建立一个变量用于两个变量的交换

 同理想要实现按照什么来排序就i完成什么样的比较函数cmp,比如结构体变量按照名字,按照年龄

int cmp_int(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}

 
//测试bubble_sort 排序整型数据
void test1()
{
	int arr[10] = { 3,1,5,2,4,7,9,6,8,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
}

int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}


void test3()
{
	struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 50},{"wangwu", 15} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n", sizeof(struct Stu));
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}

struct Stu
{
	char name[20];
	int age;
};


int cmp_stu_by_age(const void* p1, const void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}

测试bubble_sort 排序结构体数据
void test2()
{
	struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 50},{"wangwu", 15} };
	int sz = sizeof(arr) / sizeof(arr[0]); 
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
}

 如果想要进行降序排序的话,只需要将用户自行创建并封装的 cmp函数 中return的p1和p2交换即可

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1561364.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

广场舞团系统的设计与实现|Springboot+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读100套最新项目持续更新中..... 2024年计算机毕业论文&#xff08;设计&#xff09;学生选题参考合集推荐收藏&#xff08;包含Springboot、jsp、ssmvue等技术项目合集&#xff09; 目录 1. 系…

关于 ulimit 的两个坑

做过运维的人一定会遇到过 “Too many open files” 错误&#xff0c;这个错误本质是 ulimit 设置不合理导致的。关于 ulimit 设置&#xff0c;有哪些需要注意的点呢&#xff1f;本文给大家做一个介绍&#xff0c;希望对大家有所帮助。 如何确认 ulimit 设置生效了&#xff1f…

Go-Gin-Example 第八部分 优化配置接口+图片上传功能

文章目录 前情提要本节目标 优化配置结构讲解落实修改配置文件优化配置读取及设置初始化顺序第一步 验证 抽离file 实现上传图片接口图片名加密封装image的处理逻辑编写上传图片的业务逻辑增加图片上传的路由 验证实现前端访问 http.FileServerr.StaticFS修改文章接口新增、更新…

80C51实现四种流水灯流水灯 | 自用学习笔记

单个流水灯 #include <reg51.h> #include <intrins.h> // 包含移位库函数的头文件void delay(void) // 定义延时函数 {unsigned char i, j;for(i 0; i < 200; i) // 循环延时&#xff0c;延时约50msfor(j 0; j < 125; j); }void main(void){ unsigned ch…

代码随想录算法训练营Day41|LC343 证书拆分LC96 不同的二叉搜索树

一句话总结&#xff1a;初看觉得难&#xff0c;过几天再做依旧觉得不简单。 原题链接&#xff1a;343 整数拆分 拿到题乍一看两眼一抹黑。还是看题解吧。首先确定动规数组及下标的含义。这里就设置dp[i]为正整数i的最大乘积。 然后确定递推关系式。对于怎么求i的最大乘积&…

CCIE-01-VLAN-Trunk

目录 实验条件网络拓朴逻辑拓扑物理拓扑实验目的 开始配置配置SW1配置SW2检验证配置结果 实验条件 网络拓朴 逻辑拓扑 物理拓扑 实验目的 SW1和SW2之间的E2/0-3配置为trunk&#xff0c;使用802.1q协议&#xff0c;不需要配置捆绑根据逻辑图和物理图标识&#xff0c;使得R1~R7…

电力设备热设计原理(二)

本篇为西安交通大学本科课程《电力设备设计原理》的笔记。 本篇为这一单元的第二篇笔记。上一篇传送门。 电力设备传导换热 主要讨论稳态导热的计算。 通过单层和多层平壁的传导 如上图所示的大平板是一维传导问题&#xff0c;流过平板的热流量和平板两侧温度和平板厚度之间…

【机器学习300问】59、计算图是如何帮助人们理解反向传播的?

在学习神经网络的时候&#xff0c;势必会学到误差反向传播&#xff0c;它对于神经网络的意义极其重大&#xff0c;它是训练多层前馈神经网络的核心算法&#xff0c;也是机器学习和深度学习领域中最为重要的算法之一。要正确理解误差反向传播&#xff0c;不妨借助一个工具——计…

软件心学格物致知篇(5)愿望清单上篇

愿望清单 前言 最近发现愿望清单是一个很有意思的词&#xff0c;结合自己的一些过往经验得到一点点启发。 我发现在众多领域都有东西想伪装成它。 比如一些企业的企业战略&#xff0c;比如客户提出的一些软件需求&#xff0c;比如一些系统的架构设计指标&#xff0c;比如一…

k8s练习-创建一个Deployment

创建Deployment 创建一个nginx deployment [rootk8s-master home]# cat nginx-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata:name: nginx-deployment spec:selector:matchLabels:app: nginx # 配置pod的labelsreplicas: 2 # 声明2个副本template:metada…

2024.3.26 ARM

SPI相关理论 概述 SPI&#xff0c;是Serial Peripheral interface的缩写&#xff0c;是串行外围设备接口&#xff0c;是一种高速的&#xff0c;全双工&#xff0c;同步的通信总线&#xff0c;并且在芯片的管脚上只占用四根线&#xff0c;节约了芯片的管脚&#xff0c;同时为P…

数字孪生|山海鲸数据管家简介及安装步骤

哈喽,大家好啊,我是雷工! 最近在学习数字孪生相关的软件山海鲸,了解到采集Modbus协议需要先安装山海鲸数据管家,本节先学习数据管家及安装步骤,以下为学习笔记: 1、简介 数据管家是帮用户进行数据管理与转发的软件,能够解决山海鲸可视化等软件对数据接入过程中的许多…

实体机双系统安装

实体机双系统安装 第一步&#xff1a;下载openKylin镜像 前往官网下载x86_64的镜像&#xff08;https://www.openkylin.top/downloads/628-cn.html&#xff09; tips&#xff1a;下载完镜像文件后&#xff0c;请先检查文件MD5值是否和官网上的一致&#xff0c;如果不一致请重…

【python基础教程】2. 算法的基本要素与特性

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;python基础教程 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、…

Java中常见的锁策略

目录 乐观锁 vs 悲观锁 悲观锁: 乐观锁&#xff1a; 重量级锁 vs 轻量级锁 ⾃旋锁&#xff08;Spin Lock&#xff09; 公平锁 vs 非公平锁 可重⼊锁 vs 不可重入锁 读写锁 乐观锁 vs 悲观锁 悲观锁: 总是假设最坏的情况&#xff0c;每次去拿数据的时候都认为别…

蓝桥杯第1593题——二进制问题

题目描述 小蓝最近在学习二进制。他想知道 1 到 N 中有多少个数满足其二进制表示中恰好有 K 个 1。你能帮助他吗&#xff1f; 输入描述 输入一行包含两个整数 N 和 K。 输出描述 输出一个整数表示答案。 输入输出样例 示例 输入 7 2输出 3评测用例规模与约定 对于 30% …

YOLOv9改进策略 :主干优化 | 极简的神经网络VanillaBlock 实现涨点 |华为诺亚 VanillaNet

💡💡💡本文改进内容: VanillaNet,是一种设计优雅的神经网络架构, 通过避免高深度、shortcuts和自注意力等复杂操作,VanillaNet 简洁明了但功能强大。 💡💡💡引入VanillaBlock GFLOPs从原始的238.9降低至 165.0 ,保持轻量级的同时在多个数据集验证能够高效涨点…

跑spark的yarn模式时RM连不上的情况

在linux控制台跑spark on yarn一个测试案例&#xff0c;日志中总显示RM连yarn服务的时候是&#xff1a;0.0.0.0:8032 具体情况如下图&#xff1a; 我问题出现的原因&#xff0c;总结如下&#xff1a; 1.防火墙没关闭&#xff0c;关闭 2.spark-env.sh这个文件的YARN_CONF_DIR…

NRF52832修改OTA升级时的bootloader蓝牙MAC

NRF52832在OTA升级时&#xff0c;修改了APP的蓝牙MAC会导致无法升级&#xff0c;原因是OTA程序的蓝牙MAC没有被修改所以手机扫描蓝牙时无法连接 解决办法 在bootloader的程序里面加入修改蓝牙mac地址的代码实现原理&#xff1a; 在bootloader蓝牙广播开启之前修改蓝牙mac 通…

Java 处理Mysql获取树形的数据

Mysql数据&#xff1a; 代码如下&#xff1a; Entity&#xff1a; Data Accessors(chain true) public class Region {private BigInteger id;//名称private String name;//父idprivate BigInteger parentId;private List<Region> children;private Integer createTim…