Hi~!这里是奋斗的小羊,很荣幸各位能阅读我的文章,诚请评论指点,关注+收藏,欢迎欢迎~~
💥个人主页:小羊在奋斗
💥所属专栏:C语言
本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为同样是初学者的学友展示一些我的学习过程及心得。文笔、排版拙劣,望见谅。
1、qsort 使用举例
1.1qsort 是什么?
1.2使用 qsort 函数排序整型数据
1.3使用 qsort 函数排序结构数据
2、qsort 函数的模拟实现
本节内容较多,如有需要请收藏阅读。
1、qsort 使用举例
1.1qsort 是什么?
qsort 是C语言中的一个库函数,使用 qsort 库函数需要包含头文件 <stdlib.h>。这个函数是用来对任意数据类型的数据排序的。
我们之前也写过一个排序函数:
//冒泡排序 — 升序
#include <stdio.h>void bubble_sort(int arr[], int m)
{int i = 0;for (i = 0; i < m - 1; i++){int j = 0;int flag = 1;for (j = 0; j < m - 1 - i; j++){if (arr[j] > arr[j + 1]){flag = 0;int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}if (1 == flag){break;}}
}int main()
{int arr[100] = { 0 };int n = 0;do{scanf("%d", &arr[n++]);} while (arr[n - 1] != -1);//以-1为结束标志bubble_sort(arr, --n);int i = 0;for (i = 0; i < n; i++){printf("%d ", arr[i]);}return 0;
}
上面是我们之前写过的一个冒泡排序,它可以实现排序,但它排序的对象只能是整型,所以它的功能太单一。而 qsort 函数可以实现对任意类型的数据进行排序,它不仅可以完成上面我们写的冒泡排序的功能,还能对字符型、字符串型、实型、结构体类型等排序,我们只要需要提供一个比较函数就行。
我们先来了解一下 qsort 函数的用法:
以下图片均截自:C 标准库头文件 - cppreference.com ,更多详细内容请跳转查看。
上图为 qsort 函数的参数个数、参数类型、函数返回值类型及其功能的简单介绍。
上图为 qsort 函数参数的详细介绍。其中最后一个参数是一种函数指针类型,我们需要自定义一个比较函数将地址传给这个函数指针变量,再通过这个指针调用函数,这个函数就是一个回调函数。
上图为 qsort 函数的使用实例。
为什么要有 qsort 函数呢?它能帮我们做什么?在回答这个问题之前,让我们的目光再回到上节我们写的冒泡排序中,先思考另一个问题,为什么我们写的冒泡排序只能排整形数据呢?
原因就在上面的红色方框中。因为如果我们想排字符串的话,字符串不能用 > 号比较大小,应该用字符串比较函数 strcmp ,简单地修改后,我们发现其他的地方基本不需要再做修改。所以我们只需要改变上面红色方框中的代码,就可以实现对字符串排序了。
也就是说,排序的基本框架不变,变的只是数据的比较部分,那我们把基本框架封装成一个函数,而用于数据的比较部分我们再写一个函数,这样的话我们想排序什么类型,只需要写一个相应类型的比较函数,再将这个函数传给事先就分装好的框架函数,不就实现了想排序什么类型的数据就排序什么类型的数据吗?
很明显我们所说的将基本框架分装一个函数,就是我们要学习的 qsort 函数,而需要我们自己写的比较函数,它的地址就作为 qsort 函数的第四个参数。
qsort 函数就像一个半自动的机器,想使用它必须要有人操作,而且这个人必须事先明确自己想要得到什么,需要给 qsort 函数传递一个什么东西它才能给出我们想要的结果。
1.2使用 qsort 函数排序整型数据
通过上面的了解,接下来我们就来模仿 qsort 函数的使用示例写一个排序整型的小程序:
#include <stdio.h>
#include <stdlib.h>void cmp_int(const void* a, const void* b)
{if (*(int*)a > *(int*)b){return 1;}else if (*(int*)a < *(int*)b){return -1;}else{return 0;}
}void print(const void* pa, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", *((int *)pa + i));}
}void text()
{int arr[] = { 5,3,2,7,8,1,0,4,9,6 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_int);print(arr, sz);
}int main()
{text();return 0;
}
上面的代码中有一些细节的地方,比如:
指针变量a和b的类型都是 void * 类型,而我们在之前的文章中说过 void * 类型的指针是不能直接进行解引用的。由于上面的程序是排序整型数据,所以我们需要将 void * 类型的指针变量a和b强转为 int * 类型再解引用。 在 C语言(指针)2 中我们说过这么两段话:
虽然void *类型的指针不能直接进行解引用操作,也不能 +- 整数的操作, 但是当我们不知道别人给我们传的地址是什么类型的时候,我们就可以放心地去用void *来接收,这就是它的作用。
一般void *类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据。在后面的文章中会深入探讨。
相信我们现在已经明白了其中的含义。正是因为 void * 类型的特殊,qsort 函数才得以对任意数据类型的数据进行排序。
上面函数的功能只是打印出排序好的整型数据, 因此并不希望数据被改变,所以我们可以用 const 修饰指针变量,增强数据的健壮性,防止我们不小心通过指针间接地改变了数据。
1.3使用 qsort 函数排序结构数据
为了证明 qsort 函数确实可以对任意类型的数据排序,我们再来用 qsort 函数排序结构型数据:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>struct people
{char name[20];int age;
};void cmp_struct_name(const void* a, const void* b)
{return strcmp((*(struct people*)a).name, (*(struct people*)b).name);
}void print_struct(const struct people* p, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%s ", (*(p + i)).name);}
}void text1()//排序结构体数据类型的数据
{struct people p[] = { { "xiaoshuai", 25}, { "xiaomei", 24 }, { "xiaochou", 23 } };int sz = sizeof(p) / sizeof(p[0]);qsort(p, sz, sizeof(p[0]), cmp_struct_name);print_struct(p, sz);
}int main()
{text1();return 0;
}
上面是结构体成员——名字的排序,其中也有几处细节需要注意:
值得一说的是,strcmp 函数的返回值规则是:当第一个字符串大于第二个字符串时返回一个正数,小于时返回一个负数,相等时则返回0,这与 qsort 函数是一致的,所以这里没有使用 if—else 分支来判断,而是直接return了strcmp函数的返回值。
同样的道理,我们上面写过的排序整型数据的这部分代码也是可以写成下面这样的,这样写更加简单高效:
但是,这样写是有一点风险的,虽然这个风险微乎其微,但还是值得说一下。其实细心的朋友已经知道了,因为我们之前给出的截图中已经有两行大字明确说明了这种写法的风险,并且推荐了更加科学的写法。
虽然第二种写法在现实中发生错误的概率很小很小,但我们不能因为如此就放肆地使用这种写法,最后养成一种坏习惯。作为一名技术人员应当时刻保持严谨、科学、负责任的工作态度,这样才能写出高质量且万无一失的代码。
我们下面还要写一个结构体成员——年龄的排序,但是在写这个代码之前,我们先来补充一个关于结构体成员访问、之前欠下的一点内容。
结构体成员访问有两种方法,一种是我们用过的 " . " 结构体成员访问操作符,还有一种就
是 “->” 结构体成员访问操作符。它们两个的使用方法是:
下面就来用结构体成员访问操作符 “ -> ” 来实现结构体成员——年龄的排序:
#include <stdio.h>
#include <stdlib.h>struct people
{char name[20];int age;
};void cmp_struct_age(const void* a, const void* b)
{int arg1 = ((struct people*)a)->age;int arg2 = ((struct people*)b)->age;return (arg1 > arg2) - (arg1 < arg2);
}void print_struct(const struct people* p, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", (*(p + i)).age);}
}void text2()//排序结构体数据类型的数据
{struct people p[] = { { "xiaoshuai", 25}, { "xiaomei", 24 }, { "xiaochou", 23 } };int sz = sizeof(p) / sizeof(p[0]);qsort(p, sz, sizeof(p[0]), cmp_struct_age);print_struct(p, sz);
}int main()
{//text1();text2();return 0;
}
而上面的方法就不需要对结构体指针变量a和b进行解引用操作了。
说到这里,我来对 qsort 函数做一个简单的总结:
(1)qsort 函数是一个库函数,使用需要包含头文件 <stdlib.h>;
(2)qsort 函数使用的排序算法为快速排序,并且默认排升序;
(3)qsort 函数可以实现对任意数据类型的数据排序,但只是一个 “半成品” ,需要我们自己写一个某种数据类型的比较函数, 再通过函数指针调用;
(4)qsort 函数能实现的关键就是 void * 类型,正是因为它能接收任意类型的指针而实现了泛型编程的效果。
qsort 函数默认排升序,那能不能想办法让它排降序呢?其实实现起来很简单,比如下面的这个代码,我们只需要把a和b的值交换,就可以实现降序了。
2、qsort 函数的模拟实现
我们是否可以将 bubble_sort 函数改造一下,让它就像 qsort 函数一样可以实现对任意数据类型的数据排序呢?其实是可以的。既然我们已经搞清楚了 qsort 函数的实现逻辑,那就可以仿照 qsort 函数来改造我们的冒泡排序,使它也能完成对任意类型的数据排序。
首先我们可以将自己的意识分为两个,一个是小帅,另一个是小美。小帅现在需要写一个 bubble_sort 函数,这个函数可以实现对任意类型的数据排序,默认排升序。虽然小帅不知道小美要用他写的这个函数排什么类型的数据,但他可以定一个规则,告诉小美她想使用这个函数的话需要做些什么。
现在我们将意识转换为小帅。我要创建一个可以对任意类型的数据排序的函数,起个名字叫bubble_sort ,函数不需要返回值。第一个参数需要接收小美传过来的数组首元素的地址,但我不知道这个元素是什么类型,所以只能定义为 void * 类型;有了数组首元素的地址,我还需要知道数组内元素的个数,所以第二个参数要接收数组内元素的个数;我还需要知道一个元素占多少个字节,因为我没法仅通过首元素的地址和元素的个数来找到其他的元素;最后还需要一个参数来接收小美传过来的比较函数的地址,这个比较函数需要小美自己写,并且这个比较函数的规则是当前一个元素大于后一个元素时返回一个正值,小于返回一个负值,等于则返回0。
梳理清楚后我就可以写出下面函数的主体部分,用到的排序算法是冒泡排序:
void bubble_sort(void *pa, size_t sz, size_t width, int (*cmp)(const void *a, const void *b))
{int i = 0;for (i = 0; i < sz - 1; i++){int j = 0;int flag = 1;for (j = 0; j < sz - 1 - i; j++){//判断大小}if (1 == flag){break;}}
}
接下来我需要调用小美写的比较函数并判断其返回值,当返回值大于0时我要交换这两个比较的值的位置。但是由于我并不知道小美要排什么类型的数据,所以我在调用小美的比较函数时怎样传参就是一个问题。
我现在知道首元素的地址,还知道一个元素的大小宽度,那我想要访问其他元素的时候就可以先将这个元素强转为最小类型也就是 char * 类型的指针,再给这个元素加上n个元素的宽度就可以实现访问每个元素的地址了。那我就可以写出下面的代码:
void bubble_sort(void *pa, size_t sz, size_t width, int (*cmp)(const void *a, const void *b))
{int i = 0;for (i = 0; i < sz - 1; i++){int j = 0;int flag = 1;for (j = 0; j < sz - 1 - i; j++){//判断大小if (cmp((char*)pa + j * width, (char*)pa + (j + 1) * width) > 0){//交换两个值的位置}}if (1 == flag){break;}}
}
如果这个回调函数的返回值大于0,我就要交换这两个指针(地址)指向的数据的位置,那我再写一个交换函数来实现交换操作。想要交换就必须对指针进行解引用,解引用的前提又必须对 void * 类型的指针转换类型,但是在不知道类型的情况下很明显又是一个棘手的问题。
虽然我不知道指针的类型,但我知道元素的大小,类似于上面指针访问的思想我可以一个字节一个字节的交换,因为无论是什么类型它的大小都大于或等于一个字节,那对于n个字节大小的元素我就交换n次,很明显这个n就是我们知道的元素的大小宽度。那我就可以写出下面的代码:
void swap(char* buf1, char* buf2, size_t width)
{int i = 0;for (i = 0; i < width; i++){char tmp = *buf1;*buf1 = *buf2;*buf2 = tmp;buf1++;buf2++;}
}void bubble_sort(void *pa, size_t sz, size_t width, int (*cmp)(const void *a, const void *b))
{int i = 0;for (i = 0; i < sz - 1; i++){int j = 0;int flag = 1;for (j = 0; j < sz - 1 - i; j++){//判断大小if (cmp((char*)pa + j * width, (char*)pa + (j + 1) * width) > 0){//交换两个值的位置flag = 0;swap((char*)pa + j * width, (char*)pa + (j + 1) * width, width);}}if (1 == flag){break;}}
}
到这里,这个函数我已经基本写好了,最后再写一个打印函数就完成了。
现在我们将意识转换为小美。我现在知道了小帅写的 bubble_sort 函数的规则,首先我需要准备好我想排序的一组数据到数组中,再调用小帅写的 bubble_sort 函数将数组首元素的地址、数组元素个数、元素大小宽度、自定义的比较函数地址等作为参数,最后调用打印函数即可。那我需要写的代码就是下面这些:
int cmp_int(const void* a, const void* b)
{int arg1 = *(int*)a;int arg2 = *(int*)b;return (arg1 > arg2) - (arg1 < arg2);
}int main()
{int arr[] = { 5,2,7,4,9,1,0,3,8,6 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);print_int(arr, sz);
}
到此,我们就完成了冒泡排序的改造,实现了一个类似于 qsort 函数的 bubble_sort 函数。为了区分函数的编写者和使用者,我们可以将 bubble_sort 函数的实现放在另一个文件中,在别人需要使用这个函数时只需要包含相应的头文件即可。
另外,上面只是以整型类型的数据为例进行排序,最后我们再使用各种类型的数据对我们写的 bubble_sort 函数进行测试。
sort.c
#define _CRT_SECURE_NO_WARNINGS#include "sort.h"void swap(char* buf1, char* buf2, size_t width)
{int i = 0;for (i = 0; i < width; i++){char tmp = *buf1;*buf1 = *buf2;*buf2 = tmp;buf1++;buf2++;}
}void bubble_sort(void *pa, size_t sz, size_t width, int (*cmp)(const void *a, const void *b))
{int i = 0;for (i = 0; i < sz - 1; i++){int j = 0;int flag = 1;for (j = 0; j < sz - 1 - i; j++){//判断大小if (cmp((char*)pa + j * width, (char*)pa + (j + 1) * width) > 0){//交换两个值的位置flag = 0;swap((char*)pa + j * width, (char*)pa + (j + 1) * width, width);}}if (1 == flag){break;}}
}void print_int(int* pa, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(pa + i));}
}
sort.h
#pragma once#include <stdio.h>//排序
void bubble_sort(void* pa, size_t sz, size_t width, int (*cmp)(const void* a, const void* b));
//打印
void print_int(int* pa, int sz);
main.c
#define _CRT_SECURE_NO_WARNINGS#include "sort.h"int cmp_int(const void* a, const void* b)
{int arg1 = *(int*)a;int arg2 = *(int*)b;return (arg1 > arg2) - (arg1 < arg2);
}void text1()
{int arr[] = { 5,2,7,4,9,1,0,3,8,6 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);print_int(arr, sz);
}int cmp_struct_name(const void* a, const void* b)
{return strcmp((*(struct people*)a).name, (*(struct people*)b).name);
}void text2()
{struct people p[] = { { "xiaoshuai", 25, 75.5 }, { "xiaomei", 24, 54.3 }, { "xiaochou", 23, 67.8 } };int sz = sizeof(p) / sizeof(p[0]);bubble_sort(p, sz, sizeof(p[0]), cmp_struct_name);int i = 0;for (i = 0; i < sz; i++){printf("%s ", (*(p + i)).name);}
}int cmp_struct_weight(const void* a, const void* b)
{double arg1 = (*(struct people*)a).weight;double arg2 = (*(struct people*)b).weight;return (arg1 > arg2) - (arg1 < arg2);
}void text3()
{struct people p[] = { { "xiaoshuai", 25, 75.5 }, { "xiaomei", 24, 54.3 }, { "xiaochou", 23, 67.8 } };int sz = sizeof(p) / sizeof(p[0]);bubble_sort(p, sz, sizeof(p[0]), cmp_struct_weight);int i = 0;for (i = 0; i < sz; i++){printf("%lf ", (*(p + i)).weight);}
}int cmp_struct_age(const void* a, const void* b)
{int arg1 = (*(struct people*)a).age;int arg2 = (*(struct people*)b).age;return (arg1 > arg2) - (arg1 < arg2);
}void text4()
{struct people p[] = { { "xiaoshuai", 25, 75.5 }, { "xiaomei", 24, 54.3 }, { "xiaochou", 23, 67.8 } };int sz = sizeof(p) / sizeof(p[0]);bubble_sort(p, sz, sizeof(p[0]), cmp_struct_age);int i = 0;for (i = 0; i < sz; i++){printf("%d ", (*(p + i)).age);}
}int main()
{//text1();/*text2();*//*text3();*//*text4();*/return 0;
}
通过实验,我们改造的 bubble_sort 函数确实可以对任意类型的数据排序,并且排升序和降序都是可行的。
如果觉得我的文章还不错,请点赞、收藏 + 关注支持一下,我会持续更新更好的文章。
点击跳转下一节 —> C语言(指针)9