🚀前言
回顾我们前面学习了指针数组、数组指针以及简单介绍了函数指针,传参问题等。下面我们将在这些学习过的内容之上继续延展下去,通过这一篇博客,你可以收获更多的知识与内容,同时夯实自己的基础。本篇内容可能比较多,请耐心仔细阅读!💖
🚀有趣的代码
开始之前,基于前面的基础,我们先来看看两个有趣的代码
//代码1 (*(void (*)())0)(); //代码2 void (*signal(int , void(*)(int)))(int);
这两个代码是什么意思呢?先想一想
代码1:
想看里面的部分void(*p)();p是函数指针,所以对于void(*)()是函数指针类型,0本身是个值,0之前放了个类型,强制类型转换,然后进行解引用。所以说代码1是一次函数调用,调用的0作为地址处的函数.
1.把0强制类型转换为:无参,返回类型是void的函数的地址
2.调用0地址处的这个函数
代码2:
signal是函数名,有两个参数,一个是整型,一个是函数指针类型,此时简单来说只剩下void(*)(int),这又是一个函数指针类型。所以说,
代码2是一次函数声明,声明的signal函数的第一个参数的类型是int,第二参数的类型是函数指针,该函数指针指向的函数参数是int,返回类型是void,signal函数的返回类型也是一个函数指针
其实这看起来是难以理解的,所以我们可以用typedf(起别名)来简化一下,更加容易认识代码:
所以,我们要学会去拆分一下代码,不会导致看不懂别人写的代码是什么意思。
我们前面学到了函数指针,但是却没有举例说到函数指针的用途,函数指针究竟能够去做些什么呢?下面,我们一起来看一看。👇
🚀简单计算器
我们将基于简单计算器这个例子来阐述函数指针的用处在于哪,或者说怎么去用上函数指针呢?我们先来简单模拟实现一下简单计算器(基于整型类型)
实现整型加减乘除的功能
#include <stdio.h> void menu() { printf("***************************************\n"); printf("*********** 1.add 2.sub ***********\n"); printf("*********** 3.mul 4.div ***********\n"); printf("**************** 0.exit****************\n"); printf("***************************************\n"); } 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; } int main() { int input = 0; int x = 0; int y = 0; int ret = 0; do { menu(); printf("请选择:>"); scanf("%d", &input); printf("请选择2个操作数:>"); scanf("%d %d", &x, &y); switch (input) { case 1: ret = Add(x, y); printf("%d\n", ret); break; case 2: ret = Sub(x, y); printf("%d\n", ret); break; case 3: ret = Mul(x, y); printf("%d\n", ret); break; case 4: ret = Div(x, y); printf("%d\n", ret); break; case 0: printf("退出计算器\n"); break; default: printf("选择错误\n"); break; } } while (input); return 0; }
简单测试运行一下:
你会发现,输入0退出居然要输入两个操作数,这是为什么呢?因为在操作之前我们把输入的数放在前面了,非常的奇怪,退出前居然还要输入两个数,这时候我们稍微改进一下:
#include <stdio.h> void menu() { printf("***************************************\n"); printf("*********** 1.add 2.sub ***********\n"); printf("*********** 3.mul 4.div ***********\n"); printf("**************** 0.exit****************\n"); printf("***************************************\n"); } 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; } int main() { int input = 0; int x = 0; int y = 0; int ret = 0; do { menu(); printf("请选择:>"); scanf("%d", &input); switch (input) { case 1: printf("请选择2个操作数:>"); scanf("%d %d", &x, &y); ret = Add(x, y); printf("%d\n", ret); break; case 2: printf("请选择2个操作数:>"); scanf("%d %d", &x, &y); ret = Sub(x, y); printf("%d\n", ret); break; case 3: printf("请选择2个操作数:>"); scanf("%d %d", &x, &y); ret = Mul(x, y); printf("%d\n", ret); break; case 4: printf("请选择2个操作数:>"); 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); return 0; }
稍微测试运行一下:
总算解决刚开始出现的问题了,但是这时候又有一个问题:
这一段代码太过冗余了,有点重复性,重复度太高了,我们想想办法解决?
能不能把相同的代码抽离出来,把相同的代码封装成一个函数,下面我们利用函数指针进行改进一下,避免代码的冗余
#include <stdio.h> void menu() { printf("***************************************\n"); printf("*********** 1.add 2.sub ***********\n"); printf("*********** 3.mul 4.div ***********\n"); printf("**************** 0.exit****************\n"); printf("***************************************\n"); } 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 x = 0; int y = 0; int ret = 0; printf("请选择2个操作数:>"); scanf("%d %d", &x, &y); ret = pf(x, y); printf("%d\n", ret); } int main() { 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); return 0; }
简单测试运行一下:
没有任何问题,解决了代码的冗余问题。这就是函数指针的作用。
通过函数地址传递给函数参数,进入函数内部,去调用函数,这就是回调函数。后面会讲到。
🚀函数指针数组
指向函数指针数组的指针是一个 指针
开始之前,我们先来理解函数指针数组:把函数和指针放在数组中,其实就是函数指针数组,怎么理解呢?看一段代码:
怎么去调用里面的函数呢?
所以函数指针数组有什么用?怎么去用?下面来进行演示
还是刚开始冗余代码版本的计算器:
#include <stdio.h> void menu() { printf("***************************************\n"); printf("*********** 1.add 2.sub ***********\n"); printf("*********** 3.mul 4.div ***********\n"); printf("**************** 0.exit****************\n"); printf("***************************************\n"); } 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; } int main() { int input = 0; int x = 0; int y = 0; int ret = 0; do { menu(); printf("请选择:>"); scanf("%d", &input); switch (input) { case 1: printf("请选择2个操作数:>"); scanf("%d %d", &x, &y); ret = Add(x, y); printf("%d\n", ret); break; case 2: printf("请选择2个操作数:>"); scanf("%d %d", &x, &y); ret = Sub(x, y); printf("%d\n", ret); break; case 3: printf("请选择2个操作数:>"); scanf("%d %d", &x, &y); ret = Mul(x, y); printf("%d\n", ret); break; case 4: printf("请选择2个操作数:>"); 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); return 0; }
我们想一下,如果要添加实现x&y,x^y,x>>y,x<<y的功能,此时就是添加4,5,6,7,8之类的选项,case的选项越来越多以此类推,代码会变得越来越长,这时候,把代码写得整洁一些:把switch语句去掉,创建一个函数指针数组存放函数,通过输入的选择作为下标去调用即可,下面我们来看看代码的修改:
#include <stdio.h> void menu() { printf("***************************************\n"); printf("*********** 1.add 2.sub ***********\n"); printf("*********** 3.mul 4.div ***********\n"); printf("**************** 0.exit****************\n"); printf("***************************************\n"); } 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; } int main() { int input = 0; int x = 0; int y = 0; int ret = 0; int (*pfArr[5])(int, int) = { 0,Add, Sub,Mul,Div}; //添加0进去,使之下标对应起来 do { menu(); printf("请选择:>"); scanf("%d", &input); //直接让input作为下标 if (input == 0) { printf("退出计算器"); } else if (input >= 1 && input <= 4) { printf("请选择2个操作数:>"); scanf("%d %d", &x, &y); ret = pfArr[input](x, y); printf("%d\n", ret); } else { printf("选择错误\n"); } } while (input); return 0; }
这样修改的好处在于,以后想添加新的功能,只需要把函数的地址放在数组里面即可,改变范围即可,稍微调整一下代码即可。通过函数指针数组便于以后修改代码。通过这个简单的例子,演示了函数指针数组的作用。下面我们来简单测试一下:
🚩指向函数指针数组的指针
在这里,顺便提一提指向函数指针数组的指针。前面我们写了函数指针数组,是数组,我们对它&,放到一个指针里面即可。
这里基于上述的函数指针数组来用代码简单表示一下:
当然,你会发现,可以套娃套下去…这里就不展开说明了
🚀回调函数
刚开始,实现计算机的时候有说到:
这就是用了回调函数的机制,什么是回调函数?
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
说到这里,太抽象了,难以理解,这时候,我们请出一个例子qsort函数的使用。
说到qsort函数,我们先来说一说冒泡排序
🚩冒泡排序优化版
void bubble_sort(int arr[], int sz) { int i = 0; for (i = 0; i < sz - 1; i++) { int flag = 1; //用来判断数组是否有序,提高效率 int j = 0; for (j = 0; j < sz - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; flag = 0; } } if (flag == 1) { break; } } } #include <stdio.h> int main() { int arr[] = { 10,9,8,7,6,5,4,3,2,1,0 }; //利用冒泡排序把数组排成升序 int sz = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, sz); int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } return 0; }
虽然优化了一下下,提高了效率,但是不管怎么优化,都只能排序整型数组。那怎么办呢?qsort
🚩qsort函数的使用
使用快速排序的思想实现的一个排序函数
下面,我们来简单理解一下qsort函数的参数的意思:
可以看到,比较函数有void*,所以我们很有必要来理解一下void*指针:
这时候,如果用void*来接收,就不会报警告了:
void*是无具体类型的指针,这种指针可以接收任意类型的地址,void*是无具体类型的指针,所以不能解引用操作,也不能±整数操作。这里的参数是void*的原因是因为不知道传过来的类型的指针是什么,所以定义为void*
下面我们还是通过上面的例子来对qsort函数进行简单应用:(记得引用头文件 #include <stdlib.h>)
通过qsort成功实现排序,那能不能实现降序?
我们只要通过改变e1和e2相减的位置即可实现。使之逻辑相反。这个就是回调函数来实现qsort的功能!
这里只是qsort的基本使用。
上面是利用qsort函数来排序整型的,下面我们利用qsort函数来排序结构体
通过结构体的名字进行排序:
通过结构体的年龄来进行排序:
好了,通过上面,已经对qsort有了一定的认识,并且会逐渐的运用,这时候,想想:怎么把冒泡排序改造一下
🚩冒泡排序通用版
把冒泡排序改造成类似qsort函数的实现
void Swap(char* a, char* b,int width) { int i = 0; for (i = 0; i < width; i++) { char tmp = *a; *a = *b; *b = tmp; a++; b++; } } int cmp_int(const void* e1, const void* e2) { return *(int*)e2 - *(int*)e1; } void bubble_sort(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 flag = 1; //用来判断数组是否有序,提高效率 int j = 0; for (j = 0; j < sz - 1 - i; j++) { //通过利用强转base为(char*)乘以宽度的多少来进行比较 if (cmp((char*)base+j*width,(char*)base+(j+1)*width)>0) { Swap((char*)base + j * width, ((char*)base + (j + 1) * width),width); flag = 0; } } if (flag == 1) { break; } } } void test() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, sz, sizeof(arr[0]), cmp_int); int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } } int main() { test(); }
运行成功!在这里,我们稍微改造了冒泡排序,现在,来通过冒泡排序,排序结构体:
struct Stu { char name[20]; int age; }; int cmp_stu_by_age(const void* e1, const void* e2) { return ((struct Stu*)e1) ->age - ((struct Stu*)e2)->age; } void Swap(char* a, char* b,int width) { int i = 0; for (i = 0; i < width; i++) { char tmp = *a; *a = *b; *b = tmp; a++; b++; } } int cmp_int(const void* e1, const void* e2) { return *(int*)e2 - *(int*)e1; } void bubble_sort(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 flag = 1; //用来判断数组是否有序,提高效率 int j = 0; for (j = 0; j < sz - 1 - i; j++) { //通过利用强转base为(char*)乘以宽度的多少来进行比较 if (cmp((char*)base+j*width,(char*)base+(j+1)*width)>0) { Swap((char*)base + j * width, ((char*)base + (j + 1) * width),width); flag = 0; } } if (flag == 1) { break; } } } void test() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, sz, sizeof(arr[0]), cmp_int); int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } } void test1() { struct Stu s[] = { {"zhangsan",1} ,{"lisi",2},{"wangwu",3} }; int sz = sizeof(s) / sizeof(s[0]); //qsort(s, sz, sizeof(s[0]), cmp_stu_by_name); bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age); for (int i = 0; i < sz; i++) { printf("%d\n", s[i].age); } } int main() { //test(); test1(); }
简单测试运行:
好了,关于其他排序的话这里就先不展开了,就先到这里结束了。