本章内容我们主要来探讨回调函数与qsort函数以及如何利用传统排序模拟实现qsort函数的相关内容。内容丰富,大家听我娓娓道来~
目录
1.回调函数
1.1什么是回调函数
回调函数,顾名思义指的是通过函数指针调用的函数。
所谓回调回调,就是回头再调用,就和你跟你同学约干饭一样,
我:“靓仔,好久不见,下次一起干饭去啊!”
靓仔:“好啊,啥时候你叫我就行。”
在这里,我就和靓仔做了一个去干饭的约定(创建了一个函数),但还没有去践行约定,等我啥时候想起来干饭这件事了,我就叫上靓仔一起吃饭(调用这个函数),而干饭这个约定就叫做回调函数。其实回调函数这个说法比较容易产生歧义,其实我觉得应该叫被回调函数,这样大家可能就一下子理解了。
1.2利用回调函数优化转移表
我们再上一篇目中利用函数指针的方法实现了计算器的功能,我们来回顾一下:
//转移表 - 计算器
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 input = 0;
int x = 0;
int y = 0;
int z = 0;
//函数指针的数组
int(*pfArr[5])(int, int) = { 0, add,sub,mul,div };
// 0 1 2 3 4
do
{
menu();
printf("请选择:");
scanf("%d", &input);//3
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:");
scanf("%d%d", &x, &y);
z = pfArr[input](x, y);
printf("%d\n", z);
}
else if(input = 0)
{
printf("退出计算器\n");
}
else
{
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
那我们能否将加减乘除四个函数作为回调函数来写这段代码呢?请看如下代码:
//回调函数
typedef int(*pf_t)(int, int);
void add(int x, int y)
{
return x + y;
}
void sub(int x, int y)
{
return x - y;
}
void mul(int x, int y)
{
return x * y;
}
void div(int x, int y)
{
return x / y;
}
//void calc(int(*ptr)(int, int))
void calc(pf_t ptr)
{
int x = 0;
int y = 0;
int z = 0;
printf("请输入两个操作数:");
scanf("%d%d", &x, &y);
printf("%d\n", (*ptr)(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 input = 0;
//int(*pfarr[5])(int, int) = { 0,add,sub,mul,div };
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
break;
default:
printf("选择错误,重新选择\n");
}
} while (input);
return 0;
}
为了方便书写,用到了之前说过的typedef重定义,忘了的记得复习。如上述,当我们需要用到某个函数时,就将该函数传回calc函数进行调用,这就是回调函数。
2.qsort函数
2.1什么是qsort函数?
qsort函数是一种排序函数,它的函数原型如下:
//qsort函数
void qsort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*));
参数意义:
• base,指针指向待排序数组的第一个元素。
• num,base指向数组的元素个数。
• size,base指向的数组的元素大小。
• compar,函数指针,指向两个元素的比较函数,需要自己创建和编写。
总的来说,就是我们如果有一个待排序数组,我们要对数组进行排序,就将这个数组的地址、大小、元素个数以及比较函数的地址传入即可。并且再使用之前qsort函数之前,需要添加头文件#include<stdlib.h>。
2.2qsort函数使用举例
2.2.1使用qsort函数排序整型数据
我们知道,C语言中有各种各样的排序,如冒泡排序、快速排序、希尔排序等。我们可以使用冒泡排序来排序整型数据:
//冒泡排序可以用来排序整数数据
void bubSort(int arr[], int length)
{
int flag = 1;
while (length-- && flag)
{
flag = 0;
for (int i = 0; i < length; i++)
{
if (arr[i] > arr[i + 1])//把两个元素的比较的方法封装成函数,将函数地址传给排序函数
{
int temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
flag = 1;
}
}
}
}
void showArr(int arr[], int length)
{
for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubSort(arr, sz);
showArr(arr, sz);
return 0;
}
但是传统排序有个缺点,就是它并不能排序任意类型的数据,但qsort函数却可以,我们可以看到上述函数原型类型都是写的void或void*,也就是说qsort函数可以排序任意类型的数据。
假如我们仍然要对上述代码中的数组arr进行排序,我们首先确定前三个参数:
第一个参数为arr,第二个参数数组的元素个数为10,或者写成sz=sizeof(arr)/sizeof(arr[0]),第三个参数数组的元素大小我们可以写成sizeof(arr[0]),现在还剩第四个函数指针,需要我们模拟实现。
也就是说,我们要写出一个判断两个整型元素谁大谁小,如果前者大就返回1,后者大就返回-1,一样大就返回0。由于比较函数的形参确定,我们可以这么来写整型数据的比较函数;
int cmp_int(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2;
}
我们要注意的是,void*指针是不能直接解引用的,所以我们将它强转为int*类型再解引用,就能得到指向的整数,我们直接返回二者的差值就行了。
那么四个函数都集齐了,就可以完整写出该qsort函数:
qsort(arr, sz, sizeof(arr[0]), cmp_int);
所以完整代码如下:
#include<stdio.h>
#include<stdlib.h>
void showArr(int arr[], int length)
{
for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
}
int cmp_int(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2;
}
void test1()
{
int arr[] = { 5,7,2,4,6,1,8,9,0,6 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
showArr(arr, sz);
}
int main()
{
test1();
return 0;
}
2.2.2使用qsort函数排序字符数据
字符数据和整型数据其实差别不大,因为前三个数据很好确定,我们只需要将比较函数稍加修改即可:
int cmp_char(const void* p1, const void* p2)
{
return *(char*)p1 - *(char*)p2;
}
所以整体代码没什么改变:
#include<stdio.h>
void showArr(char arr[], int length)
{
for (int i = 0; i < length; i++)
{
printf("%c ", arr[i]);
}
}
int cmp_char(const void* p1, const void* p2)
{
return *(char*)p1 - *(char*)p2;
}
void test1()
{
char arr[] = { 'a','b','c','m','v','k','d','t' };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_char);
showArr(arr, sz);
}
int main()
{
test1();
return 0;
}
2.2.3使用qsort函数排序字符串数据
对于字符串的排序,我们首先肯定要将要比较的几个字符串放入数组当中。那此时需要注意的是,我们放入的相当于首元素的地址,也就是说数组中的每个元素都是地址(指针),所以这个数组是指针数组。
其次,对于字符串的比较,我们有一些地方需要注意,我们按照以下两种方法:
2.2.3.1二维数组方式初始化
很容易想到的就是将字符串放到一个数组里面去,所以我们可以用二维数组的方法初始化:
char ch[][10] = { "black","cat","apple","face","dog" };
为什么是二维数组呢,忘了的话可以去复习什么是二维数组,二维数组的元素其实就是一维数组,图解如下:
由于比较函数的两个参数分别是两个相邻元素的地址,所以传过去的地址无需解引用直接比较即可,原因是字符串的地址其实指向了字符串的首字母,而strcmp函数的参数也正好是一级指针,比较首字符,如果相同再继续往下比。整体代码如下:
#include<stdio.h>
#include<stdlib.h>
//比较函数
int cmp_string(const void* elem1, const void* elem2)
{
return strcmp(((char*)elem1), ((char*)elem2));
}
int main()
{
//二维数组初始化
char ch[][10] = { "black","cat","apple","face","dog" };
int sz = sizeof(ch) / sizeof(ch[0]);
//qsort排序
qsort(ch, sz, sizeof(ch[0]), cmp_string);
for (int i = 0; i < sz; i++)
{
printf("%s ", ch[i]);
}
return 0;
}
2.2.3.2指针数组方式初始化
当然我们也可以用指针数组初始化:
char* ch[10] = { "black","cat","apple","face","dog" };
那此时在内存中的存储就和二维数组不太一样了,我们具体来看图解:
指针数组的元素是指针变量,char* ch[10]表示ch是一个数组,数组中有10个元素,每一个数组的元素是char*类型的指针,指向常量字符串 。
而此时传给比较函数的指针是常量字符串的地址的地址,是二级指针,所以在比较函数中我们需要将其强制类型转化成二级指针再对他进行解引用,才能得到常量字符串的地址。整体代码如下:
#include<stdio.h>
#include<stdlib.h>
//比较函数
int cmp_string(const void* elem1, const void* elem2)
{
return strcmp((*(char**)elem1), (*(char**)elem2));
}
int main()
{
//指针数组初始化
char* ch[] = { "black","cat","apple","face","dog" };
int sz = sizeof(ch) / sizeof(ch[0]);
//qsort排序
qsort(ch, sz, sizeof(ch[0]), cmp_string);
for (int i = 0; i < sz; i++)
{
printf("%s ", ch[i]);
}
return 0;
}
2.2.4使用qsort函数排序结构体数据
对于结构体数据来说,排序可能稍微比其他数据复杂,然而总体思想是不变的。首先我们定义一个结构体类型数据,因为我们要进行排序,所以我们把它放在结构体数组arr中,那么前三个参数和之前一样写法就行了。
struct Person
{
char name[20];
int age;
};
struct Person arr[3] = { {"zhangsan",20},{"lisi",35},{"wangwu",18} };
这个结构体描述了一个人的名字的年龄,所以我们如果要排序,首先要明确是对名字进行排序还是对年龄进行排序。假如是对名字排序,那其实就是字符串数据的比较,如果是年龄排序,其实就是整型数据的比较。两者的比较函数分别是:
//按名字
void cmp_struct_by_name(const void* p1, const void* p2)
{
return strcmp(((struct Person*)p1)->name, ((struct Person*)p2)->name);
}
//按年龄
void cmp_struct_by_age(const void* p1, const void* p2)
{
return ((struct Person*)p1)->age - ((struct Person*)p2)->age;
}
注意结构体成员的访问这里使用的是间接访问->的符号。以下是分别对名字和年龄排序的代码:
#include<string.h>
struct Person
{
char name[20];
int age;
};
//这里的两个结构体元素怎么比较大小 - 按名字或按年龄
//按名字
void cmp_struct_by_name(const void* p1, const void* p2)
{
return strcmp(((struct Person*)p1)->name, ((struct Person*)p2)->name);
}
//按年龄
void cmp_struct_by_age(const void* p1, const void* p2)
{
return ((struct Person*)p1)->age - ((struct Person*)p2)->age;
}
void test2()
{
struct Person arr[3] = { {"zhangsan",20},{"lisi",35},{"wangwu",18} };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_struct_by_name);
qsort(arr, sz, sizeof(arr[0]), cmp_struct_by_age);
}
int main()
{
test2();
return 0;
}
2.3模拟实现qsort函数
我们需要对数据进行排序时,都是直接使用qsort函数,我们似乎并不知道函数究竟是如何实现的。那我们不如利用执之前学过的冒泡排序,来模拟实现qsort函数的功能。
2.3.1冒泡排序
冒泡排序在之前篇目中已经详细讲述了,需要复习的可以先回去看看:
2.3.2利用冒泡排序实现qsort函数
我们假设模拟的函数叫做my_qsort(),那么首先参数是固定的,只是将冒泡算法套进去而已,大家先看看能否看懂以下代码:
void my_qsort(void* base, size_t num, size_t size, int (*compar)(const void* p1, const void* p2))
{
int flag = 1;
while (num-- && flag)
{
flag = 0;
for (int i = 0; i < num; i++)
{
if (/*比较*/)//比较两相同任意数据类型的大小
{
//对两个数据的值进行交换
flag = 1;
}
}
}
}
参数表是不变的,大体也是和冒泡排序类似,不同的地方就在于由于数据类型是任意的,我们不能简单按照冒泡排序对整型变量比较以及交换的方法,对不同类型的数据应该用不同的方法,所以我们要将比较和交换写成两个独立函数。对于compar函数来说和qsort一样的,用什么数据比就用什么方法来,是灵活的,而交换函数是我们现在需要思考的。
由于数据类型不确定,因此索性不去纠结数据的类型,干脆将他们统一视为一个字节一个字节的空间来进行比较及交换,因此传入my_qsort()函数的指针统一强制类型转换成char*类型,以便后续我们一个字节一个字节操作。该部分代码如下:
void Swap(char* buf1, char* buf2, size_t size)
{
for (int i = 0; i < size; i++)
{
char temp = *buf1;
*buf1 = *buf2;
*buf2 = temp;
buf1++;
buf2++;
}
}
buf1和buf2分别是两个数据起始的地址,由于被强转换为char*,一个char*就是一个字节,size_t表示这个数据类型有几个字节,也就是大小,然后在for循环中对每个字节单独进行交换即可。
那实参部分呢?
首先,我们需要将原本需要排序的数组base强制转换为char*,即(char*)base,这是起始字节,整个数据是size个字节,(char*)base+1*size是不是就是base指向元素的后面一个元素,或者说该数组的第2个元素,那么(char*)base+i*size是不是就是数组中的第i-1个元素。从而,(char*)base+(i+1)*size不就是(char*)base+i*size的后一个数据了吗?所以说,这种按照字节单独处理的方法,不用管它究竟是什么类型,都可以套用,对于compar比较函数也是一样的,以下是完整代码:
void Swap(char* buf1, char* buf2, size_t size)
{
for (int i = 0; i < size; i++)
{
char temp = *buf1;
*buf1 = *buf2;
*buf2 = temp;
buf1++;
buf2++;
}
}
void my_qsort(void* base, size_t num, size_t size, int (*compar)(const void* p1, const void* p2))
{
int flag = 1;
while (num-- && flag)
{
flag = 0;
for (int i = 0; i < num; i++)
{
if (compar((char*)base+i*size,(char*)base+(i+1)*size) > 0)
{
Swap((char*)base + i * size, (char*)base + (i + 1) * size, size);
flag = 1;
}
}
}
}
注意,compar()需要根据不同数据类型编写不同的函数内容,在2.2使用举例中都有,要使用时直接CV就行了。
以上就是本篇文章的所有内容,也是指针的终章了,感谢大家观看~