回调函数的基本使用

news2024/11/18 15:32:04

🏖️作者:@malloc不出对象
⛺专栏:《初识C语言》
👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈
在这里插入图片描述

目录

    • 前言
    • 一、什么是回调函数
    • 二、为什么要使用回调函数
    • 三、回调函数的意义
    • 四、详解qsort函数
      • 4.1 qsort函数的功能
      • 4.2 qsort函数的参数
      • 4.3 qsort函数的基本使用
      • 4.4 compar函数的作用机理
      • 4.5 qsort函数总结
    • 五、void*的用法及注意事项
    • 六、利用冒泡排序实现对各种类型的排序


前言

本篇文章主要给大家介绍的是回调函数,兴许有很多读者没有听到过这个名词,它的用途还是比较多的,最重要的是有很好的灵活性以及封装性等性质,关于回调函数最经典涉及到C语言中的一个qsort排序库函数,本文也将着重分析一下qsort函数的实现细节。

一、什么是回调函数

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

下面我们就来看看回调函数的基本用法:

void Print(char *str)
{
    printf("hello %s!\n",str);
}
void test(void (*p)(char*))
{
    (*p)("hncu");
}
int main()
{
    test(Print);//这里将Print函数名作为实参传递,所以test函数形参要用一个函数指针来接收Print函数名
   			   //在调用test函数进入到其代码块时,通过函数指针p调用指向其所指向的函数Print,此时Print函数就是一个回调函数,它不是被直接调用的而是在test函数中被间接调用的
    return 0;
}

通过这个例子,其实我们可以更通俗的来理解一下回调函数:被回调的函数、 回头执行调用动作的函数;先被当做函数指针传入、后面又被回调的函数就是回调函数。

二、为什么要使用回调函数

很多读者可能会想,为什么不像普通函数调用那样,在回调的地方直接写函数的名字呢?这样不也可以吗?为什么非得用回调函数呢?
要回答这个问题,我们先来了解一下回调函数的好处和作用,那就是解耦,对,就是这么简单的答案,就是因为这个特点,普通函数代替不了回调函数。

何为解耦?

耦合偏向于两者或多者的彼此影响,解耦就是要解除这种影响,增强各自的独立存在能力,这样就更巨灵活性了,可以无限降低存在的耦合度,但不能根除,否则就失去了彼此的关联,失去了存在意义。

下面这幅图很好的描述了回调函数之间的关系:

在这里插入图片描述

在回调中,主程序把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,这样有没有觉得很灵活?并且丝毫不需要修改库函数的实现,这就是解耦。再仔细看看,主函数和回调函数是在同一层的,而库函数在另外一层,想一想,如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数了,这也很好的体现了封装性,将一些技术细节隐藏起来,只暴露出一部分内容。

三、回调函数的意义

回调函数可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。简而言之,回调函数就是允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
回调函数在实际中有什么作用?先假设有这样一种情况:我们要编写一个库,它提供了某些排序算法的实现(如冒泡排序、快速排序、shell排序、shake排序等等),为了能让库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,能让库可用于多种数据类型(int、float、string),此时,该怎么办呢?可以使用函数指针,并进行回调,qsort函数就其中最经典的例子,下面我将着重介绍qsort函数。

四、详解qsort函数

为什么我们要提到qsort函数呢?因为它能实现对各种数据类型的排序就是因为使用了回调函数compar,下面我们就来详细解析一下qsort函数。

4.1 qsort函数的功能

我们可以通过下面一张图简单的先了解一下qsort函数的功能:

在这里插入图片描述

它的功能是对数组的元素进行排序对数组中指向的元素进行排序,每个元素的长度为字节,使用函数确定顺序。此函数使用的排序算法通过调用指定的函数来比较元素对,并将指向这些元素的指针作为参数。该函数不返回任何值,而是通过对其元素进行重新排序来修改所指向的数组的内容。

4.2 qsort函数的参数

还是先给大家放一张标准的qsort函数参数图:

在这里插入图片描述

下面是我对qsort函数参数的分析:

在这里插入图片描述

4.3 qsort函数的基本使用

下面的代码实现的是对整型数组的升序:

#include<stdio.h>
#include<stdlib.h>

int compare(const void* e1, const void* e2)
{
	//(int*)先将元素类型强制类型转换为整型的地址,再进行解引用的操作就得到了元素的值
    //通过相减我们得到了返回值,compar函数就是通过返回值来调整元素的顺序的。
    return *(int*)e1 - *(int*)e2;
}
int main()
{
    int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), compare);
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

在这里插入图片描述

既然compar是回调函数,那么我们之前不是说过它的优点是具有很强的独立性吗?要想实现不同的功能我们只需要改变回调函数的实现就能实现库函数不同的功能了。如果我们想实现对浮点型数组、结构体数组排序,改变compar函数真的能实现功能吗?

下面就以对浮点型排序为例,其实我们只需要稍微改变一下compar函数的实现就能实现排序了。

下面代码是对浮点型数组进行降序:

#include<stdio.h>
#include<stdlib.h>

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

int main()
{
    double arr[] = { 5.0, 34.0, 1.0, 3.0, 3.0, 12.0, 38.0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), compare);
    for (int i = 0; i < sz; i++)
    {
        printf("%.1lf ", arr[i]);
    }
    return 0;
}

在这里插入图片描述

从图片中我们可以看到只需要稍微改变一下compar函数的实现就能对浮点型数组进行降序操作。
那么关于上述代码有几个需要注意的地方:
你有没有发现我在实现浮点型排序的时候在前面将两个浮点型相减的结果强转成了int型?(int)(*(double*)e2 - *(double*)e1); 为何要这么做呢?
这是因为compar函数的返回值就是int类型,它的返回值是不会变的,这就是规定。

实现降序我们只需要交换一下e1与e2的位置就可以了,读者可以记住这样一句话e1写在前面减e2实现的就是升序,e2写在前面减e1实现的就是降序。

4.4 compar函数的作用机理

如下是compar函数的作用机理:

在这里插入图片描述

我们来详细分析一下,假设以上述代码对整型的升序为例,compar函数的返回值为:*(int*)e1 - *(int*)e2。如果e1大于e2返回值就为1,这时候e1在e2的前面又比e2大,要得到升序,所以将两者进行交换,这样大的值就往后面走了;如果e1小于e2返回的是-1,不进行交换,位置不变。
同样的,如果我们对数组进行降序的话只需要在返回值的地方把e1和e2交换一下位置即可:*(int*)e2 - *(int*)e1。如果e2大于e1返回值为-1,这时候e2在e1的后面又比e1大,要得到降序,所以将两者进行交换,这样的小的值就往后面走了;如果e2小于e1返回的是1,不进行交换。
升序和降序其实是相反的,我们只要了解其中的交换规则就好了。

4.5 qsort函数总结

qsort可以用于任意类型的排序,它的注意事项有以下几点:

1.compare函数的返回类型一定为int型,那么在对浮点型数组进行排序时我们是要将它们相减的结果强制类型转化为int型的,在自定义的结构体类型中我们也可能定义了浮点型的数据或者数组,所以我们也要将它们相减的结果强制类型转化为int型
2.在进行字符串的比较时我们不是采用的关系运算符来比较,而是用strcmp库函数来专门比较,它的工作原理跟compar函数有点相似:
若字符串1大于字符串2,函数返回值为1;
若字符串1小于字符串2,函数返回值为-1;
若字符串1等于字符串2,函数的返回值为0.
3.最后compar函数的返回形式也是可以写成另外一种形式的,这里以整型数组为例:*(int*)e1 > *(int*)e2 ? 1 : -1;当然的话我更推荐标准写法它更简洁也更方便,写成这种形式的话增大了代码量而且还会影响机器的运行速度。

五、void*的用法及注意事项

前面我们讲到qsort函数为什么能实现对各种数据类型排序?
其根本原因是compar函数的形参类型是void*型,它能接收各种指针类型,由此我们就可以实现对各种数据类型的排序操作。

void*的用途:void*可以被任何类型的指针接收,也可以接收任意类型的指针(常用)。
我们的库、系统接口的设计上尽量设计成通用接口,例如:一个函数被不同的类型来使用…

下面给大家讲一下void*的用法和注意事项:

void*类型的指针可以接收任意类型的地址,也能被任意类型接收(最常见的NULL其实就是void*类型,NULL == (void*)0)
void*类型的指针不能进行解引用的操作,因为指针类型决定了你可以访问几个字节,这里是void*型所以不确定它到底是什么类型。
void*类型的指针不能进行+/-操作,指针类型决定了+/-一个单位走多少个字节,这里同样的指针类型不确定。**

下面来看相应的一组例子:

在这里插入图片描述

六、利用冒泡排序实现对各种类型的排序

我们知道冒泡排序有很多缺陷,先不说时间复杂度,它只适用于整型数组的排序,所以它其实也是非常鸡肋的一种排序,那么在学完qsort快速排序之后,我们能不能进行仿写一下改造一下使其能对所有数据类型进行排序呢?

下面我们就用冒泡排序来仿写一下,其实大体上是差不多的,只不过qsort底层是用快速排序的思想实现的,而且我们可以直接进行调用它,冒泡排序就要我们自己去实现了,当然了冒泡排序作为最经常使用的排序之一相信对大家也是没什么难度。

我们先来梳理一下思路,要实现对任意类型数据排序,我们仿照qsort函数的参数形式:void qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*));我们发现compar函数其实是可以不用变的,它是用来对任意类型元素比较的,是对数组中指向的元素进行排序,每个元素的长度为字节,*base其实就是数组名,num就数组的大小,size是数组元素的大小,所以我们要做的就是封装一个Bubble_sort间接调用compar对元素进行排序。

下面我们来看看具体的代码实现,这里以升序为例:

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

void swap(char* buff1, char* buff2, int width)
{
	//既然我们把base强转为char*型,那么如果我们要进行交换元素,是不是这宽度个字节全部都要进行交换才能达到交换俩个元素的目的呢
    for (int i = 0; i < width; i++)
    {
        char temp = *buff1;
        *buff1 = *buff2;
        *buff2 = temp;
        buff1++;
        buff2++;
    }
}

void Bubble_sort(void* base,int sz,int width,int (*compar)(void* e1, void* e2))
{
	//我们不确定元素是什么类型,那我们就先默认把它看成是char*型,由width我们知道了每个元素的宽度,也就是占多少个字节。
	//我们每次跳过宽度个字节,是不是每次就是跳过一个元素呢,由此我们就可以每次对比两个元素的大小了
    for(int i = 0; i < sz - 1; i++)
    {
        for(int j = 0; j < sz - 1 - i; j++)
        {
            if(compar((char*)base + j * width, (char*)base + (j+1) * width) > 0)
            {
            	//每俩个相邻元素进行比较,根据compar函数的返回值来控制升序降序,要不要交换元素
                swap((char*)base + j * width, (char*)base + (j + 1) * width, width);            
            }       
        }    
    }
}

void test1()
{
    int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    Bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);

    for (int i = 0; i < sz; i++)
        printf("%d ", arr[i]);
}

int main()
{
    test1();
    return 0;
}

我们来看看结果:

在这里插入图片描述

接着我们就来看看对结构体数组进行排序的例子吧,刚好在qsort部分我也没有进行举例说明:

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

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

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

void Bubble_sort(void* base, int sz, int width, int (*cmp_stu_by_age)(const void* e1, const void* e2))
{
    for (int i = 0; i < sz - 1; i++)
    {
        for (int j = 0; j < sz - 1 - i; j++)
        {
            if (cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
            {
                swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
            }
        }
    }
}
void test2()
{
    struct Stu s[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",10} };
    int sz = sizeof(s) / sizeof(s[0]);

    Bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);

    for (int i = 0; i < 3; i++)
    {
        printf("%s %d\n", s[i].name, s[i].age);
    }
}

int main()
{
    test2();
    return 0;
}

实现了对结构体年龄的升序排序:

在这里插入图片描述

如果我们想对名字进行升序操作,只需要改变一下compar部分就可以了,下面我们来看看结果:

int cmp_stu_by_name(const void* e1, const void* e2)
{
	//注意这里我们比较字符串一定要使用strcmp函数,将字符一个个进行一次比较
    return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

在这里插入图片描述

好了本篇文章关于回调函数就讲到这里了,如果有任何疑问或者错处欢迎大家在评论中互相交流🙈🙈

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

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

相关文章

打开新世界大门,测试人可以用Chrome插件干什么?

什么是Chrome插件 这篇文章所说的Chrome插件&#xff0c;也就是我们通常说的Chrome扩展 (Chrome Extension)&#xff0c;是一个用Web技术开发、用来增强浏览器功能的软件&#xff0c;它其实就是一个由 HTML、CSS、JS、图片等资源组成的一个 .crx 后缀的压缩包。 让我们来看看…

Android12屏下指纹解析

版权声明&#xff1a;本文为梦想全栈程序猿原创文章&#xff0c;转载请附上原文出处链接和本声明 前言&#xff1a;google官方更新了Android12的指纹架构&#xff0c;新添加了关于屏下指纹(屏下光学跟屏下超声波)的支持&#xff0c; 刚好近期要研究这个&#xff0c;想把自己的…

移动硬盘安装ubuntu系统二——启动U盘安装

一. 简介 在之前的一篇文章中记录了使用VMware Workstation给移动硬盘中安装Ubuntu系统过程&#xff0c;本篇文章简单记录使用启动盘安装 Ubuntu 20.04.5系统到移动硬盘。 二. 制作Ubuntu镜像的系统启动盘 按照官网 Install Ubuntu desktop上介绍&#xff0c;在 Windows 上可…

【Transformer】——李宏毅机器学习笔记

Transformer 前言 transformer是一个sequence-to-sequence(seq2seq) 的 model input a sequence&#xff0c;output a sequence. The output length is determined by model. 例如 语音辨识&#xff1a; 那么为什么不能把以上三种模型结合起来&#xff0c;进行语音识别呢&…

代码随想录拓展day3 922. 按奇偶排序数组II;24. 两两交换链表中的节点;234.回文链表;143.重排链表

代码随想录拓展day3 922. 按奇偶排序数组II&#xff1b;24. 两两交换链表中的节点&#xff1b;234.回文链表&#xff1b;35.搜索插入位置 数组和链表的题目。链表的操作几天没看又忘了&#xff0c;果然是要及时复习加反复复习。 922. 按奇偶排序数组II 922. 按奇偶排序数组 …

【小程序】宿主环境之通信模型和运行机制

目录 宿主环境 1. 什么是宿主环境 2. 小程序的宿主环境 通行模型 1. 通信的主体 2. 小程序的通信模型 运行机制 5. 小程序启动的过程 6. 页面渲染的过程 宿主环境 1. 什么是宿主环境 宿主环境&#xff08;host environment&#xff09;指的是程序运行所必须的依赖环…

论文笔记Point·E: A System for Generating 3D Point Clouds from Complex Prompts

之前的文本生成3D模型的方法生成一个模型需要多块GPU跑好几个小时&#xff0c;该文章提出的方法生成一个3D模型只需要单GPU1-2分钟。 该文章生成的3D模型的质量并不是当下最好的&#xff0c;但是生成速度很快&#xff0c;因此在现实中很有意义。 从文本生成3D模型的过程分为三…

Redis 对象

在 Redis底层数据结构介绍1 中我们介绍了Redis用到的所有主要数据结构&#xff0c;比如简单动态字符串&#xff08;SDS&#xff09;、双端链表、字典、压缩列表、整数集合等等。Redis并没有直接使用这些数据结构来实现键值对数据库&#xff0c;而是基于这些数据结构创建了一个对…

第7章 数据库设计和ER模型

第7章 数据库设计和ER模型 考试范围 7.1-7.7 考试题型&#xff1a;数据库设计题 考试内容&#xff1a; 掌握基本ER模型的概念与ER图的设计&#xff1b; 掌握将ER模型转换成关系模式的方法。 1、掌握基本ER模型的概念与ER图的设计 概念 E-R 模型是数据库设计中广泛使用的数…

2022-金盾信安杯

web 有来无回 考察xxe盲注 参考博客&#xff1a;https://blog.csdn.net/m0_49623330/article/details/113641498 <!ENTITY % a SYSTEM "http://vps/test.dtd"> %a; ] > 在自己服务器上编写dtd文件 <!ENTITY % dtd "<!ENTITY % hack SYSTEM ht…

零基础小白如何提高学Python的效率?

Python在所有的编成语言对小白来说是最友好的一种语言&#xff0c;简单、清晰、易学&#xff0c;但是有句话说万事开头难&#xff0c;对于很多连计算机基础都没有的伙伴来说&#xff0c;Python学习的效率极其低&#xff0c;这也导致了一部分放弃学习Python。 为了能够解决大家…

Android MAT的使用

下载与配置 MAT下载地址&#xff1a; Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation JAVA 11下载地址&#xff1a; Java Downloads | Oracle 由于最新版的MAT还需要JAVA 11&#xff0c;所以还需要配置JAVA 11的环境。 JAVA 11环境配置&#xff…

ssm药店药品进销存管理系统idea maven mysql

任何系统都要遵循系统设计的基本流程&#xff0c;本系统也不例外&#xff0c;同样需要经过市场调研&#xff0c;需求分析&#xff0c;概要设计&#xff0c;详细设计&#xff0c;编码&#xff0c;测试这些步骤&#xff0c;基于JSP技术、SSM框架、B/S机构、Mysql数据库设计并实现…

【项目实战:核酸检测平台】第五章 众志诚城

本章目标 完成转运人员、接收人员、数据上传人员端 用到技术&#xff1a; EasyExcel、ElementUIPlus。lodop打印 概述 这一章要完成转运人员、接收人员、数据上传人员端的业务模块&#xff0c;从网上的资料我并没有找到相关的界面&#xff0c;没关系自己脑补就好了&#x…

React DAY05

复习&#xff1a; 1.JSX中的数据绑定 内容绑定&#xff1a;<div>{表达式}</div> 属性绑定&#xff1a;<img src{表达式}/> 样式绑定&#xff1a;<div className{表达式} style{样式对象}></div> 事件绑定&#xff1a;<button onClick{函数} …

跨境电商卖家:减少客户流失的 5 个最佳策略

关键词&#xff1a;跨境电商卖家、客户流失 跨境电商卖家获取新客户的成本可能比保留现有客户高出 25%。 这是有道理的&#xff1a;您可以花费数周时间研究如何让新客户进入您的业务&#xff0c;并投入大量时间和精力来制定完美的潜在客户生成策略&#xff0c;但如果无法留住合…

VMware创建Linux虚拟机之(五)Spark完全分布式部署教程

Hello&#xff0c;转眼间已到2022年底&#xff0c;学期末…… 总体来说&#xff0c;今年经历了很多&#xff0c;真正的成长了许多&#xff0c;成熟了许多。 只能说&#xff0c;希望&#xff0c;明天依旧美好&#xff01;&#xff01;&#xff01; &#x1f412;本篇博客使用到…

12 系统数据库和数据库工具

1. 系统数据库 Mysql数据库安装完成后会给我们初始化四个数据库&#xff1a; mysql&#xff1a;存储Mysql服务器正常运行所需要的各种信息&#xff08;市区、主从、用户、权限&#xff09;information_schema&#xff1a;提供了访问数据库元数据的各种表和视图&#xff0c;包…

SpringBoot之Redis整合

目录 在pom.xml中添加启动器 application.yml添加配置 API测试 存取字符串类型 存取哈希类型 等效操作redis 字符串类型 本人idea&#xff1a;2020.1.3 springboot&#xff1a;2.7.6 redis&#xff1a;5.0.14.1可用 在pom.xml中添加启动器 <dependency>…

北漂外卖小哥转行程序员,他说:想让家人过上更好的生活,扎心

前言&#xff1a; 对于程序员转行送外卖的新闻我们见得很多了&#xff0c;但是从一名外卖小哥转行做一名Python程序员的新闻&#xff0c;反倒见的很少&#xff0c;但是每年转行做程序员的人大有人在。 朋友16年本科毕业后就开始自己创业&#xff0c;1年后创业失败了&#xff…