前言:笔者最近需要在ubuntu平台上写一个类似于TimeSetEvent的计时器,实现按一定频率重复执行回调函数的功能。这就需要运用C/C++函数指针的性质。由于笔者希望计时器预留给回调函数的接口可以适配任何函数,所以需要使用C++泛型编程的性质并解决回调函数参数多样化的问题。这篇文章是笔者解决问题的学习笔记和记录,欢迎交流和指正。
函数是有地址的,函数的地址是存储其机器语言代码的内存的开始地址。
#include <stdio.h>
int func(int a,int b)
{
return a+b;
}
int main(void)
{
printf("%p",func);
}
读者可以自行编译上面这一段程序,发现程序可以输出结果。即函数编译后机器语言代码在内存中的起始位置。
int型变量存在地址,所以有int*类型的指针,指向int型变量,储存int型变量的地址;函数也存在地址,所以也应该存在特定类型的指针,指向函数,储存函数的地址。这便是函数指针。
函数指针存在很多类型。通过其指向函数的返回值和参数列表来决定其类型。例如,上面的代码段的func函数,其返回值类型为int,参数列表为(int , int)。那么,指向func函数的指针的类型被定义为
int (*)(int,int)
具体的,读者可以按照如下方式定义一个指针型变量p1,用于存储func函数的地址:
int (*p1)(int,int);
注意,p1作为指针的名称,没有出现在最后,而是出现在括号内。
因此,下面这一段代码是可以通过编译的:
int func(int a,int b)
{
return a+b;
}
int main(void)
{
int (*p1)(int,int)=func;
}
以此类推,返回值为void,参数为void的函数指针类型为
void (*)(void)
返回值为float,参数为两个float类型数组的指针类型为
float (*)(float*,float*)
如上,p1是函数func的指针,那么通过以下方法可以调用func函数:
int c=(*p1)(3,5);
考虑到语言设计的包容性,事实上,对p1指针解引用的过程可以被省略,也就是说,下面这个操作也是允许的:
int d=p1(3,5);
?具体示例如下:
#include <iostream>
int func(int a,int b)
{
return a+b;
}
int main(void)
{
int (*p1)(int,int)=func;
int c=(*p1)(3,5);
std::cout<<c<<std::endl;
int d=(p1)(3,5);
std::cout<<d;
return 0;
}
输出结果:
8
8
?
按照C/C++规范,其实函数也是有类型的。就比如我们的func函数,其类型为:
int(int,int)
而p1作为指向它的指针,也具有类型:
int (*)(int,int)
两者便有了对应关系。
我们在声明一个函数的时候,就好像声明了一个变量。如下,声明func函数:
int func(int,int);
#include <iostream>
int func(int,int);
int main(void)
{
int (*p1)(int,int)=func;
int c=(*p1)(3,5);
return 0;
}
int func(int a,int b)
{
return a+b;
}
声明了func,就是告诉编译器程序里有一个int(int,int)类型的函数。我们之前觉得古怪的函数声明,是不是逐渐变得合理而自然而然起来了?
函数指针这一性质的价值可以体现在泛型编程中。这里笔者使用一个简单的例子进行说明。比如说,我要实现一个最简单的定时器,让程序能够在启动定时器的5秒后执行某个回调函数(回调函数,一般指通过函数指针执行的函数)
#include <iostream>
#include <ctime>
#include <thread>
void printnum(int num)
{
std::cout<<num<<std::endl;
}
//计时器函数使用泛型编程,FuncPtr为函数指针,FuncPam为函数参数
template <typename FuncPtr,typename FuncPam>
void timer(int period,FuncPtr funcptr,FuncPam funcpam)
{
clock_t time1=clock();
clock_t time2;
clock_t delta;
while(true)
{
time2=clock();
delta=time2-time1;
if (delta>=period)
{
//通过函数指针调用函数 ,参数为funcpam
(*funcptr)(funcpam);
break;
}
}
}
int main(void)
{
//创建计时器线程,设定为5秒后执行回调函数printnum
std::thread timer1(timer<void(*)(int),int>,5000,printnum,2024);
timer1.join();
}
通过函数指针和泛型编程,timer定时器可以被设定为任意后执行任意参数数量为1、任意返回类型的回调函数。试想,没有函数指针,完全无法达到这个目的!
想必读者一定注意到,我上面写的计时器所能执行的回调函数类型还是有限的。如果存在一个回调函数,它没有参数,或是它有多个参数,那么就无法用这个计时器触发了!
十几年前,制定C++语法规则的大佬们解决了这个问题。
在C++11标准中,引入了“可变参数模板”,它表示可以接受任意数量的模板参数,并将它们打包为一个模板参数包。在函数模板中,我们可以使用 typename...?来定义一个模板参数包。例如:
template <typename... Args>
void my_function(Args... args)
{
// ...
}
使用该模板时,也要标明...
根据这个性质,可以改进我们的计时器:
#include <iostream>
#include <ctime>
#include <thread>
void printnum()
{
std::cout<<2024<<std::endl;
}
//计时器函数使用泛型编程,FuncPtr为函数指针,FuncPam为函数参数列表
template <typename FuncPtr,typename... FuncPam>
//模板处typename后要加上...
//使用泛型FuncPam时要加上...
void timer(int period,FuncPtr funcptr,FuncPam... funcpam)
{
clock_t time1=clock();
clock_t time2;
clock_t delta;
while(true)
{
time2=clock();
delta=time2-time1;
if (delta>=period)
{
//通过函数指针调用函数,没有参数
(*funcptr)(funcpam...);
//使用泛型funcpam时要加上...
break;
}
}
}
int main(void)
{
//创建计时器线程,设定为5秒后执行回调函数printnum
std::thread timer1(timer<void(*)(void)>,5000,printnum);
timer1.join();
}