hello,各位小伙伴们大家好!本篇文章小风将带着大家了解C语言中的另一大重要知识点回调函数:回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
一、qsort函数全解(<stdlib.h>库函数)
1.qsort函数介绍
当我们谈及回调函数时,qsort函数可以说是回调函数应用中的一个经典案例。
qosrt函数的功能是非常强大的,之所以称之它为万能排序函数,顾名思义它可以对任意数据类型的数据进行排序,它底层实现排序的算法是快速排序,并不受外界条件所限制。而这种强大的功能就是建立在回调函数所实现的。
函数原型:qsort函数包含了四个参数,分别为:接受排序的数组名、数组元素的个数、每个元素所占空间的大小(单位是字节)、以及回调函数
//qsort函数的应用
//函数原型:
//void qsort(void* base, //接收的需要排序的数组,数组名
// size_t num, //数组元素的个数
// size_t size, //数组中每个元素所占的空间的大小(单位:字节)
// int (*compar)(const void*, const void*) //函数指针,指向的函数功能是用来比较大小的方法,返回整型结果
// );
2.qsort函数的具体应用
下面是通过qsort函数实现对各种类型数组进行排序:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//定义一个结构体变量
typedef struct
{
char name[20];
int age;
char xh[10];
}Stu;
//回调函数1:整型数据定义比较方法
int com_int(const void* n1, const void* n2)
{
return *(int*)n1 - *(int*)n2;
}
//回调函数2:字符型数据比较定义
int com_char(const void* n1, const void* n2)
{
return *(char*)n1 - *(char*)n2;
}
//回调函数4:结构体类型中的字符串数据比较定义
int com_struct_str(const void* n1, const void* n2)
{
return strcmp((*(Stu*)n1).name, (*(Stu*)n2).name);
}
//整型输出
void print_arr_int(int arr[], size_t sz)
{
int i = 0;
while (i < sz)
printf("%d ", arr[i++]);
puts("");
}
//字符输出
void print_arr_ch(char arr[], size_t sz)
{
int i = 0;
while (i < sz)
printf("%c ", arr[i++]);
puts("");
}
//结构体数组字符串输出
void print_arr_struct(Stu stu[], size_t sz)
{
int i = 0;
while (i < sz)
printf("%s ", stu[i++].name);
puts("");
}
//整型测试
void test_int()
{
//整形数组
int arr_int[] = { 1,4,2,4,6,8,28,9,10,5 };
//元素个数
size_t num = sizeof(arr_int) / sizeof(arr_int[0]);
printf("排序前:");
print_arr_int(arr_int, num);
qsort(arr_int, num, sizeof(arr_int[0]), com_int);
printf("排序后:");
print_arr_int(arr_int, num);
}
//字符型测试
void test_char()
{
//字符型数组
char arr_ch[] = { 'A','H', 'S','D','L','C','O','G','F','Y' };
//元素个数
size_t num = sizeof(arr_ch) / sizeof(arr_ch[0]);
printf("排序前:");
print_arr_ch(arr_ch, num);
qsort(arr_ch, num, sizeof(arr_ch[0]), com_char);
printf("排序后:");
print_arr_ch(arr_ch, num);
}
//结构体测试
void test_struct()
{
//字符串
Stu stu[3] = { {.age = 17, .name = "zhangsan", .xh = "0001293"},
{.age = 19, .name = "lisi", .xh = "0001283"},
{.age = 18, .name = "wangwu", .xh = "0001222"} };
size_t num = sizeof(stu) / sizeof(stu[0]);
printf("排序前:");
print_arr_struct(stu, num);
qsort(stu, num, sizeof(stu[0]), com_struct_str);
printf("排序后:");
print_arr_struct(stu, num);
}
//主函数
int main()
{
test_int();//整型数组测试
test_char();//字符型数组测试
test_struct();//结构体中的字符串测试
return 0;
}
从上述代码中我们可以现只要定义了所需的回调函数,qsort就能对其指定数据类型进行排序,这其中不仅包括了字符型,整型,同时还包括字符串结构以及结构体类型。感兴趣的小伙伴们可以尝试在自己电脑上的编译其进行调用一下,加深理解。
3.qsort函数分析
(1)回调函数的设计
相信很多小伙伴们在看完上述的应用实例后,心中肯定充满了各种各样的疑惑,就比如说当我们设计回调函数时,为何都采用const void*类型形式参数进行接收?
- 首先我们传递的值是来自所需排序数组中的元素,因此他是不能改变的,为了提高代码的安全性,我们采用const来进行修饰。
- 其次void表示无类型,在C语言中它可以用来接收任意类型的数据,如果我们需要对它接收的数据进行操作时,只需强制类型转换即可。
- 最后为什么要使用指针接收,小风认为因为接收数据类型是不确定的,传递过来的参数所占空间的大小也是不确定的,因为形式参数是是实参的一份临时拷贝,如果采用传值调用并且接收的内容过大时,系统需要另外开辟一块很大的空间进行接收,出于此考虑,所以采用传址调用更加合适,提高了代码的运行效率,同时在一定情况下能节省空间的消耗。
那为什么回调函数返回类型都设置为int类型呢?
这其实与qsort函数的排序规则有关,当我们查看C\C++的官方API文档(地址链接:<cstdio> (stdio.h) - C++ Reference (cplusplus.com))中对qsort函数的定义不难发现其中qsort函数对两个函数的比较规则定义如下图所示:
- 当回调函数的值<0:表示第1个值小于第2个值
- 当回调函数的值=0:表示第1个值等于第2个值
- 当回调函数的值>0:表示第1个值大于第2个值
这也就不难理解为何需要将回调函数的返回类型设置为int类型了
三、使用冒泡排序实现qsort函数
为了便于大家理解qsort函数中的底层原理以及讲解,这里我们将采用冒泡排序算法来实现qsort函数。
以下是实现qsort完整代码
#include <stdio.h>
//交换函数
void swap(size_t sz, char* x, char* y)
{
int i = 0;
for (i = 0; i < sz; i++)
{
char tmp = *(x + i);
*(x + i) = *(y + i);
*(y + i) = tmp;
}
}
//整型比较
int cmp_int(void* x, void* y)
{
return *(int*)x - *(int*)y;
}
//模仿qsort的功能实现一个通用的冒泡排序
void bubble_sort(void* base, size_t nums, size_t sz, int(*cmpar)(const void* x, const void* y))
{
int i = 0;
for (i = 0; i < nums - 1; i++)
{
int j = 0;
for (j = 0; j < nums - i - 1; j++)
{
if (cmpar((char*)(base)+j*sz, (char*)(base)+(j+1)*sz) > 0)
{
swap(sz, (char*)(base)+j * sz, (char*)(base)+(j + 1) * sz);
}
}
}
}
//打印函数
void print_int(int* arr, size_t nums)
{
int i = 0;
for (i = 0; i < nums; i++)
{
printf("%d ", arr[i]);
}
puts("");
}
//主函数
int main()
{
int arr_int[10] = { 10,9,8,7,6,5,4,3,2,1 };
//计算数组长度
size_t numbers = sizeof(arr_int) / sizeof(arr_int[0]);
//排序前
print_int(arr_int, numbers);
//进行排序
bubble_sort(arr_int, numbers, sizeof(arr_int[0]), cmp_int);
//排序后
print_int(arr_int, numbers);
return 0;
}
在该代码中,小风认为最为重要的内容为bubble_sort函数中第二重循环部分:
在这里我们首先需要将传递的无类型指针(void*)数据强转为char*,因为char*所指向的字节数在所有类型指针中是最少的,并且其余类型的指针指向的字节数都是char*的整数倍,因此我们可以通过cha*指针修改任意类型的数据(例如:int*类型的数据指向的字节数为4,char*指针只需移动4次即可修改一个整型数据)
图解如下
总结
本篇文章向大家讲解了何为回调函数,以及qsort回调函数实例的整个实现过程,小风在在本篇博客中分享的是以冒泡排序的方式实现,希望大家能在此理解的基础之上能够进行拓展,使用其他的排序算法实现,当然也很乐意大家能将自己所写的代码分享在评论区。
如果大家对于指针内容仍有不解之处可以查看小风所写的关于指针进阶内容:《C语言指针进阶》:字符指针、数组指针、指针数组、数组传参和指针传参、函数指针、函数指针数组、函数指针数组的指针-CSDN博客