当前位置: 首页 > news >正文

【C语言篇】深入理解指针4(模拟实现qsort函数)

文章目录

  • 回调函数是什么
  • qsort函数介绍和使用举例
    • qsort函数介绍
    • qsort函数排序整型数据
    • 使用qsort排序结构数据
  • qsort函数的模拟实现
  • 总结
  • 写在最后

回调函数是什么

回调函数就是⼀个通过函数指针调⽤的函数

如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数时,被调⽤的函数就是回调函数。

回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。

回想一下我们在设计一个计算器的时候:

需要写加减乘除函数如下:

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

在使用回调函数改造前:

  • 可以发现存在很多冗余的地方,在每个分支都需要书写相同的scanfprintf语句
int main()
{int x, y;int input = 1;int ret = 0;do{       printf("*********************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf("*********************\n");printf("请选择:");scanf("%d", &input);switch (input){case 1:printf("输⼊操作数:");scanf("%d %d", &x, &y);ret = add(x, y);printf("ret = %d\n", ret);break;case 2:printf("输⼊操作数:");scanf("%d %d", &x, &y);ret = sub(x, y);printf("ret = %d\n",ret);break;case 3:printf("输⼊操作数:");scanf("%d %d", &x, &y);ret = mul(x, y);printf("ret = %d\n", ret);break;case 4:printf("输⼊操作数:");scanf("%d %d", &x, &y);ret = div(x, y);printf("ret = %d\n", ret);break;case 0:printf("退出程序\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}

我们发现调用的函数都是int (int,int)类型的,我们可以把调⽤的函数的地址以参数的形式传递过去,使⽤这样类型的函数指针接收,函数指针指向什么函数就调⽤什么函数,这⾥其实使⽤的就是回调函数的功能。

void calc(int(*pf)(int, int))
{int ret = 0;int x, y;printf("输⼊操作数:");scanf("%d %d", &x, &y);ret = pf(x, y);printf("ret = %d\n", ret);
}

使用回调函数改造后:

int main()
{int x, y;int input = 1;int ret = 0;do{       printf("*********************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf("*********************\n");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;
}

注意区分这和我们在【C语言篇】深入理解指针3(附转移表源码)中实现的转移表,这里使用的是回调函数,但在转移表中我们使用的是函数指针数组


qsort函数介绍和使用举例

qsort函数介绍

在这里插入图片描述

void qsort(void* base, //指向待排序数组的第一个元素的指针size_t num, //base指向数组中的元素个数size_t size,//base指向的数组中一个元素的大小,单位是字节int (*cmp)(const void*, const void*) //函数指针 - 传递函数的地址    );
  • 头文件为stdlib.h

    在这里插入图片描述

  • 参数四个介绍如上

  • 对最后一个参数特别介绍一下:

    • 函数指针,指向的函数是用来比较待排序数组的元素大小的

    • 由使用qsort函数的用户来实现

    • 在这里插入图片描述

      • 如果p1指向的元素小于p2,则返回小于0的数字
      • 如果二者相等,则返回0
      • 如果p1指向的元素大于p2,则返回大于0的数字
  • qsort函数默认是排升序,如果想排降序,则在compare函数里将上述规则反一下即可,即当p1指向的元素小于p2时返回大于0的数字

qsort函数排序整型数据

#include <stdio.h>
#include <stdlib.h>//qosrt函数的使⽤者得实现⼀个⽐较函数int int_cmp(const void * p1, const void * p2)
{return (*( int *)p1 - *(int *) p2);
}
int main()
{int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };int i = 0;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;
}

使用qsort排序结构数据

struct Stu //学⽣
{char name[20];//名字int age;//年龄
};
//假设按照年龄来⽐较
int cmp_stu_by_age(const void* e1, const void* e2)
{return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
//strcmp - 是库函数,是专⻔⽤来⽐较两个字符串的⼤⼩的
//假设按照名字来⽐较
int cmp_stu_by_name(const void* e1, const void* e2)
{return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
//按照年龄来排序
void test2()
{struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
//按照名字来排序
void test3()
{struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{test2();test3();return 0;
}

qsort函数的模拟实现

使用回调函数,模拟实现qsort

注意:

  • qsost底层采用的是快速排序的方法,在这里我们使用更简单的冒泡排序的排序算法来模拟实现qsort函数,对快排想要了解更多的读者可以看看【初阶数据结构篇】冒泡排序和快速排序(中篇)
  • 这里使用void*的指针,以实现泛型编程

在实现前我们先温故一下冒泡排序
在这里插入图片描述

  • 总共n个数据,要排n-1趟
  • 第i(i从0开始取)趟要比较n-1-i次
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 print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}
}int main()
{int arr[] = { 3,1,7,9,4,2,6,5,8,0 };//排序 - 升序int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz);print_arr(arr, sz);return 0;
}

冒泡排序只需要两个参数,待排序数组和数组元素个数

我们要实现的qsort是可以针对任何数据进行排序,那想一下我们知道用户使用这个函数的时候是拿来排序什么数据吗?显然是不知道的,所以在内部实现时,我们需要更改什么呢?分析如下:

  • 首先是趟数和一趟之内的比较次数,这是冒泡算法,无论什么数据都不需要改变整体的大框架

重点在于以下两点:

  1. 比较的方式
  • 由于不知道用户排序的数据类型,传过来的数组首元素地址我们必须使用void*指针接收,是不能进行解引用的,且数据类型是不能传参的,那我们该怎么找到相邻元素比较呢?

于是我们在参数中添加了数组元素的大小(即宽度,一个元素占几个字节,这是用户可以传参的),这样就能找到相邻元素了
在这里插入图片描述

(char*)base + j * width
(char*)base + (j + 1) * width) 

这样在内层循环中就能依次找到两个相邻元素了

接下来就是如何比较,由于我们不知道用户排序什么数据,所以没办法实现两个数据的比较,例如整数可以直接使用关系操作符,而字符串需要strcmp函数等等,于是我们把比较两个数据大小的函数交给用户去实现,所以在参数中使用了一个函数指针

这样比较两数的方式就更改完毕了

if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
  • 这里我们默认还是qsort的比较规则,用户实现compare函数时如果遵守:当第一个元素大于第二个元素时,就返回大于0的数字,此时我们交换,按这个规则排序出来为升序,反之为降序

  1. 交换数据的方式
  • 同样的是,我们不知道数据类型,但我们知道数据的大小,所以我们可以一个一个字节的交换
    在这里插入图片描述
void Swap(char* buf1, char* buf2, size_t width)
{int i = 0;char tmp = 0;for (i = 0; i < width; i++){tmp = *buf1;*buf1 = *buf2;*buf2 = tmp;buf1++;buf2++;}
}

这样我们就完成了qsort函数的模拟实现

如下:

void Swap(char* buf1, char* buf2, size_t width)
{int i = 0;char tmp = 0;for (i = 0; i < width; i++){tmp = *buf1;*buf1 = *buf2;*buf2 = tmp;buf1++;buf2++;}
}void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2))
{//趟数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])//比较两个元素if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0){//交换两个元素Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);}}}
}

总结

本篇模拟实现qsort函数是很典型的回调函数的例子,因为不知道用户排序数据的类型,所以qsort函数的实现方把比较两个数据的函数交给用户自己去实现,这个函数通过函数指针传递给qsort,在qsort函数内部发生比较时再根据函数指针调用这个比较函数,这种就是回调函数

同时,在qsort函数的实现中,我们多次使用了void*指针

  • void* base用以接收不同类型的数组
  • 规定compare函数参数设置为两个const void*,用以接收不同的数据类型,用户使用时知道排序什么数据进行强制类型转换后再使用

巧妙地使用void*指针实现了对不同数据排序,这种编程也叫做泛型编程


写在最后

C语言指针是一个重头戏,关于指针的内容会有4-5篇博客,敬请期待喔💕

以上就是关于深入理解指针4的内容啦,各位大佬有什么问题欢迎在评论区指正,您的支持是我创作的最大动力!❤️

在这里插入图片描述

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • DIGITEL1500Ion Pump System离子泵系统控制器说明含电路图电路板图
  • 理解和控制 OBS 媒体源 - 前提条件与操作
  • C++ //练习 17.2 定义一个tuple,保存一个string、一个vector<string>和一个pair<string, int>。
  • ECMAScript6中的模块:export导出、import导入
  • MFC将类A中的事件在类B中处理采用回调函数实现
  • MySQL中的distinct和group by哪个效率更高?
  • vba代码插入折线图
  • SQL进阶技巧:如何不使用union all进行行转列?【三种方法实现】
  • 【机器学习】深度学习实践
  • tea-入土为安的第十七天
  • 基于三帧差算法的运动目标检测系统FPGA实现,包含testbench和MATLAB辅助验证程序
  • [C++][opencv]基于opencv实现photoshop算法图像剪切
  • Java的jdk配置成功,但是输入java -version等,命令行没有任何反应
  • 【区块链+商贸零售】吾卡数字预付式消费服务平台 | FISCO BCOS应用案例
  • SpringBoot-配置加载顺序
  • Effective Java 笔记(一)
  • JavaScript新鲜事·第5期
  • js递归,无限分级树形折叠菜单
  • Laravel Telescope:优雅的应用调试工具
  • node.js
  • October CMS - 快速入门 9 Images And Galleries
  • rabbitmq延迟消息示例
  • React as a UI Runtime(五、列表)
  • React-flux杂记
  • vue学习系列(二)vue-cli
  • win10下安装mysql5.7
  • yii2权限控制rbac之rule详细讲解
  • 关于使用markdown的方法(引自CSDN教程)
  • 机器学习学习笔记一
  • 罗辑思维在全链路压测方面的实践和工作笔记
  • 普通函数和构造函数的区别
  • 前言-如何学习区块链
  • 浅谈Golang中select的用法
  • 浅析微信支付:申请退款、退款回调接口、查询退款
  • 学习JavaScript数据结构与算法 — 树
  • 一起来学SpringBoot | 第三篇:SpringBoot日志配置
  • 移动端高清、多屏适配方案
  • ​LeetCode解法汇总1410. HTML 实体解析器
  • #define与typedef区别
  • #vue3 实现前端下载excel文件模板功能
  • #window11设置系统变量#
  • $.each()与$(selector).each()
  • (3)(3.5) 遥测无线电区域条例
  • (笔试题)合法字符串
  • (附源码)计算机毕业设计ssm基于B_S的汽车售后服务管理系统
  • (三)docker:Dockerfile构建容器运行jar包
  • (五)网络优化与超参数选择--九五小庞
  • (转载)hibernate缓存
  • .dat文件写入byte类型数组_用Python从Abaqus导出txt、dat数据
  • .NET Core 将实体类转换为 SQL(ORM 映射)
  • .Net Core 微服务之Consul(三)-KV存储分布式锁
  • .NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划
  • .net web项目 调用webService
  • .net 获取某一天 在当月是 第几周 函数
  • .net 写了一个支持重试、熔断和超时策略的 HttpClient 实例池