C语言 - 函数指针与回调函数
1.函数指针
函数指针:首先它是一个指针,一个指向函数的指针,在内存空间中存放的是函数的地址;
1.1 函数指针的定义形式
函数指针的定义: 函数的返回值类型(*指针名)(函数的参数列表类型)
1.2 函数指针的使用方法
#include <stdio.h>
//返回两个数中的最大值
int max(int a, int b)
{
return a > b ? a : b;
}
int main()
{
int x = 100;
int y = 200;
int maxval = 0;
//定义函数指针
int (*pmax)(int, int) = max; //也可以写作int (*pmax)(int a, int b)
//使用函数指针去调用函数
maxval = pmax(x, y); //写法1
//maxval = (*pmax)(x, y);//写法2
printf("Max value: %d\n", maxval);
return 0;
}
第 14 行代码对函数进行了调用。pmax 是一个函数指针,在前面加 * 就表示对它指向的函数进行调用。注意( )
的优先级高于*
,第一个括号不能省略
程序运行结果:
1.3 函数指针充当函数参数
下面实现了一个简单的回调函数
#include <stdio.h>
int Max(int a, int b)
{
return a > b ? a : b;
}
int Min(int aa, int bb)
{
return aa > bb ? bb : aa;
}
int Sum(int a, int b)
{
return a + b;
}
//以函数指针充当函数参数
void callback(int(*pFun)(int, int), int a, int b)
{
printf("%d\n", pFun(a, b));//函数指针调用
}
int main()
{
//函数指针充当函数参数
//回调函数: 统一接口
callback(Max, 1,2); //最大值
callback(Min, 3,4); //最小值
callback(Sum, 1,1); //求和
return 0;
}
1.4 函数指针充当函数返回值
#include <stdio.h>
int Max(int a, int b)
{
return a > b ? a : b;
}
//这样写会报错!编译器解析不了函数的返回值,需要使用typedef给函数指针起别名
//int(*p)(int, int) returnFunction(int(*pf)(int, int), int a, int b)
//{
// return pf;
//}
//函数指针起别名
typedef int(*PFUNC)(int, int);
//函数指针充当函数返回值
PFUNC returnFunction(PFUNC pf, int a, int b)
{
printf("%d\n", pf(a, b));
return pf;
}
int main()
{
PFUNC p = returnFunction(Max, 1, 2);
printf("%d\n", p(5, 6));
return 0;
}
1.5 使用typedef给函数指针起别名
#include <stdio.h>
//1.给简单数据类型别名
typedef int 整数;
//2.给数组起别名
typedef int ARRAY[3];
//万能看typedef语句
//1.直接用别名定义的名字替换别名
//typedef int array[3];
//2.去掉typedef 剩下就是你定义的东西
// int array[3]
typedef int ARRAY2D[2][2];
int Max(int a, int b)
{
return a > b ? a : b;
}
//3.函数指针起别名
typedef int(*PFUNC)(int, int);
//函数指针充当函数参数
PFUNC returnFunction(PFUNC pf, int a, int b)
{
printf("%d\n", pf(a, b));
return pf;
}
int main()
{
int iNum = 1;
整数 iNum2 = 2;
ARRAY array; //int array[3]
for (int i = 0; i < 3; i++)
{
array[i] = i;
printf("%d\t", array[i]);
}
printf("\n");
ARRAY num[2]; //int num[2][3];
ARRAY2D array2D; //int array2D[2][2];
PFUNC pf = NULL;
//int(*pf)(int, int)=NULL;
//很少见,基本不这样用,函数指针更多的使用方式是: 调用函数,充当函数参数
pf = returnFunction(Max, 1, 2);
printf("%d\n",pf(100, 200));
//上面两句话等效下面一句话
int result = returnFunction(Max, 1, 2)(100, 200);
printf("%d\n", result);
return 0;
}
1.6 复杂的函数指针
#include <stdio.h>
//void(*p)(); 去掉变量名剩下就是指针: void(*)()
void print()
{
printf("空类型的指针充当函数指针\n");
}
int Max(int a, int b)
{
return a > b ? a : b;
}
int Sum(int a, int b)
{
return a + b;
}
void printData(int(*pMax)(int, int), int a, int b)
{
printf("%d\n", pMax(a, b));
}
//void printData(int(*pMax)(int, int), int a, int b)
//void (*p)(int(*pMax)(int, int), int a, int b)
//void(*p)(int(*)(int, int),int,int)
//以一个以函数指针为参数的函数指针为参数的函数
void userPrint(void(*p)(int(*)(int, int),int,int), int(*pMax)(int, int), int a, int b)
{
p(pMax, a, b);
}
int main()
{
//1.万能指针充当函数指针
void* pVoid = NULL;
//使用前必须做类型转换
pVoid = print; //print的类型: void(*)();
((void(*)())pVoid)();
(*(void(*)())pVoid)();
//2.以一个以函数指针为参数的函数指针为参数的函数
userPrint(printData, Max, 1, 2);
userPrint(printData, Sum, 1, 2);
void (*pFunc)(void(*)(int(*)(int, int), int, int),
int(*)(int, int),
int, int) = NULL;
pFunc = userPrint;
pFunc(printData, Sum, 100, 200);
//3.函数指针数组:多个函数指针的集合
int (*pMax)(int, int) = NULL;
int(*pArray[2])(int, int);
typedef int(*pXX)(int, int);
pXX X[2];
X[0] = Max;
X[1] = Sum;
//int *array[2];
pArray[0] = Max;
pArray[1] = Sum;
for (int i = 0; i < 2; i++)
{
printf("%d\n",pArray[i](100, 200));
}
typedef int(*B)(int, int);
typedef int(*C)(int, int);
typedef void(*A)(B b, int, int);
void (*pFunc)(A a,C c,int, int) = NULL;
//void XXX(A a, C c,int d,int e);
return 0;
}
2. 函数指针数组
函数指针数组可以理解为将函数指针储存进数组中,这个数组便是函数指针数组。
2.1 函数指针数组的定义
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int main()
{
//函数指针数组的定义
int (*Farr[2])(int x, int y) = { Add, Sub };
//调用
int ret = Farr[1](2, 5);
printf("%d\n", ret);
return 0;
}
函数指针数组的定义与数组指针数组的定义类似,我们定义的Farr先与方括号结合,形成数组,拿去数组后,剩下的便是该数组的类型,也就是函数指针。
2.2 函数指针数组的应用
我们可以利用函数指针数组完成计算器功能(转移表)
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int input = 0;
//我们将函数添加进函数指针数组中
int (*Farr[])(int, int) = {0, Add, Sub };
do
{
printf("请选择算法(1.Add 2.Sub 0.exit):>");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器\n");
break;
}
if (input >= 1 && input <= 2)
{
int x = 0;
int y = 0;
printf("请输入两个操作数:>");
scanf("%d%d", &x, &y);
//通过调用函数指针数组使用对应的函数
int ret = Farr[input](x, y);
printf("%d\n", ret);
}
else
{
printf("输入有误,请重新输入\n");
}
} while (input);
return 0;
}
使用函数指针数组后,我们可以避免switch语句的使用,是代码看起来更整洁。
3. 回调函数
3.1 什么是回调函数呢?
回调函数本质上也是普通函数,只是调用机制有所区别——首先通过传参的形式将该函数的地址传递给其他函数,然后在其他函数中通过函数指针调用该函数。在其他函数中通过函数指针调用该函数的过程称为回调,而作为被调用的该函数则被称为回调函数。有人可能会疑惑何必多此一举,直接在其他函数中调用该函数就好,为何一定要通过指针中转。
3.2 为什么需要回调函数
这就不得不提到项目联合开发带来的必然后果——接口的兼容性问题。举个超级简单的例子:程序员小A和程序员小B联合开发一个项目,要求小A开发的函数必须为小B开发的函数提供灵活可用的接口。 假如小A小B是好朋友,他们天天在一起,而且也不考虑开发效率问题,那就串行方式一起开发好了。如下例所示,在同一个文件中,小A先负责开发Add()函数,小B后负责开发add()函数,小A需要调用小B的函数,即小A为小B提供了一个接口,通过这个接口小B可以随意通过更改add()函数,间接更改了Add()函数。 由于小A在使用小B的函数前小B还没有实现,所以他们需要先一起商量一下小B需要实现的函数名称、函数返回值、参数类型,然后小A在调用前先声明。小A在完成自己的工作后就可以休息了,然后小B就按照之前商量好的定义函数,对于函数体中具体怎么写完全由小B自由发挥了:
/*test.h*/
#include<iostream>
using namespace std;
int add(int, int);
void Add(int a, int b)
{
cout << add(a, b) << endl;
}
int add(int a, int b)
{
return a + b;
}
/*main.cpp*/
#include"test.h"
void main()
{
Add(1, 2);
}
延续上文的故事,但现在场景变了。为了提高效率需要小A和小B并行操作,但工作类容没有变。显然他们需要在两个文件中完成各自的任务。小A同样需要声明小B的函数。对于现实中的项目开发而言,这种方式最为常见。
/*test.h*/
#pragma once
#include<iostream>
using namespace std;
int add(int, int);
void Add(int a, int b)
{
cout << add(a, b) << endl;
}
/*test.cpp*/
#include"test.h"
int add(int a, int b)
{
return a + b;
}
void main()
{
Add(1, 2);
}
看到这里你肯定回想,这也太简单了,完全没有啰嗦的必要,其实我只是想一步步引出为什么要有回调函数的存在。通过上面的例子我们不难发现,小A为小B提供了接口,使得小B定义的函数能够在合适的时机被调用,这里必须有一个前提是小A和小B要提前商量好小B所需实现的函数的名称、返回值、参数列表。 而现在我们延续上面的故事,但场景又变了,小A和小B完全不认识,甚至小A不知道小B要用自己的接口,那么再按照上面的思路无论对小A还是小B来说都不那么友好了。首先对于小A来说,每次调用小B定义的函数前都需要声明,很是麻烦。而对于小B来说,由于小A定义的函数函数体对于小B来说是封装看不见的,小B对于自己定义的函数如何传到小A定义的函数中去很不直观,而且小B定义的函数名称、返回值类型、参数列表必须与小A在之前做出的声明保持完全一致。 因而,回调函数闪亮登场。如下例所示,小A不再需要每次调用小B定义的函数之前都要进行声明,小A只需要提供一个函数指针来接收小B传过来的函数的地址,而不用管函数名是什么。小A用这个指针就可以直接调用小B定义的函数。小B可以自己起函数名,只是返回类型和参数列表必须和小A的声明保持一致。
/*test.h*/
#pragma once
#include<iostream>
using namespace std;
void Add(int(*callbackfun)(int, int), int a, int b)
{
cout << callbackfun(a, b) << endl;
}
/*test.cpp*/
#include"test.h"
int add(int a, int b)
{
return a + b;
}
void main()
{
Add(add, 1, 2);
}
上面例子可以看出,回调函数必须通过函数指针进行传递和调用,为了简化代码,一般会将函数指针起个别名,格式为:
typedef 返回值类型 (*指针名) (参数列表)
则上面的例子部分代码可以修改如下。无论是pf(a,b)还是(pf)(a,b)都是可以的,我打印了pf和pf,发现它们都表示的是同一函数的地址。
typedef int(*callbackfun)(int, int);
void Add(callbackfun pf, int a, int b)
{
cout<<pf<<endl;
cout<<*pf<<endl;
cout << (*pf)(a, b) << endl;
cout << pf(a, b) << endl;
}
回调函数规避了必须在调用函数前声明的弊端,而且能够让用户直观地感受到自己定义的函数被调用。小A需要在声明函数指针时规定参数列表,小B在定义回调函数时需要与小A声明的函数指针保持相同的参数列表。当然小B可以自己决定在函数体中是否使用这些参数,前提是小A会提前为回调函数形参设置默认值。
/*test.h*/
#pragma once
#include<iostream>
using namespace std;
typedef int(*callbackfun)(int, int,int);
void Add(callbackfun pf, int a=0, int b=0,int c=0)
{
cout << pf(a, b,c) << endl;
}
/*test.cpp*/
#include"test.h"
int add(int a, int b,int c)
{
return a + b;
}
void main()
{
Add(add, 1, 2);
}
3.3 有哪些函数可以做回调函数
可以做回调函数的函数在C++中目前我遇到过的有两种,第一种是C语言风格的函数,第二种是静态成员函数。第一种上面已经详细介绍过了,就不再赘述。第二种我在此做详细说明。
3.4 静态成员函数做回调函数
众所周知,类的非静态成员函数的参数列表中隐含了一个this指针,当用类的对象访问类的非静态成员函数时,C++编译器会将this指针指向该对象,从而保证了函数体中操纵的成员变量是该对象的成员变量。即使你没有写上this指针,编译器在编译的时候自动加上this指针。 而在调用回调函数的函数中,它会提供接受回调函数地址的函数指针,该函数指针严格规定了参数列表。由于this指针的存在,非静态成员函数的参数列表和函数指针的参数列表参数个数无法匹配。如下例所示,函数指针callbackfun有两个参数,非静态成员函数隐含this指针从而有三个参数,显然不匹配。
class AddClass
{
private:
int a, b;
public:
int add(int aa, int bb);
};
int AddClass::add(int aa, int bb)
{
a = aa;
b = bb;
return a + b;
}
void Add(int(*callbackfun)(int, int), int a, int b)
{
cout << callbackfun(a, b) << endl;
}
void main()
{
Add(AddClass().add, 1, 2);
}
当然如果你执着于使用非静态成员函数,就只能通过普通全局函数做中转,如下例所示。虽然能够实现,但看起来总是傻傻的。
class AddClass
{
private:
int a, b;
public:
int add(int aa, int bb);
};
int AddClass::add(int aa, int bb)
{
a = aa;
b = bb;
return a + b;
}
void Add(int(*callbackfun)(int, int), int a, int b)
{
cout << callbackfun(a, b) << endl;
}
void main()
{
Add(AddClass().add, 1, 2);
}
类的静态成员函数属于类,为所有对象所共享,它没有this指针,因此这里我们采用静态成员函数作为回调函数。但这里我们又遇到了一个问题,就是静态成员函数是无法访问类的非静态成员的,因此如果用户有访问非静态成员的需求,那么需要在静态成员函数的参数列表中做一点小小的修改。在形参列表中加入万能指针void*作为一个形参,然后在函数体中再进行类型强转。
/*test.h*/
#include<iostream>
using namespace std;
typedef int(*callbackfun)(int, int, void*);
void Add(callbackfun pf, int a, int b,void* p)
{
cout << pf(a, b, p) << endl;
}
/*test.cpp*/
#include<test.h>
class AddClass
{
private:
int a, b;
public:
static int add(int aa, int bb,void* temp);
};
int AddClass::add(int aa, int bb,void* p)
{
AddClass* temp = (AddClass*)p;
if (temp) {
temp->a = aa;
temp->b = bb;
}
return temp->a + temp->b;
}
void main()
{
AddClass* a = new AddClass;
Add(AddClass::add, 1, 2,a);
}
3.5 小结
①回调函数本质其实是对函数指针的一种应用,上面的例子都比较简单,还没有完全体现回调函数的威力。
②回调函数是一种设计系统的思想,能够解决系统架构中的部分问题,但是系统中不能过多使用回调函数,因为回调函数会改变整个系统的运行轨迹和执行顺序,耗费资源,而且会使得代码晦涩难懂。
③C++ STL中有大量使用回调函数的例子。如下例所示,遍历函数for_each()中的lamda表达式就是一个回调函数,当然我们也可以像上面那样定义全局函数或静态成员函数然后将地址传进去。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
void main()
{
vector<int> v{ 1,4,6,5,3 };
for_each(v.begin(), v.end(), [=](int val){cout << val << " ";});
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10亿数据,如何做迁移?
· 推荐几款开源且免费的 .NET MAUI 组件库
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· c# 半导体/led行业 晶圆片WaferMap实现 map图实现入门篇
· 易语言 —— 开山篇