系列文章目录
这里是C语言指针系列所有文章的目录。
指针(二)带你搞定闻之色变的指针——超详细版!(指针与数组及其应用冒泡排序)
指针(三)带你搞定闻之色变的指针——超详细版!(指针、函数、数组及其应用转移表)
文章目录
前言
qsort函数是quick sort的缩写,其底层用的是快速排序,其实排序方式有很多种,比如冒泡排序、插入排序、选择排序、快速排序等,但目的相同,都是排序,就像从北京到上海,坐飞机、高铁等不同的交通方式。今天我们用冒泡排序实现排序函数,跟之前冒泡排序不同的是,这次的函数能够对任何数据类型进行排序!
1.回调函数
qsort库函数用到一个很关键的点是回调函数,我们先看回调函数。
回调函数是通过函数指针进行调用的函数。
当我们把一个函数的指针作为参数传递给另外一个函数,当这个指针被用来调用其所指向的函数,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用,作为对该事件或条件的响应。
在指针(三)带你搞定闻之色变的指针——超详细版!(指针、函数、数组及其应用转移表) 这篇文章中我们提到,实现计算器第一版的代码是冗余的,也就是下图中的红色框部分,其实区别是调用的函数不同。那么我们可以将其封装成一个新函数,只要将加减乘除等函数作为参数传递给新函数从而达到在调用新函数时,实现不同运算效果。
如下所示,Calc的参数是函数指针,每次实现加减乘除等运算时,调用该函数,并将封装好的实现加减乘除的函数指针传过来就好了。Calc函数被调用时,函数内部调用Add, Sub, Mul, Div等函数,Add等函数被称为回调函数。
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("**********************\n");
printf("**** 1:Add 2:Sub ****\n");
printf("**** 3:Mul 4:Div ****\n");
printf("**** 0:exit ****\n");
printf("**********************\n");
}
int main()
{
int i = 0;
int x = 0;
int y = 0;
int (*arr[5])(int, int) = { 0,Add, Sub, Mul, Div };
do {
menu();
printf("请输入:>");
scanf("%d", &i);
if (i >= 1 && i <= 4)
{
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", arr[i](x, y));
}
else if (i < 0 || i>4)
printf("输入错误,请重新输入\n");
else
printf("退出计算机\n");
} while (i);
return 0;
}
void Calc(int(*pf)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("%d\n", ret);
}
//将重复的代码封装成一个新函数
int main()
{
int input = 0;
int x = 0;
int y = 0;
do {
menu();
printf("请输入:>");
scanf("%d", &input);
switch (input)
{
case 1:
Calc(Add);
//Calc函数被调用时,函数内部调用Add函数,Add等函数被称为回调函数
break;
case 2:
Calc(Sub);
//Calc函数被调用时,函数内部调用Sub函数,Sub函数被称为回调函数
break;
case 3:
Calc(Mul);
//Calc函数被调用时,函数内部调用Mul函数,Mul函数被称为回调函数
break;
case 4:
Calc(Div);
//Calc函数被调用时,函数内部调用Div函数,Div函数被称为回调函数
break;
case 0:
printf("退出计算机\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
2. qsort使用举例
在使用qsort之前,我们先研究一下qsort这个函数怎么用。从C++官网https://zh.cppreference.com/w/c/symbol_index可以查询到qsort函数的参数类型,及各参数的说明,qsort默认将数据排成升序。
void qsort(void* ptr, //泛型指针,指向待排序的数组的指针
size_t count, //数组的元素数目
size_t size, //数组每个元素的字节大小
int (*comp)(const void*, const void*)
//函数指针类型,用于接受比较函数的地址。
//两个相邻的元素进行比较,如果第一个大,返回正数;第二个大,返回负数;相等则返回零。
//这个函数由我们自己根据所比较的数据类型实现
);
2.1 升降序说明
在对qsort函数参数进行剖析后,我们尝试使用该函数,那么我们首先要根据我们要比较的数据类型实现一个比较函数,且该函数的返回值符合一下规则,此时函数的返回数据正负与qsort要求的一致,而qsort默认将数据排成升序,因此此时数据会被排成升序,但是如果我们在第一个数大于第二个数的时候返回负数,第一个数小于第二个数的时候返回正数,相等的时候返回值不变,那么排序结果就是降序,因为正常情况下,第一个数大于第二个数,返回正数,二者交换,大的数去后面,如果底层是冒泡排序,一趟排序的结果就是最大的数去了最后,循环结束,升序排列;但是,如果第一个数小于第二个数,返回正数,二者交换,小的数去后面,如果底层是冒泡排序,一趟排序的结果就是最小的数去了最后,循环结束,降序排列;
两个相邻的元素进行比较,如果第一个大,返回正数;第二个大,返回负数;相等则返回零。
2.2 使用举例
2.2.1 整型排序
//将排序得到的整型数组进行打印
void Print_arr(int arr[], int sz)
{
for (int i = 0; i < sz; i++)
printf("%d ", arr[i]);
}
int cmp_int(const void* p1, const void* p2) //参数设置与qsort要求的一致
{
return *(int*)p1 - *(int*)p2;
//此处直接返回p1-p2,当p1>p2,返回值为正...即与所要求的返回值一致
}
void cmp_char(const void* p1, const void* p2)
{
return strcmp(p1, p2);
}
int main()
{
int arr[] = { 1,5,2,6,8,7,3,4,9,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
Print_arr(arr, sz);
return 0;
}
2.2.2 字符排序
#include <string.h>
void cmp_char(const void* p1, const void* p2)
{
return strcmp(p1, p2);
//此处直接返回strcmp库函数的返回值,当p1>p2,strcmp返回值为正...即与所要求的返回值一致
}
int main()
{
char arr1[] = "adcbegwz";
int sz = sizeof(arr1) / sizeof(arr1[0]);
qsort(arr1, sz, sizeof(arr1[0]), cmp_char);
return 0;
}
有趣的一点是,字符串隐藏的’\0’也参与了排序,也是应该想到的,这也不影响后续结构体按照字符串类型的名字进行排序。
2.2.3 结构体排序
说到结构体,我们先看一个操作符,->,对结构体的成员内容进行访问,有两种方式,
1.结构体变量.成员名
2.结构体指针->成员名
如下图所示,
2.2.3.1 按姓名进行结构体比较
#include <string.h> //strcmp的头文件
#include <stdlib.h> //qsort的头文件
struct stu{
char name[20];
int age;
};
void cmp_struct_name(const void* p1, const void* p2)
{
return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name);
//(struct stu*)将p1强制转化为struct stu*类型
//strcmp返回值类型符合要求
}
int main()
{
struct stu arr[3] = { {"xiaoming",20},{"wangwu",40},{"lisi",30} };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_struct_name);
return 0;
}
strcmp的定义如下:
strcmp比较两个字符串时,从前往后,对两个字符串相同位置的字符根据其ASCII码值的大小进行比较,一旦分出大小,后面的字符不再进行比较。
2.2.3.2 按年龄进行结构体比较
#include <stdlib.h>
struct stu{
char name[20];
int age;
};
void cmp_struct_age(const void* p1, const void* p2)
{
return ((struct stu*)p1)->age - ((struct stu*)p2)->age;
}
int main()
{
struct stu arr[3] = { {"xiaoming",20},{"wangwu",40},{"lisi",30} };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_struct_age);
return 0;
}
3.qsort模拟实现
我们模拟实现排序算法,可以对任何数据进行排序,底层用冒泡排序,对结构体根据姓名排升序。
#include <string.h>
struct stu{
char name[20];
int age;
};
bubble_sort(void* base, size_t num,size_t size,int(*cmp)(const void* p1,const void* p2))
{
for (int i = 0; i < num - 1; i++) //趟数
{
for (int j = 0; j < num - 1 - i; j++) //一趟内部的两两比较
{
//比较时,不知道传来的参数是什么类型的,所以将指针强制转换为char*,+j*size
//表明跳过一个size大小类型的数据
if (cmp((char*)base + j * size, (char*)base + (j+1)*size) > 0)
//此处如果>改成<,可以实现降序
//此处用到泛型指针,也就是我们之前提到的泛型编程,然而泛型指针不能直接进行操作
//我们要先进行类型强制转换,可以转换为最小单元的字符指针,便于操作
swap((char*)base + j * size, (char*)base + (j + 1) * size,size);
}
}
}
//传入用于比较的函数
void cmp_struct_name(const void* p1, const void* p2)
{
return ((struct stu*)p1)->name - ((struct stu*)p2)->name;
}
//排序算法实现对任意两个元素进行交换的函数
void swap(char* bf1, char* bf2, int size)
{
char tmp = 0;
for (int i = 0; i < size; i++)
//将size字节的数据,一个字节一个字节的交换
{
tmp = *bf1;
*bf1 = *bf2;
*bf2 = tmp;
bf1++;
bf2++;
}
}
int main()
{
struct stu arr[3] = { {"xiaoming",20},{"wangwu",40},{"lisi",30} };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_struct_name);
return 0;
}
我们在调试窗口可以看到,数组的元素根据姓名进行升序排列。
We did it!
成功啦!
总结
老师说,今天这一节是目前难度系数最高的,但是我感觉理解起来还可以,而且复现代码的时候第一次想不起来回头看是qsort函数里进行比较的代码,可能很大程度感谢博客吧,其实学习的时候有很多不懂的,写博客、敲代码,一个知识点一个知识点的过,到今天,虽然复杂,可也是由一个一个小的知识点叠加,就好像学高数,后期题比较难,是因为综合的知识点多了,一时之间反应不过来,那么我们在前期学习简单知识点的时候一定要理解后多练、勤复习、多总结。其实学习啊,是互通的,一通百通,方法是一致的。
所有的努力都不会被辜负,待六月花开,我们满载而归。
好啦,今天就到这里啦,下期见~