【C语言】深入理解指针(四):回调函数与qsort函数的奥秘
前言
在C语言的学习中,指针一直是一个让人又爱又恨的话题。它强大而灵活,但也容易让人陷入困惑。今天,我们就来深入探讨指针的一个重要应用——回调函数,以及基于回调函数的经典函数qsort。
一、回调函数:隐藏在背后的英雄
先来看一个实际问题。在编写代码时,我们常常会遇到一些重复的逻辑,比如在实现一个简单的计算器程序时,输入输出的操作是重复的,只有具体的计算逻辑不同。传统的解决方法是写多个函数,每个函数都包含输入输出和计算逻辑,但这样会导致代码冗余。而回调函数的出现,就是为了解决这个问题。
1.1 回调函数是什么
回调函数本质上就是一个通过函数指针调用的函数。当你把一个函数的指针(地址)作为参数传递给另一个函数时,这个指针被用来调用其所指向的函数,那么被调用的函数就被称为回调函数。它的调用不是由函数的实现方直接调用,而是在特定的事件或条件发生时由另一方调用,用于对该事件或条件进行响应。
1.2 使用回调函数改造计算器程序
一个使用回调函数改造计算器程序的例子。改造前的代码中,每个计算操作(加、减、乘、除)都需要单独写输入输出和计算逻辑,代码冗长且重复。而使用回调函数后,我们把计算逻辑的函数地址作为参数传递给一个通用的计算函数calc,这样就避免了重复代码的编写。
void calc(int(*pf)(int, int))//函数calc调用函数指针来访问对应的函数
{
int ret = 0;
int x, y;
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
在main函数中,我们只需要根据用户的选择调用calc函数,并将对应的计算函数(如add、sub、mul、div)的地址传递给它即可。
switch (input)
{
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
这种使用回调函数的方式,不仅让代码更加简洁,而且提高了代码的可维护性和可扩展性。当我们需要添加新的计算操作时,只需要定义一个新的计算函数,并在main函数中添加对应的分支即可,而不需要修改calc函数。
完整代码:
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 calc(int(*pf)(int, int))
{
int ret = 0;
int x = 0;
int y = 0;
printf("请输入操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int x = 0;
int y = 0;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0,add,sub,mul,div };
do
{
printf("*********************\n");
printf("1、 add 2、sub \n");
printf("3、 mul 4、div \n");
printf("0、 eixt \n");
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:
printf("退出程序\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input);
return 0;
}
二、qsort函数:排序界的瑞士军刀
qsort函数是C语言标准库中的一个非常强大的排序函数,它使用了快速排序算法,可以对任意类型的数据进行排序。它的强大之处在于,它允许用户自定义比较函数,通过回调函数的方式,来决定数据的排序规则。
2.1 使用qsort函数排序整型数据
首先,我们需要定义一个比较函数int_cmp,这个函数接收两个const void类型的参数,分别指向要比较的两个元素。在比较函数中,我们**需要将这两个参数强制转换为int类型,然后比较它们所指向的整数值**。
int int_cmp(const void * p1, const void * p2)
{
return (*(int *)p1 - *(int *)p2);
}
然后,在main函数中,我们调用qsort函数,并将数组、数组的大小、每个元素的大小以及比较函数作为参数传递给它。
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
运行程序后,我们可以看到数组被成功排序。
完整代码:
int int_cmp(const void* p1, const void* p2)
{
return (*(int*)p1 - *(int*)p2);//转换为int* 类型再进行解引用操作
}
int main()
{
int arr[] = { 1,3,5,7,9,2,4,6,8,10 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);//快速排序法
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
2.2 使用qsort函数排序结构体数组
qsort函数不仅可以对基本数据类型进行排序,还可以对结构体数组进行排序。我们定义一个Stu结构体,包含学生的姓名和年龄。然后,我们定义了两个比较函数cmp_stu_by_age和cmp_stu_by_name,分别按照年龄和姓名对学生进行排序。
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
在test2和test3函数中,我们分别调用qsort函数,并将结构体数组、数组的大小、每个元素的大小以及对应的比较函数作为参数传递给它。
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_by_age);
}
void test3()
{
struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
通过这种方式,我们可以非常灵活地对结构体数组进行排序,而不需要自己编写复杂的排序算法。
三、模拟实现qsort函数:深入理解回调函数与void*
在课件的最后,我们看到了一个模拟实现qsort函数的例子。这个例子不仅让我们更加深入地理解了qsort函数的工作原理,还让我们学习了如何使用回调函数和void*指针。
3.1 void*指针的作用
在C语言中,void指针是一种特殊的指针,它可以指向任意类型的数据。当我们不确定要操作的数据类型时,可以使用void指针。在模拟实现qsort函数时,我们使用void*指针来指向数组的首地址,这样就可以对任意类型的数据进行操作。
3.2 模拟实现qsort函数
在模拟实现qsort函数时,我们使用了冒泡排序算法。首先,我们定义了一个比较函数int_cmp,这个函数与前面的例子中的比较函数相同。然后,我们定义了一个_swap函数,用于交换两个元素的值。在_swap函数中,我们使用了void指针来指向要交换的两个元素,并通过强制类型转换将它们转换为char指针,然后逐字节地交换它们的值。
void _swap(void *p1, void * p2, int size)
{
int i = 0;
for (i = 0; i < size; i++)
{
char tmp = *((char *)p1 + i);
*((char *)p1 + i) = *((char *)p2 + i);
*((char *)p2 + i) = tmp;
}
}
接下来,我们定义了bubble函数,这个函数实现了冒泡排序算法。在bubble函数中,我们使用了void指针来指向数组的首地址,并通过强制类型转换将它转换为char指针,然后逐个比较数组中的元素,并根据比较函数的结果交换它们的值。
void bubble(void *base, int count, int size, int(*cmp)(void *, void *))
{
int i = 0;
int j = 0;
for (i = 0; i < count - 1; i++)
{
for (j = 0; j < count - i - 1; j++)
{
if (cmp((char *)base + j * size, (char *)base + (j + 1) * size) > 0)
{
_swap((char *)base + j * size, (char *)base + (j + 1) * size, size);
}
}
}
}
最后,在main函数中,我们调用bubble函数,并将整型数组、数组的大小、每个元素的大小以及比较函数作为参数传递给它。
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
通过模拟实现qsort函数,我们不仅加深了对回调函数和void*指针的理解,还学习了如何使用这些知识来解决实际问题。
四、总结
今天,我们深入探讨了指针的一个重要应用——回调函数,以及基于回调函数的经典函数qsort。我们看到了回调函数如何帮助我们简化代码,提高代码的可维护性和可扩展性。我们也看到了qsort函数的强大功能,它可以对任意类型的数据进行排序,并且允许用户自定义排序规则。最后,我们通过模拟实现qsort函数,深入理解了回调函数和void*指针的作用。
希望这篇文章能帮助你更好地理解指针和回调函数,让你在C语言的学习之路上更进一步。如果你对这篇文章感兴趣,欢迎点赞、收藏和评论,你的支持是我继续创作的动力。