指针之旅(5)—— 万能指针与回调函数的搭配:万能排序qsort函数的使用规则及其模拟实现。

目录

1. 回顾:万能指针void* 与 回调函数 的特性

1.1 万能指针void*

1.2 回调函数

2. qsort函数的使用规则

2.1 qsort的头文件和排序方向

2.2 qsort的函数参数表解析

2.3 结构体数组排序举例

3. 冒泡排序模拟万能排序qsort的实现

3.1 冒泡排序的回顾与疑问

3.2 qsort的模拟实现(万能版冒泡排序)


1. 回顾:万能指针void* 与 回调函数 的特性

1.1 万能指针void*

《指针之旅(2)——const修饰词 && 野指针、空指针与泛型指针》中我们就学习过void*型指针了,本篇我们需要用到它的3个性质:

  1. void*指针可以⽤来接收任意类型地址。(这也是它被称作万能指针的原因)
  2. void* 指针有些指针运算是不能直接进行:(1)指针的+-整数(2)指针解引⽤。 
  3. 如果非要用void*指针进行上述运算,必须对void*指针进行强制类型转换。(指针类型决定访问步长,void*指针的步长为0,不能运算)

1.2 回调函数

《指针之旅(4)—— 指针与函数:函数指针、转移表、回调函数》中也学习过回调函数了,这里再简单介绍一下回调函数:

  1. 回调函数回调触发函数(主调函数)中的一个形式参数。
  2. 回调触发函数(主调函数)用一个函数指针来接收回调函数的地址,方便主调函数调用回调函数。(该函数指针的去*类型是跟回调函数的类型是一致的)
  3. 主调函数的函数体中,会使用该函数指针来调用回调函数,具体要实现什么功能由回调函数的内容决定

2. qsort函数的使用规则

2.1 qsort的头文件和排序方向

使用qsort函数要包含头文件<stdlib.h>

qsort默认的排序规则是"从小到大排序(升序排序)",这与回调函数的规则有关。

2.2 qsort的函数参数表解析

qsort的函数原型:

void qsort (void* base,    size_t num,    size_t size,
            int (*compar)(const void*,const void*));

qsort与其他排序函数一样,无返回值。下面介绍一下这4个参数:

1. 参数base:指的是待排序数组的首地址

  • 作用:这个参数告诉qsort函数需要排序的数据从何处开始。

2. 参数num:表示的是数组中元素的个数

  • 作用:这个参数确保qsort函数知道排序操作需要处理多少个数据项。

3. 参数size:是数组中每个元素的大小(类型),以字节为单位。

  • 作用:这个参数至关重要,因为它告诉qsort函数每个数组元素占据多少内存空间。

4. 参数compar:compar是指向比较函数的函数指针

该比较函数使用者实现,有以下默认的限定

  1. 如果 元素1 大于 元素2,则返回大于0的值。
  2. 如果 元素1 等于 元素2,则返回0。
  3. 如果 元素1 小于 元素2,则返回小于0的值。

qsort只规定这3条默认限定,不做其他规定。如果限定1和限定3的内容刚好相反,那么qsort由升序排序 变为 降序排序

举例说明:用qsort函数对一个整型数组排序。

首先我们要自己写出一个整型数据的比较函数,针对上述3项限定,我们可以写出这两个比较函数:

int int_cmp1(const void* p1, const void* p2)
{
	if (*(int*)p1 > *(int*)p2)
		return 1;
	else if (*(int*)p1 < *(int*)p2)
		return -1;
	else
		return 0;
}

int int_cmp2(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2 );
}

疑点解惑:

1. 应该用哪个比较函数?

答:这两个比较函数都是正确的,因为它们都符合qsort的3条默认限定。

2. 为什么比较函数中的参数类型是const void*?

答:首先,qsort是个万能排序函数,void*可以接收任意类型的数据。

其次,去掉void*类型来看,const修饰的对象是*p1和*p2,也就是说*p1和*p2的值不能被改变,这保证了数组的元素不会在比较函数中不会被指针改变。

3. 作比较时,为什么使用的是*(int*)p1,而不是*p1?

答:因为p1是void*型的指针,该类型的指针访问的步长为0,即不能访问。要先进行强制类型转换,才有访问的步长(权限)。

现在我们有了自己的比较函数,可以对整型数组排序了:

2.3 结构体数组排序举例

现在知道怎么使用qsort了,让我们尝试用qsort对一个“学生信息”结构体数组进行排序:

结构体的定义:

struct Stu         //“学生信息”结构体
{
    char name[20];        //名字
    int age;                   //年龄
};

1.按名字升序排序:

//strcmp - 是库函数,是专⻔⽤来⽐较两个字符串的⼤⼩的
int cmp_stu_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
//按照名字来排序
void test1()
{
	struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_name);
}

2.按年龄升序排序:

int cmp_stu_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
//按照年龄来排序
void test2()
{
	struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_age);
}

3. 冒泡排序模拟万能排序qsort的实现

库函数中的万能排序函数是由快速排序算法quick_sort演化而来的,鉴于大多数人没学过快速排序,所以这里我用冒泡排序算法来演示。

3.1 冒泡排序的回顾与疑问

我们先来回顾一下冒泡排序怎么写:

void bubble_sort(int arr[], int sz)
{
	for (int i = 0; i < sz-1; i++)	//i是趟数,要排sz-1趟
	{
		for (int j = 1; j < sz-i; j++) //arr[sz-i]以及它后面的元素已经排好顺序
		{
			if (arr[j - 1] > arr[j])	//整型数据的比较
			{
				int t = arr[j];			//整型数据的交换
				arr[j] = arr[j - 1];
				arr[j - 1] = t;
			}
		}
	}
}

在冒泡排序的框架下,对于其他类型的数组来说:还是要排sz-1趟,排序中的arr[sz-i]以及它后面的元素已经排好顺序。所以 i 和 j 的for循环大体不用改变。

但是原本bubble_sort的比较部分和数据交换部分都是只针对int型的。而万能排序是能对所有类型都能排序的(比如正数、浮点数、字符、字符串),所以这一部分要改。

3.2 qsort的模拟实现(万能版冒泡排序)

我们仿照库函数qsort的参数表写出了冒泡排序版的万能排序函数bubsort:

void void_swap(const void* e1, const void* e2, size_t size)
{
	for (int n = 0; n < size; n++)
	{
		char t = *((char*)e1+n);        重点
		*((char*)e1+n) = *((char*)e2+n);
		*((char*)e2 + n) = t;
	}
}

void bubsort(void* base, size_t num, size_t size, int (*compar)(const void* e1, const void* e2))
{
	for (int i = 0; i < num-1; i++)
	{
		for (int j = 1; j < num-i; j++)
		{
			//如果arr[j-1] > arr[j]就交换
			if (compar((char*)base + (j - 1) * size, (char*)base + j * size) > 0)   重点
				void_swap((char*)base+(j-1)*size, (char*)base+j*size, size);	重点
		}
	}
}

这里有几个重点需要说明一下:

1. 怎样得到数组arr[j-1]与arr[j]的地址来比较大小?

很简单,对base加够整数使得该结果能指向元素arr[j-1]与arr[j]的首地址就行。

需要注意的是:base现在是void*指针,需要强制类型转换;我们有参数size,代表着数组每个元素的大小,单位是一个字节。即base+j*size就是数组arr[j]的地址,所以base要强制类型转换成char*型。

2. 怎样交换arr[j-1]和arr[j]的数据内容?

首先要知道一点:数据是一个字节一个字节来存储的,如果一个字节一个字节地交换数据,交换到该元素的最后一个字节,那么整个元素就完成了交换。

所以在这里我们交换size次,每次只交换char型大小的数据。

现在我们来试试这个万能冒泡排序bubsort能不能使用:

int int_comp(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}
//用bubsort排序整型数组:
int main()
{
	int arr[] = { 1,3,4235,353,54,53,323,4,2,42,7,5 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubsort(arr, sz, sizeof(arr[0]), int_comp); 

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

结果:

我们成功了!!!


本期分享结束,感谢大家的支持Thanks♪(・ω・)ノ

评论 35
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值