指针的深入理解(五)
文章目录
- 指针的深入理解(五)
- 前言
- 一.函数指针数组
- 1.1函数指针的理解
- 1.2函数指针的类型
- 二.转移表
- 2.1转移表的概念
- 2.2计算器
- 2.3函数指针数组的应用
- 三.回调函数
- 3.1回调函数的概念
- 3.2回调函数的应用
- 四.指针知识梳理
- 五.qsort的模拟实现
- 5.1冒泡排序
- 5.2qsort函数介绍
- 5.3冒泡排序实现qsort
- 5.4整形比较函数
- 5.5 结构体字符串函数
- 5.6结构体整型比较函数
- 5.7计算指针位置
- 5.8交换函数
- 5.9qsort源码
- 后言
前言
哈喽,各位小伙伴大家好!经过前面对指针的学习,我们已经将指针的内容基本学完了。今天小编就带大家学以致用,用所学的指针知识完成转移表和qsort函数的模拟实现。话不多说,咱们进入正题!向大厂冲锋!
一.函数指针数组
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;
}
int (*pf1)(int,int) = Add;
int (*pf2)(int, int) = Sub;
int (*pf3)(int, int) = Mul;
int (*pf4)(int, int) = Div;
以这段代码为例。我们把四个加减乘除的函数分别用四个指针指向。那初始化的话我们只需把四个指针存在一个数组里面,这个数组就是函数指针数组。
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 (*pf1)(int,int) = Add;
int (*pf2)(int, int) = Sub;
int (*pf3)(int, int) = Mul;
int (*pf4)(int, int) = Div;
ptr[4] = {pf1,pf2,pf3,pf4};//未写出函数指针类型。
这里我们就完成了函数指针数组的初始化,把函数指针存放到数组里。但是我们还没写出函数指针数组的类型。那函数指针数组类型该怎么写呢?
int(*ptr)(int,int) = {pf1,pf2,pf3,pf4};函数指针类型
我们先把函数指针数组写成函数指针类型先,我们在对他进行改造。现在ptr是个指针,我们不希望他是个指针,而是个数组。那我们该怎么做呢?
int (*ptr[4])(int, int) = { pf1,pf2,pf3,pf4};//函数指针数组类型
我们在ptr后面写个方括号,ptr先和方块结合说明他是个数组,把ptr【】去掉后,剩下的就是数组元素类型。类型未函数指针类型,说明这个数组的每个元素是函数指针,所以这个数组就是函数指针数组。
二.转移表
2.1转移表的概念
现在我们懂了什么是函数指针数组,真所谓学以致用。现在我们用我们刚学到的热乎知识来写一个转移表。
什么是转移表?
简单来说转移表就是使用一个函数指针数组来访问函数,这个函数指针数组就像一个踏板一样,可以帮你跳转到其他函数,这就是转移表。
2.2计算器
现在假如我们想写一个计算器,这个计算器能完成两个数的加减乘除运算。我们的代码简单粗暴的话一般会这么写。
#define _CRT_SECURE_NO_WARNINGS 1
#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 x, y;
int input;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个操作数~\n");
scanf("%d%d", &x, &y);
printf("%d\n", Add(x, y));
break;
case 2:
printf("请输入两个操作数~\n");
scanf("%d%d", &x, &y);
printf("%d\n", Sub(x, y));
break;
case 3:
printf("请输入两个操作数~\n");
scanf("%d%d", &x, &y);
printf("%d\n", Mul(x, y));
break;
case 4:
printf("请输入两个操作数~\n");
scanf("%d%d", &x, &y);
printf("%d\n", Div(x, y));
break;
case 0:
printf("退出计算器~");
break;
default:
printf("输入错误,请重新输入~");
break;
}
} while (input);
return 0;
}
我们的思路就是先把菜单函数写出来,在把实现运算的加减乘除代码封装成一个函数。再用do_while循环实现循环计算,switch根据输入选择调用哪个函数,分情况调整即可。
但是大家是不是发现了一个问题,就是这样写出来的代码非常的长。那为什么会这么长呢?
原因在于这段代码有着许多重复性的代码,他们之间唯一不同的就是调用的函数。那我们有没有什么办法既能完成运算又能是代码简洁精炼呢?这就需要用到我们刚学到的函数指针数组的知识啦!
2.3函数指针数组的应用
现在我们想把这个代码简化。这段代码冗余部分区别就在于调用的函数不同,那我们前面说了转移表可以帮我们跳转到不同的函数。所以我们可以用转移表帮我们跳转到不同的函数,之后调用即可,这样一来就不用把每种函数调用的情况列举,直接把冗余代码合并成一段代码即可。
int (*ptr[5])(int, int) = { NULL,Add,Sub,Mul,Div };//为了与函数的菜单
// 序号对齐,多加入Null
int main()
{
int n = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &n);
if (n < 5 && n>0)
{
int x, y;
printf("请输入两个操作数~");//输出两个操作数
scanf("%d%d", &x, &y);
int res = ptr[n](x, y);//调用转移表调用函数
printf("%d\n", res);
}
else if (n == 0)
{
printf("退出计算器~");//退出计算器
break;
}
else
printf("请重新输入~\n");//输入错误
} while (n);
return 0;
}
这里我们思路和之前差不多,但是我们创建了一个函数指针数组,为了函数指针数组的下标与菜单函数的数字对其,我们在数组前加入NULL。接着把冗余部分用转移表整合成一段代码即可。之后对输入数用 if分情况修改即可。这就是转移表。
大家可以看到我们学的这些指针的知识还是很有用的,只是我们还不能熟练运用,所以我们更加需要好好学习!
三.回调函数
3.1回调函数的概念
简单来说,回调函数就是一个通过函数指针调用的函数.
如果你把函数指针作为参数传给另一个函数。当这个函数被用来调用其指向的函数时,被调用的函数就是回调函数。如果没听懂也没关系,接下来我们写一段代码来感受一下什么是回调函数。
3.2回调函数的应用
其实上面那个代码还有一种改造方式。
大家看重复部分都非常相似,那我们是不是可以把这些重复部分封装成一个函数呢?但是他们调用的函数又不同。那怎么办?那我们是不是把先要调用的函数地址作为参数传给新函数,这样新函数再通过传过来的地址调用即可,这样这个函数就能实现调用不同的函数,完成不同的运算。那代码我们就应该这样写。
void Calc(int (*pf)(int , int ))//回调函数
{
int x, y;
printf("请输入两个操作数~\n");
scanf("%d%d", &x, &y);
printf("%d\n", pf(x, y));//调用传参函数
}
int main()
{
int n = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &n);
switch (n)
{
case 1:
Calc(Add);
break;
case 2:
Calc(Sub);
break;
case 3:
Calc(Mul);
break;
case 4:
Calc(Div);
break;
case 0:
printf("退出计算器~");
break;
default:
printf("输入错误,请重新输入~");
break;
}
} while (n);
return 0;
}
回调函数的思路大概是这样。
siwtch根据输入操作数,将不同的运算函数作为参数传给回调函数,回调函数再通过传来的指针调用运算函数,之后break跳出继续下一次选择。
四.指针知识梳理
现在我们已经把指针的内容全部学完了。那大家还记得我们学了那些指针和指针数组吗?正所谓,好记性也怕烂笔头,我们对学完的指针要及时梳理才能加深理解和记忆。比如小编现在写的博客就是经过复习的加深理解后整理知识,把学到的内容讲解给大家听。这个过程是对知识点的加深理解。那我们现在就来回顾一下我们学了那些内容吧。
我们大概学了这么多的内容,这是我写的一个简单的思维导图。大家可以看看,之后也可以写出自己的思维导图。这里给大家区分一下当名字里有指针和数组时怎么区分是数组还是指针。我们只需看最后的名词是啥,是指针就是指针,前面的内容就说明这个指针指向什么。是数组就是数组,前面的内柔在说明这个数组的每个元素是一个什么类型的指针。
五.qsort的模拟实现
5.1冒泡排序
冒泡排序
冒泡排序相信很多小伙伴都已经耳熟能详了。这里简单讲一下。冒泡排序的思想就是,进行n躺比较,每趟比较进行相邻两数的比较,满足条件就交换位置。每次比较后比较数移动。每趟比较将待排序数中最大或最小的数排序好。具体代码如下:
for (int i = 0; i < n - 1; i++)//趟数
{
for (int j = 0; j < n - 1 - i; j++)//待排序数
{
if (a[j] > a[j + 1])//比较
{
int tmp = a[j];//交换
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
}
5.2qsort函数介绍
qsort函数是用来排序的库函数,直接可以用来排序数据,并且最厉害的地方可以排序任意类型的数据。底层的采用的是快速排序的方式。函数有四个参数
- void* base 指针,指向待排序数组的第一个元素
- size_t num 正整数,代表待排序数组元素个数
- size_t size 正整数,代表待排序数组元素的大小,单位是字节
- int (compar)(const void,const void*) 比较函数指针,由这个函数完成数据的比较
5.3冒泡排序实现qsort
现在我们想用冒泡排序算法实现qsort的功能。如果按照原来的冒泡排序写法,只能比较整型,可是qsort函数需要完成任意类型数组的比较。那我们就需要对原来的代码进行改造,那怎么改造呢?我们来思考一下。
现在我们知道比较和交换的地方需要改造,我们把它封装成函数之后再调用这些函数来完成比较和交换的功能。
排序的数据可能时整型数组,还可能是结构体。所以我们就写出多个对应比较函数。
5.4整形比较函数
我们这里统一以函数的返回值作为判断大小的标准,qsort函数对比较函数的返回直接也是这么要求的。
如果返回值为大于0说明前一个数大于后一个数,等于说明两个数相等,小于0说明前一个数小于后一个数。那我们可以直接让两个数作差,大于的话作差之后返回的是大于0的数,等于作差返回0,小于的话作差返回的数小于0的数。那函数就可以这样写。
int cmp_int(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2;//p1p2分别指向
// 两个比较数
}
因为我们这里比较整型,所以我们直接强制类型转化为int*的指针即可。
5.5 结构体字符串函数
如果我们比较结构体字符串的话,比如比较名字,名字就是字符串。字符串的比较可以用库函数strcmp比较。
strcmp的返回值也是根据字符串的大小来决定返回值是大于,等于,还是小于0。那这个函数是怎么比较字符串的大小?
知道这些我们直接调用strcmp函数比较字符串即可,又因为它是根据字符串大小返回值。所以我们直接return strcmp的返回值即可。那比较结构体字符串的话,我们就把指针强制类型转化结构体指针,再用间接访问操作符访问结构体成员即可。
int cmp_stu_by_name(const void* p1, const void* p2)//结构体字符串比较函数
{
return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name);
}
5.6结构体整型比较函数
如果我们要比较结构体年龄的话,年龄用整型表示,那就是比较整形。那思路和我们的整型比较函数一样。但是结构体的话,就把指针强制类型转化为结构体类型,再用间接访问操作符访问结构体成员即可。
int cmp_stu_by_age(const void* p1, const void* p2)//结构体整形比较函数
{
return ((struct stu*)p1)->age - ((struct stu*)p2)->age;
}
5.7计算指针位置
我们要比较两个元素,需要把指针位置传给比较函数。所以我们需要根据base确定出指针位置。那该怎么算呢?
这里给大家分析后知道,指针应该写成base+(j)*width的形式。
5.8交换函数
比较后,如果满足条件就需要交换元素位置。那我们就把元素指针位置传给函数。
那我们已经把base转为char类型的指针,我们就需要把元素的每个字节交换即可。
以这个整型为例,我们只需把前四个字节和后四个字节交换,就能完成9和8的交换。
所以我们还需要把width传给函数,再用 for循环交换每个元素字节即可。
void swap(char* p1, char* p2, int with)//交换函数
{
for (int i = 0; i < with; i++)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
5.9qsort源码
之后我们就把这些函数放在一起,再修改下参数就可完成qsort函数的模拟啦。
int cmp_int(const void* p1, const void* p2)//整形比较函数
{
return *(int*)p1 - *(int*)p2;//p1p2分别指向
// 两个比较数
}
int cmp_stu_by_name(const void* p1, const void* p2)//结构体字符串比较函数
{
return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name);
}
int cmp_stu_by_age(const void* p1, const void* p2)//结构体整形比较函数
{
return ((struct stu*)p1)->age - ((struct stu*)p2)->age;
}
void swap(char* p1, char* p2, int with)//交换函数
{
for (int i = 0; i < with; i++)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
void bubble_sort(void* base, int n, int width, int(*p1)(const void*, const void*))
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n - 1 - i; j++)
{
if (p1((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
后言
到这里咱们就把指针的内容全部学完啦!虽然过程艰辛,但我始终相信能让你变优秀的事情没有一件是轻松的!大家回去好好消化理解下指针的内容,今天就分享到这里,咱们下期见!拜拜~
共勉:请不要相信胜利就像山上的蒲公英一样唾手可得。但请相信,世界上总有一些美好
值得我们全力以赴,哪怕粉身碎骨! ———贺炜