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

news2024/11/26 4:33:17

文章目录

  • 回调函数是什么
  • 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的内容啦,各位大佬有什么问题欢迎在评论区指正,您的支持是我创作的最大动力!❤️

在这里插入图片描述

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

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

相关文章

豆包MarsCode:开启AI辅助编程的新时代

文章目录 引言MarsCode简介主要功能安装步骤JetBrains 安装1. /doc 文档生成2. /fix 智能修复 AI Fix3. /test 单元测试生成 使用步骤深入与最佳实践常见问题解答结语学习资源互动环节 引言 在人工智能技术飞速发展的今天&#xff0c;编程领域的创新也在不断涌现。豆包MarsCod…

从繁琐到高效:采购合同管理软件如何使企业受益

企业普遍面临由合同管理复杂性引发的压力&#xff0c;这已成为一个普遍现象。手动处理和整理大量的关键信息&#xff0c;同时确保所有文档的透明度和一致性&#xff0c;无疑是一项重大挑战。为了降低流程时间并提高效率&#xff0c;采用有效的工具显得尤为必要。 然而&#xf…

44.【C语言】指针(重难点)(G)

目录 19.字符指针变量 *定义 *简单说明 *如果是字符串 *像数组一样指定访问常量字符串的字符 *练习 20.数组指针变量 *定义 *格式 *例子 问题1 问题2 *利用指针打印 21.二维数组传参的本质 往期推荐 19.字符指针变量 *定义 指向字符的指针变量&#xff0c;用于存储字符在内存…

传统软件开发和敏捷软件开发之间的区别

传统软件开发与敏捷软件开发都是系统软件设计开发的方式&#xff0c;都是软件设计的重要类型。 1. 传统软件开发 1.1 基本流程 传统软件开发是用于设计和开发简单软件的软件开发过程。当软件的安全性和许多其他因素不太重要时使用它。它由新手用来开发软件。它包括五个阶段&…

掌控库存,简化管理 — InvenTree 开源库存管理系统

InvenTree &#xff1a;简化您的库存管理&#xff0c;让效率和控制力触手可及。- 精选真开源&#xff0c;释放新价值。 概览 InvenTree&#xff0c;一款专为精细化库存管理而设计的开源系统&#xff0c;以其高效和灵活性在众多库存管理工具中脱颖而出。它以Python和Django框架…

详谈平衡二叉搜索树(AVL树)

文章目录 AVL树的概念AVL树节点AVL树的插入AVL树的旋转新节点插入较高左子树的左侧---左左&#xff1a;右单旋新节点插入较高右子树的右侧---右右&#xff1a;左单旋新节点插入较高左子树的右侧---左右&#xff1a;先左单旋再右单旋新节点插入较高右子树的左侧---右左&#xff…

Vue - 详情介绍v-emoji-picker、vue3-emoji-picker和vue3-emoji表情包组件

Vue - 详情介绍v-emoji-picker、vue3-emoji-picker和vue3-emoji表情包组件 本篇详情介绍在Vue2.x和Vue3.x中使用&#xff08;emoji&#xff09;表情包组件&#xff0c;通过提供直观、易于使用的emoji表情选择功能&#xff0c;增强用户在使用Web应用时的表达力和互动性。 1. v…

Linux驱动开发基础(总线驱动设备模型)

所学来自百问网 目录 1.驱动设计的思想&#xff1a;面向对象/分层/分离 1.1 面向对象 1.2 分层 1.3 分离 2.总线驱动设备模型 2.1 相关函数和结构体 2.1.1 platform_device 2.1.2 platform_driver 2.1.3 相关函数 2.2 platfrom_driver和platfrom_device的注册过程 …

解决麒麟 V10 SP1 升级 Python 后 Yum 不可用问题

目录 一、前提概要 二、解决办法 1、卸载原有的 python 2、安装 Python 3.7.9 rpm 3、安装一系列 yum 相关 rpm 4、rpm 包下载 一、前提概要 在部署 gaussDB 的时候&#xff0c;安装代理时要求 python 版本满足 3.7.9&#xff0c;但已安装的麒麟 V10 内集成的 python 版…

GitHub Actions 遭利用,14个热门开源项目令牌泄露风险激增

近日&#xff0c;有攻击者通过 CI/CD 工作流中的 GitHub Actions 工具窃取了谷歌、微软、AWS 和 Red Hat 等多个知名开源项目的 GitHub 身份验证令牌。 窃取这些令牌的攻击者可在未经授权的情况下访问私有存储库、窃取源代码或向项目中注入恶意代码。 Palo Alto Networks Un…

【STM32 Blue Pill编程】-STM32CubeIDE开发环境搭建与点亮LED

开发环境搭建与点亮LED 文章目录 开发环境搭建与点亮LED1、STM32F103C8T6及STM32 Blue Pill 介绍2、下载并安装STM32CubeIDE3、编程并点亮LED3.1 在Stm32CubeIDE中编写第一个STM32程序3.1.1 创建项目3.1.2 设备配置3.1.2.1 系统时钟配置3.1.2.2 系统调试配置3.1.2.3 GPIO配置3.…

饲料粉碎加工:玉米豆粕小麦秸秆破碎机械设备

饲料粉碎机是一种专门用于将各种原料如玉米、小麦、豆粕、秸秆等物料进行破碎、细化的机械设备。其工作原理主要依赖于旋转的刀盘或锤片&#xff0c;在高速旋转过程中产生强大的冲击力和剪切力&#xff0c;将物料粉碎至所需粒度。这一过程不仅提高了饲料的利用率&#xff0c;还…

鸿蒙环境和模拟器安装

下载华为开发者工具套件&#xff0c;并解压 https://developer.harmonyos.com/deveco-developer-suite/enabling/kit?currentPage1&pageSize10 双击dmg安装ide 复制并解压sdk 安装模拟器 https://yuque.antfin-inc.com/ainan.lsd/cm586u/po19k1mi9b2728da?singleDoc#…

Unity大场景切换进行异步加载时,如何设计加载进度条,并配置滑动条按照的曲线给定的速率滑动

一、异步加载场景的过程 1、异步加载场景用到的API LoadSceneAsync 2、异步加载的参数说明 &#xff08;1&#xff09;默认参数&#xff1a;SceneManagement.LoadSceneAsync(“SceneName”); AsyncOperation task SceneManager.LoadSceneAsync("SceneName");&a…

James Forshaw的.NET Remoting反序列化升级版之TypeFilterLevel.Low模式无文件payload任意代码执行

引用 这篇文章的目的是介绍一款基于James Forshaw的.NET Remoting反序列化工具升级版在TcpServerChannel的TypeFilterLevel.Low模式无文件payload任意代码执行poc的开发心得 文章目录 引用简介.NET Remoting的应用程序通道介绍.NET Remoting的应用程序利用场景介绍扩展ysoseria…

【卫星影像地图API】常见地图服务_WMS_WFS_WCS_ WMTS

地图服务作为一种展现数据集的良好方式&#xff0c;为地理信息的共享起到重要作用。本文将介绍常见地图服务的相关内容。 网络地图服务&#xff08;WMS&#xff09; &#xff08;1&#xff09;概念 网络地图服务 (Web Map Service&#xff0c;WMS)指从地理信息动态产生具有地…

Docker最佳实践进阶(二):Docker Compose容器编排

大家好&#xff0c;在上篇文章中博主演示了Dockerfile常用的命令&#xff0c;以及如何利用Dockerfile构建镜像&#xff0c;生成容器服务&#xff0c;但是在实际应用环境中&#xff0c;特别是在微服务架构中&#xff0c;一个应用系统可能包含多个微服务&#xff0c;每个微服务可…

遇到的基本问题

遇到的基本问题 Linux常用操作 1、关闭防火墙、配置本地yum源、添加静态网卡 systemctl stop firewalld if [ getenforce "Enforcing" ];thensetenforce 0 fi sleep 3 echo "防火墙和selinux高级权限管理已关闭" ############ #添加静态网卡 #########…

配置访问权限|预防数据泄漏

IT行业正在以闪电般速度发展&#xff0c;而网络攻击也随之激增。在今年4月份的IT数据泄漏报告中&#xff0c;教育行业数据泄漏事件数量最多&#xff0c;其次是医疗保健行业、IT服务和软件行业。 为什么有许多数据泄漏事件&#xff1f; 通常是由于缺乏访问权限的认证&#xff0…

渗透实战——为喜欢的游戏“排忧解难”

本文仅用于技术研究学习&#xff0c;请遵守相关法律&#xff0c;禁止使用本文所提及的相关技术开展非法攻击行为&#xff0c;由于传播、利用本文所提供的信息而造成任何不良后果及损失&#xff0c;与本账号及作者无关。 资料查询来源- 安全社区与AI模型结合探索【文末申请免费…