首页 > 编程知识 正文

C语言 回调函数之qsort函数使用及冒泡法模拟,回调函数使用场景

时间:2023-05-03 08:42:27 阅读:263106 作者:2935

今天我们通过一个qsort函数的模拟实现来让大家感受回调函数的概念。

1:qsort函数的使用

首先先简单的介绍一下qsort函数,这个函数是我们C语言中的一个库函数,它的功能是实现一组数据的快速排序。说到这里大家肯定能想到,这个函数就是实现一个排序功能的函数,而我们也曾写过冒泡排序的函数,那么这个库函数究竟有什么与众不同的地方呢?

接下来我想先请大家回忆起冒泡排序的函数,代码如下:

#include <stdio.h>void print_arr(int arr[], int sz){int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("n");}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 main(){//排序int arr1[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };int sz = sizeof(arr1) / sizeof(arr1[0]);printf("排序前:");print_arr(arr1, sz);bubble_sort(arr1, sz);printf("排序后:");print_arr(arr1, sz);return 0;}

上述代码实现的功能就是对一个整型数组进行升序的排序,代码中的bubble_sort函数就是冒泡排序的函数。而冒泡排序的具体思路我在前面的文章中曾经详细的介绍过,这里就不再多提了。如果大家对冒泡的思想有所遗忘,我建议大家能够复习一下,因为我后面所讲的知识中冒泡排序的思想是很重要的一部分。

运行结果:

其实看完这段代码大家也可能已经意识到了,这个排序的函数只能对一个整型数组进行排序,而假设我们要排序一个字符数组,又或者是一个浮点数数组,更甚者我们想要排序一个结构体,那这个函数是不是就显得有些无力了。但是qsort函数却可以实现任意类型的数据排序。

接下来我们就来分析qsort函数:

这是MSDN中对qsort函数的描述,一个快速排序的函数。

我把函数的声明部分拿出来和大家一起分析一下:

void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );

函数名:qsort
函数的返回类型:void

函数共有四个参数,接下来我将继续根据MSDN里的注释和大家一起来分析。

参数一:base

变量base,目标数组的起始地址.我们要排序的这组数肯定是存放在一个数组中的,所以首先我们要把数组的起始地址传过去(也就是数组名)。注意参数的类型是void*(这里的void*有特殊的作用,后面模拟时会加以介绍)

参数二:num

变量num,数组元素的个数,返回值为整型int

参数三:width

变量width,数组单个元素所占字节数。

参数四:compare

是一个函数指针,指向一个比较函数,该函数有两个参数,参数类型均为为void*指针,分别指向要比较的数组两个元素的地址。

刚才我说了,qsort函数能实现任意类型的元素比较,而这个点就体现在最后这个函数指针所指向的比较函数这里。这个函数的功能就是实现两个元素的比较,然后把这种比较方法传个qsort函数,这样qsort函数就可以根据这种比较方法来实现对任意类型元素的排序。

通俗一点来讲就是,我们要实现对一组数据的排序,但不同类型数据的排序方法又不同,于是我们把这个不同点提炼出来,自己写一个排序的方法,然后把这个方法传给排序函数,排序函数就可以根据你所设定的这种方法来进行排序。

这个比较功能的实现规则就是当元素1大于元素2的时候就返回一个大于0的值,相等就返回0,小于返回小于0的值。

还有一点就是qsort函数调用要引<stdlib.h>这个头文件。

口头描述终究是有些虚无缥缈,接下来我们就用qsort函数来实现对一组整型数据的排序来让大家感受一下:

#include <stdlib.h>#include <stdio.h>void print_arr(int arr[], int sz){int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("n");}int cmp_int(const void* e1, const void* e2){return *(int*)e1 - *(int*)e2;}int main(){int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };int sz = sizeof(arr) / sizeof(arr[0]);printf("排序前:");print_arr(arr, sz);qsort(arr, sz, sizeof(arr[0]), cmp_int);printf("排序后:");print_arr(arr, sz); return 0;}

上述代码就是用qsort函数实现对整型数据排序的过程,前面我们逐个分析了qsort函数的四个形参,接下来我们拿出qsort函数的调用和前面所讲的对比一下。

qsort(arr, sz, sizeof(arr[0]), cmp_int);

可以看到我们分别给qsort函数传给了四个参数,分别是:数组起始地址、数组元素个数、数组元素所占字节数、和指向比较函数的函数指针。和函数的形参正好对应,这些相信大家一定可以很好的理解。

接下来我们再重点来分析一下比较函数,由于我们调用qsort函数的目的是对一组整型数据进行比较,所以我们要将整型元素的比较方法写在比较函数中,然后传给qsort函数。qsort函数会根据你的比较方法来实现对这组数据的排序。而比较规则我在前面也说了,是根据两个元素的大小关系返回不同的参数,整型元素实现起来就比较简单了,直接将两个元素相减的值作为返回值返回回去即可。

运行结果:

接下来我们再来看一组对结构体元素排序的代码:

#include <stdlib.h>#include <stdio.h>void print_arr(int arr[], int sz){int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("n");}struct S{char name[20];int age;};int cmp_stu_by_name(const void* e1, const void* e2){return strcmp(((struct S*)e1)->name, ((struct S*)e2)->name);}int main(){struct S arr[] = { { "zhangsan", 20 }, { "lisi", 80 }, { "wangwu", 5 } };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);return 0;}

我们来对结构体数组中的姓名项来进行排序,排名姓名大家可能有点懵逼,其实这里体现的思想就是比较字符串大小,我们知道每个字符串都有相对应的ASCII码值,把这些字符所有ASCII码值加起来就是字符串的大小。而比较方法是使用strcmp函数来实现的,这个函数比较之后如果前者大返回大于0的数,相等返回0,小于返回小于0的数,和比较函数正好吻合,所以我们直接将strcmp函数的返回值返回过去即可。

简单分析一下我们可以得到,这三个姓名的大小有小到大应该是:lisi、wangwu、zhangsan.

我们可以通过调试来观察其变化:

调用qsort函数前:

调用qsort函数后:

希望通过这两段代码大家可以学会熟练的运用qsort函数来实现各种类型的排序。

2:冒泡法模拟实现qsort函数

学会qsort函数的调用方法之后,博主想和大家再更深层次的探讨一个问题,我们知道,qsort函数是系统的库函数,而我们只是直接拿过来调用它。那么我们可不可以自己来写一个函数来实现qsort函数的功能,对任意数据进行排序呢?当然是可以的,接下来我们就来一起实现以下。

模拟实现的方法有许多,这里我采用一种比较简单的,也就是我在开篇提到的冒泡方式来实现。我将通过对先前冒泡函数的不足之处进行补充,使之可以实现qsort函数的功能。

冒泡排序首先要拿到数组的起始地址,整型冒泡排序时,由于要排序的是一个整型数组,所以我们传过去一个整型元素地址用一个整型指针来接收。很明显这样的写法只能接收整型数组,而我们的qsort函数是可以排序任意类型的数据,也就要求他可以接收任意类型的指针,所以仅仅用整型指针来接收显然是不够的。因此在qsort函数中需要用void*指针来接收数组的起始地址。接下来我就来介绍一下void*指针。

void*指针叫无类型指针,是无具体类型的指针,它可以接收任意类型的指针,但不能进行加减整数的操作,也不能被解引用。

所以这里我们就可以用void*指针来接收任意类型数组的起始地址。这里肯定会有人有疑惑了,虽然你用void*指针可以接收任意类型的指针,但是它不能被解引用,也就是说我们拿不到数组里的元素,这应该怎么办呢?

这里就要用到强制类型转换操作符,我们可以将void*指针强制转换成整型指针或者字符指针,这样就可以进行解引用操作了。

接下来继续来分析冒泡排序,第二个参数我们接收的是数组元素的个数sz,在介绍冒泡排序时我们说过,冒泡排序要进行sz-1趟冒泡排序,而第i趟冒泡排序需要比较sz-1-i次,所以拿到数组元素个数我们就可以将数组每个元素都进行比较,如果前者大于后者,就将他们交换位置。这里所采用的思路和整型冒泡排序的思路是一样的。

但是元素比较的方法显然是不同的。整型冒泡排序中,我们可以直接调用两个数来进行比较大小,但如果元素的类型不同,比较的方法也会发生变化。所以我们需要写一个函数,把元素比较的方法放进去,然后我们可以通过这个函数的函数指针,在需要比较的时候来调用这个函数。没错这个函数就是我们前面所写的compare函数。

我在前面介绍过compare函数的比较规则,就是比较两个元素的大小来返回不同值。所以在对两个数进行比较时,我们可以把这两个数的地址作为参数传给compare函数,然后根据compare函数的返回值来判断这两个数的大小,并且决定是不是要交换这两个数的位置。

而在对两个参数进行比较的时候就必然要对起始地址进行解引用操作,并且对起始地址进行加减来找到数组中所有的元素。我们所模拟实现的qsort函数是可以对任意类型的数据进行转换的,会接收不同类型的元素进入到函数中来比较,所以解引用操作是必须要具有通用性的。

这里我们所采用的解决方法是这样的:前面我们提到过将数组的起始地址放到void*指针中去了,接下来我们将这个地址强制类型转化成char*类型,我们知道char*指针只占一个字节,所以我们在对起始地址加减的时候只需给要往后跳的位数并乘上要比较元素的字节数(这块如果想不通可以看后面的代码来辅助理解),这样我们就可以实现对任意类型元素指针的解引用和加减了。

当然为了实现这步操作,我们所模拟的qsort函数就需要接受到数组单个元素的字节数。

分析到这里我们知道,为了实现这个qsort函数,就必须得到四个参数,分别是起始地址(用空类型指针接收)、数组元素个数(用来使数组中的元素两两比较)、数组单个元素所占字节数(能够把任意类型的元素传给compare函数)、指向compare函数的指针(用来调用compare函数)。和我们前面在MSDN中所查找的完全一致。

接下来就可以写出我们所模拟的qsort函数的代码了:

#include <stdio.h>void print_arr(int arr[], int sz){int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("n");}void Swap(char* buf1, char* buf2, int width){int i = 0;for (i = 0; i < width; i++){char tmp = *(buf1 + i);*(buf1 + i) = *(buf2 + i);*(buf2 + i) = tmp;}}//void* 的指针是无具体类型的指针//可以接收任意数据类型的地址//void* 的指针不能直接+- 整数的操作//void* 的指针不能直接解引用操作void bubble_sort(void*base, int sz, int width, int(*cmp)(const void* e1, const void*e2)){int i = 0;for (i = 0; i < sz - 1; i++){int j = 0;for (j = 0; j <sz-1-i; j++){if (cmp((char*)base+j*width, (char*)base+(j+1)*width)>0){Swap((char*)base + j*width, (char*)base + (j + 1)*width, width);}}}}int cmp_int(const void* e1, const void* e2){return *(int*)e1 - *(int*)e2;}int main(){int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };int sz = sizeof(arr) / sizeof(arr[0]);printf("排序前:");print_arr(arr, sz);bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);printf("排序后:");print_arr(arr, sz); return 0;}

这里我们把所模拟qsort函数称为bubble_sort函数。函数的实现思想前面已经全部分析完了。

最后还有一点需要点出,我们知道当compare函数的返回值大于0是说明前面的元素大于后面的数,这个时候我们要把两个数进行交换。交换的过程我们是在一个交换函数swap中进行的。

swap函数的交换方法和普通交换的方法也是不同的,普通交换是是创建一个中间变量来让两个函数直接交换。而在swap函数内部我们不能直接拿到元素所有字节的内容,swap函数拿到的两个元素的地址是char*类型的,也就是说只有一个字节。我们同时需要将单个元素所占的字节数传给swap函数,然后再swap函数内部用一个for循环,让这两个元素每个字节每个字节进行交换,最终实现对他们的完全交换。

以上就是模拟qsort函数的全部思想。

3:回调函数

其实介绍qsort函数模拟过程的实现还有一个重要的目的就是引出我们C语言中一个非常重要的概念:回调函数。

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

这个地方的回调函数就是compare函数,当排序函数需要比较的时候,我就调用回调函数compare函数来进行比较,这种机制也被称作回调函数机制。

试想一下,如果我们没有回调函数的机制,我们还能不能实现对任意类型数据的排序。答案是不能的,因为没有办法用一种统一的办法来实现对不同类型数据的比较。

我们所写的排序函数的差异就在于不同元素的比较,因此我们就将这个差异提炼出来,每个类型元素的比较就写自己的比较函数,然后在排序函数中特定(需要比较)的时候来统一调用。这就是回调函数的重要性。

本篇文章主要想向大家主要讲述了三个知识,qsort函数的使用方法、用冒泡的方法模拟实现qsort函数、和回调函数的概念,并且中间还穿插讲解很多小的知识点。

最后希望这篇文章能对大家起到帮助作用,谢谢大家。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。