指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针
【格式】:返回类型* 函数名(参数表)
int func(int x, int y)
int* func(int x, int y)
下面展示一个指针函数的相关案例
Open()
函数从外界接收一个值,用于在函数内部开辟出一块大小为n的空间,然后return返回,返回类型为int*
,此时外界使用int*
来进行接收,就获取到了函数内部开辟出这个数组的首元素地址,然后通过循环为数组中n个元素初始化int* Open(int n)
{
int* a = (int*)malloc(sizeof(int) * n);
if (NULL == a)
{
perror("fail malloc");
exit(-1);
}
return a;
}
int main(void)
{
int n = 10;
int* arr = Open(n);
memset(arr, 0, sizeof(int) * n);
for (int i = 0; i < n; ++i)
{
*(arr + i) = i + 1;
}
printf("Initialized Successfully\n");
return 0;
}
通过运行结果可以看出确实可以起到初始化数组的效果
讲完指针函数,我们也来说说它的双胞胎兄弟 —— 函数指针
经过上面所讲的字符指针、数组指针,相信你马上就能类比出函数指针:没错,它就是一个指针,所指向的就是一个数组
数组名
和&数组名
的区别,虽然它们都指向数组的首元素地址,但是在它们往后偏移时,访问的字节数却不同;既然一个数组可以取出它的地址,那么函数是否可以取出它的地址呢?一起来看看函数名
还是&函数名
,它们的地址都是相同的,这是为什么呢?这就是语法规定的,一个函数名取不取地址都是这个函数的地址,因为对于函数来说也没有什么首函数的地址,是吧对于数组的地址,我们可以用数组指针保存起来,那函数可以吗?当然可以,使用到的就是【函数指针】
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
答案揭晓,就是第二个,解析如下
[]
先结合,所以在*
和指针变量外加了一()
,其实对于函数指针也是一样的, 若是不加这个括号的话,就会变成* pf()
,pf就会优先和后面的()
结合,那么这会被编译器当成是一个函数的声明(*pf)
就会是一个指针,向外一看有个()
,说明它指向一个函数,这个函数的参数就是Add形参部分两个参数的类型int
所以Add函数的函数指针应该写成下面这种形式
int (*pf)(int, int) = &Add;
清楚了函数该如何去声明后,那既然有了这个指针,而且它指向一个函数,是否可以通过这个指针去调用这个函数呢?
int ret = (*pf)(3, 4);
printf("ret = %d\n", ret);
int ret2 = Add(1, 2);
printf("ret = %d\n", ret2);
通过调试来观察可以发现,编译器很智能,确实是通过函数指针的指向去找到函数的地址
也可以通过汇编来看,很清晰地看出它们都去call
了这个函数的地址
函数名
还是&函数名
,它们所取到的地址都是一样的,所以我们可以将函数指针的声明写成下面这种形式,读者可以自己去试一下,效果也是一样的int (*pf)(int, int) = Add;
Add
赋给了pf
,然后调用的时候在前面加上一个*
作为解引用,取到这个函数,那其实Add和pf就是一样的,所以我们可以像pf(1, 2)这样去调用函数,具体如下//int ret = (*pf)(3, 4);
int ret = pf(3, 4);
int ret2 = Add(1, 2);
通过运行可以发现效果也是一样的,所以前面的*
其实是可以省略的,甚至你多加几个像(****pf)(3, 4)
都是可以的
通过函数指针的学习,我们来看看下面两道很有趣的代码
下面两题均来自《C陷阱与缺陷》
代码:
(*(void (*)())0)();
解析:
如果你是头一次看上面这段代码的话,心里一定是一个大大的问号???现在我就来解释一下
()
,括号里面的这种形式若是你自己去看的话就是一个函数指针,那相当于就是对0进行一个强制类型转换,把它变成一个函数地址,然后前面的*
我们刚才讲过,就是对这个函数进行解引用,获取到这个函数。那么最后一步便是去调用这个函数具体的分解可以看看下图
void (*)()
—— 》一个没有形参,返回类型为void的函数指针(void (*)())0
——》 对0进行强制类型转换,使其被解释成为一个函数的地址*(void (*)())0
——》对0地址处的函数进行解引用,获取到这个函数(*(void (*)())0)()
——》调用0地址处的函数代码:
void (*signal(int, void(*)(int)))(int);
解析:
同理,若是第一次见一定会被它绕晕了了
signal
,它呢是C语言中的一个信号函数,有兴趣可以去了解一下,我们知道()
的优先级高于*
,所以signal会和后面的内容先结合,那其实已经可以看出这是一个函数声明了。进到里面再来看看这个函数有两个参数,一个是int
,一个是函数指针
,那么外层的又是什么呢?void (*)(int)
的函数指针,其实这就是signal函数的返回类型,是一个函数指针同样地,我们再来捋一遍
分步细说:
void (*)(int)
—— 》是一个函数指针,为signal函数的形参signal(int, void(*)(int))
——》 是一个函数声明,signal与右侧的()
率先结合,内部有两个形参void (*)(int)
——》也是一个函数指针,不过是作为signal函数返回类型优化:
对于上面的这种写法你是否觉得很冗余,其实可以再度进行一个优化,那么你可能很快就看得懂了
void (*)(int)
是出现了两次,之前我们在C语言中有学习过typedef
这个关键字,可以用来对一个很长的数据类型或者变量进行重命名,那么在这里我们也可以这样做(*)
里面,因为语法这么规定了,去掉变量名后就是它的类型typedef void(*ptr_t)(int);
*
不要了,函数指针这里是可以省略的//void (*signal(int, void(*)(int)))(int);
ptr_t signal(int, ptr_t);
原文现身:
指针可以存放在一个数组中,那函数指针可以吗?来看看【函数指针数组】吧,这一块可以先看看指针数组与数组指针的辨析
int (*parr3[10])[5];
[]
改为()
即可,当然你也可以改个名字int (*pfArr[10]();
[10]
,因为[]
的优先级较高,所以pArr会和它先结合,那其实就可以肯定它为一个数组了int (*pfArr)();
声明知道了,那具体怎么使用呢?怎么去接收多个函数的地址呢?再来看看
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int main(void)
{
int (* pfArr[2])(int, int) = {Add, Sub};
int ret1 = pfArr[0](5, 3);
int ret2 = pfArr[1](5, 3);
printf("ret1 = %d\n", ret1);
printf("ret2 = %d\n", ret2);
return 0;
}
{Add, Sub}
,然后还是和函数指针一样的声明,只需要在指针变量后加上一个[2]
即可,那么这就是一个【函数指针数组】
对于【函数指针数组】,我想你应该感受到了它的强大,竟然可以存放多个数组的地址然后根据不同的下标索引找到不同的函数进行调用,如果使用得当,那一定可以事半而功倍
对于函数指针数组而言,有一个很经典的应用就是转移表,简单来说就是计算器
void menu()
{
printf("**************************\n");
printf("***** 1.Add 2.Sub *****\n");
printf("***** 3.Mul 4.Div *****\n");
printf("***** 5.Cls 0.Exit*****\n");
printf("**************************\n");
}
int main(void)
{
int input = 0;
int x = 0, y = 0;
int ret = 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 5:
system("cls");
break;
case 0:
break;
default:
printf("请输入正确的内容:");
break;
}
} while (input);
return 0;
}
但是仔细观察可以发现,每一条case
语句中,都有重复的工作,就显得很冗余,为什么每个case里都要放一个输入呢,这是我后来发现的问题,若是把这个输入放在外面的话,就会造成按下0想要退出的时候还会出现输入运算数的情况,因为这是处于一个do…while的循环之中
但是此处我若是利用函数指针数组的话,就会很方便了
int (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div};
input
来实现不同的功能,只有当input >= 1 && input <= 4
时,才进行运算,此时把输入操作符的逻辑放在这里即可,便不会影响其他功能了do {
menu();
printf("请输入你的选择:>");
scanf("%d", &input);
int (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div};
if (input == 0)
{
break;
}
if (input >= 1 && input <= 4){
printf("请输入两个运算数:>");
scanf("%d %d", &x, &y);
int ret = pfArr[input](x, y);
printf("结果为:%d\n", ret);
}
else if (input == 5) {
system("cls");
}
else {
printf("输入有误,请重新输入\n");
}
} while (input);
学习了函数指针数组后,你是否有联想到取出这个数组的地址再存放到指针里去呢?这不,它来了
*
和()
了,因为存在优先级的问题,指针变量会和[]
相结合,所以我们可以取出函数指针数组的地址,给到一个指针作为接收,这个指针即为ptr
*
作为指针符,这是不对的, 那是用来对函数指针所指向函数的地址进行解引用的,可不能混淆,所以我们要另外再加一个*
,与ptr进行结合(*ptr)[5]
即可,便发现里面存放的都是函数指针。这么分析下来这个【ptr】确实是一个指向函数指针数组的指针再来看一组练习巩固一下
const char*
,返回类型为void
的函数const char*
,返回类型为void
的函数指针void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
回调函数就是一个通过【函数指针】调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
最大的一个目的,就是为了实现:解耦!
在主入口程序中,把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,且不需要修改库函数的实现,变的很灵活,这就是解耦
主函数和回调函数是在同一层的,而库函数在另外一层。如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数了,这也就是在日常工作中常见的情况
注:使用回调函数会有间接调用,因此,会有一些额外的传参与访存开销,对于MCU代码中对时间要求较高的代码要慎用
功能与菜单
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("***** 5.Cls 0.Exit*****\n");
printf("**************************\n");
}
主程序与回调函数
void calc(int (*p)(int, int))
{
int x = 0, y = 0;
printf("请输入两个运算数:>");
scanf("%d %d", &x, &y);
int ret = p(x, y);
printf("结果为:%d\n", ret);
}
int main(void)
{
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 5:
system("cls");
break;
case 0:
break;
default:
printf("请输入正确的内容:\n");
break;
}
} while (input);
return 0;
}
通过画图来看一下是如何通过函数指针来实现的回调
学习过数据结构的同学一定接触过【快速排序】,即QuickSort。不了解的可以看看 数据结构 | 十大排序超硬核八万字详解
base
—— 待排序元素的起始地址,类型为【void】表示可以传递任何类型的数组num
—— 表示待排序数据的元素个数size
—— 表示数组中每个元素所占的字节数int (*compar)(const void*, const void*)
—— 函数指针,用于接收回调函数首先我们用它来排下整型数组试试
cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void test1()
{
int arr[10] = { 2,3,6,7,5,1,4,9,10,8 };
int sz = sizeof(arr) / sizeof(arr[0]);
printarray(arr, sz);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
printarray(arr, sz);
}
运行结果:
cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void*
的指针呢?这种类型的指针一般被我们称作为【垃圾桶】,那垃圾桶我们平常都在用,不考虑垃圾分类的话,可以接收任何种类的垃圾,那么在这里就是可以接收任何类型的数据,即整型、字符型、浮点型,甚至是自定义类型它都可以接受int *
的指针,那么它就指向了我们要待排序的数组。但是要怎么比较和交换两个数据呢,这就要看qsort()函数内部的实现了,它是基于快速排序的思想,如果你懂快速排序的话,脑海里立马就能浮现出它们的比较的场景[strcmp]
也是这样的:
-1
0
1
当然,除了上面这种内置类型外,自定义类型的数据也是可以比较的,接下去我们来比较一下两个学生的信息
typedef struct stu {
char name[20];
int age;
}stu;
void test2()
{
stu ss[3] = { {"zhangsan", 22}, {"lisi", 55}, {"wangwu", 33} };
qsort(ss, 3, sizeof(ss[0]), cmp_byname);
//qsort(ss, 3, sizeof(ss[0]), cmp_byage);
}
void*
类型的指针,但是在比较的时候要转换为结构体指针,否则就无法访问到成员了。对于【姓名】的比较是按照首字母的ASCLL码值来的,这里我们直接使用库函数strcmp
即可,比较的规则和qsort()是一致的Cmp_ByName(const void* e1, const void* e2)
{
return strcmp(((stu*)e1)->name, ((stu*)e2)->name);
}
Cmp_ByAge(const void* e1, const void* e2)
{
return ((stu*)e1)->age - ((stu*)e2)->age;
}
首先来看按照名字排序的结果
然后是按照年龄排序的结果
void BubbleSort(int* a, int 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 t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;
}
}
}
}
但此时我若是要用这个冒泡排序去排任意类型的数据呢?该如何进行修改
qsort()
函数了。我们可以仿照着它的参数来写写看void bubblesort(void* base, int num, int sz, int(*cmp)(const void* e1, const void* e2))
>
、>
、==
了,此处函数指针就派上了用场,我们可是使用函数指针去接收不同的回调函数,继而去实现不同的类型数据的比较,也就是上面所写的Cmp_int
、Cmp_ByName
、Cmp_ByAge
那现在,我们就来实现一下上面说到的这两块内部逻辑
j
和j + 1
这两个位置上的值要如何进行比较的问题,那既然base指向首元素地址,那有同学说不妨让它进行偏移,但是它的类型是void*
,虽然这种类型的指针可以接收各种各样的数据地址, 但是却无法进行偏移,因为它也不知道要偏移多少字节,所以我上面在回调函数内部对两个形参进行了强转才可以进行比较char
类型的字符,在内存中只占有1个字节的大小,那么char*
的指针每次后移便会偏移一个字节,那既然在形参我们传入了数组中每个元素在内存中所占字节数的话,就可以使用起来了,和char*
的指针去做一个配合//判断两数是否需要交换
if (cmp((char*)base + j * sz, (char*)base + (j + 1) * sz) > 0)
{
//两数据交换的逻辑
}
接下去就来实现两数交换的逻辑
char*
指针一个字节一个字节去访问数据的,所以交换的时候也需要按照字节来交换。单独封装一个Swap()函数,把要交换两个数的地址和单个数据所占的字节数传入声明:
void Swap(char* buf1, char* buf2, int sz)
调用:
Swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz);
内部逻辑就是单个数据的交换【记住,这只是单个数据,所以循环sz次】
void Swap(char* buf1, char* buf2, int sz)
{
//两个数据按照字节一一交换
for (int i = 0; i < sz; ++i)
{
int t = *buf1;
*buf1 = *buf2;
*buf2 = t;
buf1++;
buf2++;
}
}
仔细看一下这张图,你就清楚整个调用过程了
我们为什么要用回调函数呢?
记得在一次C++开发面试的时候被被一位主面官问到过这个问题,现在再回答一遍。
我们对回调函数的使用无非是对函数指针的应用,函数指针的概念本身很简单,但是把函数指针应用于回调函数就体现了一种解决问题的策略,一种设计系统的思想。
在解释这种思想前我想先说明一下,回调函数固然能解决一部分系统架构问题但是绝不能再系统内到处都是,如果你发现你的系统内到处都是回调函数,那么你一定要重构你的系统。回调函数本身是一种破坏系统结构的设计思路,回调函数会绝对的变化系统的运行轨迹,执行顺序,调用顺序。回调函数的出现会让读到你的代码的人非常的懵头转向。
那么什么是回调函数呢,那是不得以而为之的设计策略,想象一种系统实现:在一个下载系统中有一个文件下载模块和一个下载文件当前进度显示模块,系统要求实时的显示文件的下载进度,想想很简单在面向对象的世界里无非是实现两个类而已。但是问题恰恰出在这里,显示模块如何驱动下载进度条?显示模块不知道也不应该知道下载模块所知道的文件下载进度(面向对象设计的封装性,模块间要解耦,模块内要内聚),文件下载进度是只有下载模块才知道的事情,解决方案很简单给下载模块传递一个函数指针作为回调函数驱动显示模块的显示进度。
下面是模拟实现这个文件下载模块的代码,仅供参考
#include
#include
#include
typedef void(*on_process_callback)(std::string data);
//处理完成的回调
void on_process_result(std::string data)
{
//根据返回消息进行处理
std::cout << data.c_str() << std::endl;
};
class TaskProcessing
{
public:
TaskProcessing(on_process_callback callback) : _callback(callback)
{};
void set_callback(on_process_callback callback)
{
_callback = callback;
};
void do_task()
{
//当文件传输完成
if (_callback)
{
srand((int)time(NULL));
if (rand() & 1)
{
(*_callback)(std::string("ftp succeed"));
}
else
{
(*_callback)(std::string("ftp failed"));
}
}
};
private:
on_process_callback _callback;
};
int main()
{
TaskProcessing* process = new TaskProcessing(on_process_result);
process->do_task();
system("pause");
}
最后来总结一下本文所学习的内容
以上就是本文要介绍的所有内容,感谢您的阅读