一、函数指针
1.1 函数的地址
在讲解函数指针变量之前,我们先思考一下什么是函数指针变量,我们可以同数组指针变量进行类比:
数组指针—是指针—是存放指向数组的指针,是存放数组地址的指针;
函数指针—是指针—是存放指向函数的指针,是存放函数地址的指针;
数组是有地址的,那么函数是否也有地址呢?
我们来做个测试:
#include <stdio.h> void test() { printf("hehe\n"); } int main() { printf("test: %p\n", test); printf("&test: %p\n", &test); return 0; }
运行结果:
我们发现:确实打印出来了地址,所以函数是有地址的,并且同数组名是数组首元素地址一样,函数名也是函数的地址,我们可以通过 &函数名 的方式来获得函数的地址。
1.2 函数指针变量
如果我们要将函数的地址存放起来,就得创建函数指针变量,而函数指针变量的写法和数组指针也有许多相似之处:
函数的返回值类型(*指针名)(函数的参数类型)
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int add(int x, int y) { return x + y; } int main() { int (*pf)(int x, int y) = &add; //int——表示pf指向函数的返回类型 //pf——函数指针变量名 //int x, int y——pf指向函数的参数类型和个数的交代 int ret = (*pf)(3, 5); printf("%d\n", ret); return 0; }
运行结果为 8
1.3 函数指针的使用
参考如下代码:
int (*pf)(int a, int b) = &Add; int ret1 = (*pf)(3, 5);//相当于Add(3,5) int ret2 = pf(3, 5);//相当于Add(3,5)
- 对pf解引用相当于通过pf找到Add函数名,然后输入参数进行使用。
- 而我们知道&Add==Add,所以我们也能通过直接使用函数指针变量来调用函数。
- 但是函数指针变量不能像其他指针变量进行±运算
1.4 两段有趣的代码
- 代码1:
(*(void (*)())0)();
首先我们从里往外拆分,在这里,我们把0强制类型转换成函数指针类型,这个函数指针参数是无参,返回值类型是void,然后通过解引用去调用函数,我们可以将其简化为pf。void (*)() — 是函数指针,参数是无参,返回类型是void。
(void (*)()) — 函数指针外面加上括号,表示强制类型转换。
(*(pf)0)();//简化后
这下我们比较容易看出这段代码是先将0强制类型转换为函数指针类型,然后对其解引用。解引用之后相当于调用在0地址的函数,因为其参数为空所以只有一个单独的()。
- 代码2:
void (*signal(int , void(*)(int)))(int);
首先signal与()结合说明其是一个函数名,它有两个参数,一个整型,另一个是函数指针类型。
我们将signal(int ,void(*)(int))单独拿出来,这段代码只剩void(*)(int),这就说明该函数的返回类型是一个函数指针,指向一个参数为int,返回为void的函数。
可能有小伙伴觉得这种写法太复杂了,想简化成下面这种形式:
void (*)(int) signal(int , void(*)(int))
很遗憾,上面这种写法是错误的
事实上,在C语言中有一个关键字叫typedef,我们可以用它来将复杂的类型简单化。
1.4.1 typedef关键字
我们可以用typedef关键字来简化signal函数:
1. typedef void(*pfun_t)(int);//将void(*)(int)简化 2. pfun_t signal(int, pfun_t);//化简之后
二、计算器
2.1 函数指针数组
学习了函数指针数组的创建,可能小伙伴们会想,函数指针数组到底有什么用呢?别着急,函数指针的用途可大了,比如说,我们要写一段代码来实现计算器。
我们可以采用一般写法:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> 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****\n"); printf("*****2.sub****\n"); printf("*****3.mul****\n"); printf("*****4.div****\n"); printf("*****0.exit****\n"); printf("**************\n"); } int main() { int x = 0; int y = 0; int ret = 0; int input = 0; do{ menu(); printf("请选择:"); scanf("%d", &input); switch (input) { case 1: printf("请输入两个数:"); scanf("%d %d", &x, &y); ret = add(x, y); printf("%d\n", ret); break; case 2: printf("请输入两个数:"); scanf("%d %d", &x, &y); ret = sub(x, y); printf("%d\n", ret); break; case 3: printf("请输入两个数:"); scanf("%d %d", &x, &y); ret = mul(x, y); printf("%d\n", ret); break; case 4: printf("请输入两个数:"); scanf("%d %d", &x, &y); ret = div(x, y); printf("%d\n", ret); break; case 0: printf("退出计算器\n"); break; default: printf("选择错误,重新选择\n"); break; } } while (input); }
我们发现,确实能够实现计算器的加减乘除功能,但是我们也观察到,随着计算器功能增加,代码也会越来越长。显然,这样的代码显得太冗余了,我也需要对其进行改造。而要进行改造,我们就不得不利用函数指针!
改造后:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> 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******\n"); printf("*****2.sub******\n"); printf("*****3.mul******\n"); printf("*****4.div******\n"); printf("*****0.exit*****\n"); printf("****************\n"); } int main() { //函数指针的数组 int(*parr[])(int, int) = { 0, add, sub, mul, div }; int x = 0; int y = 0; int ret = 0; int input = 0; do{ menu(); printf("请选择:"); scanf("%d", &input); if (input >= 1 && input <= 4) { printf("请输入两个数:"); scanf("%d %d", &x, &y); ret = parr[input](x, y); printf("%d\n", ret); } else if (input == 0) { printf("退出计算器\n"); } else { printf("选择错误,重新选择\n"); } } while (input); }
我们发现,结果依然是正确的,但是这样的代码就没有了上面那样的冗余,我们通过一个下标,在函数指针数组里面找到了一个函数的地址,然后通过这个地址去调用这个函数,直接传参得出结果,这个效率就快得多。
但这种写法也存在一定的局限性,它里面只能存放相同类型的函数,即只能计算整数,不能计算浮点数!
2.2 回调函数
看到上面我们写的第一种计算器的方式:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> 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****\n"); printf("*****2.sub****\n"); printf("*****3.mul****\n"); printf("*****4.div****\n"); printf("*****0.exit****\n"); printf("**************\n"); } int main() { int x = 0; int y = 0; int ret = 0; int input = 0; do{ menu(); printf("请选择:"); scanf("%d", &input); switch (input) { case 1: printf("请输入两个数:"); scanf("%d %d", &x, &y); ret = add(x, y); printf("%d\n", ret); break; case 2: printf("请输入两个数:"); scanf("%d %d", &x, &y); ret = sub(x, y); printf("%d\n", ret); break; case 3: printf("请输入两个数:"); scanf("%d %d", &x, &y); ret = mul(x, y); printf("%d\n", ret); break; case 4: printf("请输入两个数:"); scanf("%d %d", &x, &y); ret = div(x, y); printf("%d\n", ret); break; case 0: printf("退出计算器\n"); break; default: printf("选择错误,重新选择\n"); break; } } while (input); }
我们发现代码中存在许多重复的部分那有没有什么方法来简化一下代码呢?
那就是我们今天要介绍的回调函数。
那什么是回调函数呢?唉,别着急,我们还是先举一个例子,用calc函数来代替上面计算器代码中冗余的部分:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> 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******\n"); printf("*****2.sub******\n"); printf("*****3.mul******\n"); printf("*****4.div******\n"); printf("*****0.exit*****\n"); printf("****************\n"); } void calc(int(*p)(int, int)) { int x = 0; int y = 0; int ret = 0; printf("请输入两个数:"); scanf("%d %d", &x, &y); ret = p(x, y); printf("%d\n", ret); } int main() { int x = 0; int y = 0; int ret = 0; int input = 0; 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: printf("退出计算器\n"); break; default: printf("选择错误,重新选择\n"); break; } } while (input); }
回调函数其实就是通过函数指针调用的函数!
如果我们把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
简单理解就是,我们通过函数指针来调用其所指向的函数,就被称为回调函数。
三、qsort函数
3.1 qsort函数的使用
在C语言库中,有一个qsort的库函数,它可以用来排序任意类型的数据。它的详细介绍可以参考cplusplus网站:qsort,这里我们只需要掌握它的参数类型、返回值即可。
声明:void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void , const void))
- base – 指向要排序的数组的第一个元素的指针。
- nitems – 由 base 指向的数组中元素的个数。
- size – 数组中每个元素的大小,以字节为单位。
- compar – 用来比较两个元素的函数。
作用:对数组元素进行排序(升序)
返回值:void
细心的小伙伴可能会发现,我们这里出现了一个新的指针类型 void*,这是究竟是一种什么类型的指针呢?
void* 也是一种指针类型,这种指针类型我们称之为通用指针类型。void* 类型的指针变量,可以接收任意类型数据的地址。既然void* 可以接收任意类型数据的地址,那么它的“大小”也是未知的,我们无法对p进行加减、解引用等常规操作。
3.1.1 qsort对整型数组的排序
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> int cmp_int(const void* e1, const void* e2)//这个函数能够比较e1和e2指向的两个元素,并给出返回值(回调函数) { return *(int*)e1 - *(int*)e2; } void print_arr(int arr[], int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } } //test1测试qsort函数排序整型数据 void test1() { int arr[] = { 8, 2, 6, 4, 5, 2, 7, 1, 9 }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), cmp_int); print_arr(arr, sz); } int main() { test1(); return 0; }
运行结果如下:
3.1.2 qsort对结构体的排序
(一)按年龄来比较
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> struct stu { char name[20];//名字 int age;//年龄 }; //test2测试qsort函数排序结构体数据 int cmp_stu_by_age(const void* e1, const void* e2) { return ((struct stu *)e1)->age - ((struct stu *)e2)->age; } void print(struct stu* s, int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%s %d\n", s[i].name, s[i].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_by_age); print(s, sz); } int main() { test2(); return 0; }
(二) 按名字来比较(ASCII码)
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <string.h> struct stu { char name[20];//名字 int age;//年龄 }; //test2测试qsort函数排序结构体数据 int cmp_stu_by_name(const void* e1, const void* e2) { return strcmp(((struct stu *)e1)->name, ((struct stu *)e2)->name); } void print(struct stu* s, int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%s %d\n", s[i].name, s[i].age); } } void test2() { struct stu s[] = { { "zhangsan", 10 }, { "lisi", 30 }, { "wangwu", 15 } }; int sz = sizeof(s) / sizeof(s[0]); qsort(s, sz, sizeof(s[0]), cmp_stu_by_name); print(s, sz); } int main() { test2(); return 0; }
3.2 模拟实现qsort函数
讲解完qsort函数的使用,相信小伙伴们对其也有了一定的理解,但仅仅学会用是远远不够的,想要彻底地掌握,我还必须明白它的底层逻辑,这就需要我们去模拟实现qsort函数。
首先我们要理解qsort函数是怎么进行排序的,qsort函数排序和我们之前学过的冒泡排序有类似之处,冒泡排序也是通过比较两个元素大小来确定谁在前、谁在后。但是冒泡排序仅限于比较整型元素,不能对各种类型的变量进行排序,因此,我们可以把qsort函数看作是冒泡排序的一种拓展。
代码示例如下:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <string.h> void Swap(char* buf1, char* buf2, int width) { int i = 0; for (i = 0; i < width; i++) { char tmp = *buf1; *buf1 = *buf2; *buf2 = tmp; buf1++; buf2++; } } bublle_arr(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2)) { int i = 0; for (i = 0; i < sz - 1; i++) { int j = 0; for (j = 0; j < sz - 1 - i; j++) { if (cmp((char*)base + j*width, (char*)base + (j + 1)*width) > 0) { Swap((char*)base + j*width, (char*)base + (j + 1)*width, width); } } } } int cmp_int(const void* e1, const void* e2) { return (*(int*)e1) - (*(int*)e2); } void test1()//排序整型类型数据 { int arr[] = { 7, 5, 3, 6, 9, 8, 1, 2, 0 }; int sz = sizeof(arr) / sizeof(arr[0]); bublle_arr(arr, sz, sizeof(arr[0]), cmp_int); int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } printf("\n"); } struct stu { char name[20]; int age; }; int cmp_name(const void* e1, const void* e2) { return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name); } void test2()//排序结构体数据 { struct stu s[] = { { "zhangsan", 33 }, { "lisi", 45 }, { "wangwu", 25 } }; int sz = sizeof(s) / sizeof(s[0]); bublle_arr(s, sz, sizeof(s[0]), cmp_name); int i = 0; for (i = 0; i < sz; i++) { printf("%s %d", s[i].name, s[i].age); printf("\n"); } } int main() { test1(); test2(); return 0; }