查看原文
其他

万字长文系统梳理C++函数指针

脚本之家 2021-06-30

The following article is from 帝都高级待业专家 Author 三丨级丨狗

  脚本之家

你与百万开发者在一起

来源 | 帝都高级待业专家 (ID:ThreeDog521

如若转载请联系原公众号

本篇的内容相对比较简单 主要从语法的层面讲解函数指针的使用以及应用场景。都是些面向入门者的基础,大佬轻喷。

首先:什么是函数指针。

这个问题老生常谈了,不用理解的多么复杂,它其实就是一个特殊的指针,它用于指向函数被加载到的内存首地址,可用于实现函数调用。

听上有点像函数名,函数名也是记录了函数在内存中的首地址,加()就可以调用。

不错,不过函数指针和函数名还是有点区别的,他们虽然都指向了函数在内存的入口地址,但函数指针本身是个指针变量,对他做&取地址的话会拿到这个变量本身的地址去。

而对函数名做&取址,得到的还是函数的入口地址。如果是类成员函数指针,差别更加明显。

关于函数名和函数指针的差异,找到一篇帖子介绍的比较深入,如果看完这篇文章你还没晕的话,可以回过头来去看看这位大佬的讲解https://www.cnblogs.com/hellscream-yi/p/7943848.html

函数指针有啥用?

和通过函数名调用一样,函数指针给我们提供了另一种调用函数的可能

而他又具备变量的特性,可以作为参数传递,可以函数返回

因此在一些直接通过函数名无法调用的场景下,函数指针就有了用武之地。

我们接下来还是先说说函数指针怎么写,完后再提供一些具体应用场景来说明它有什么用。

函数指针的写法

大多数初学者包括我在内,潜意识里对于函数指针都有点抵触,能不用的时候都尽量不用。

因为我们印象里见过的函数指针很可能是这样的:

double * (*p1)(const double * , int m);
int (*funcArry[10])(intint);
typedef char * (MyObject::*FUNC_PTR )(const chat * str);
void * (* ( * fp1)(int))[10];
#define double (*(*(*fp3)())[10])();
int (*(*fp4())[10])();

甚至还有:

int *(*(*fp)(int(*)(intint), int(*)(int)))(intintint(*)(intdouble * (p1)(const double * , int m)));

好在一般这种反人类的写法,只会经常出现在大学的期末试卷里,生产实践中谁也不会把函数写成这个鬼样子。

不过这也奠定了我们内心深处对于函数指针深深的抵触和恐惧。

普通函数指针

言归正传,我们来说说函数指针的语法该怎么理解。

声明

函数指针就是一种特殊的指针。

如果你要声明一个变量:

int a ;

而一个指针呢:

int *a;

那一个函数指针,就是在一个变量指针的写法基础上加一个括号,告诉他这是一个指向函数的指针就可以:

int (*a)();

这样,a就是一个函数指针了。

这个括号(*a)一定要加,否则就成了int *a();编译器会认为这是一个 返回int *的函数a;

这时候呢,int (*a)();就声明了一个函数指针变量a,它可以指向一个返回int,参数列表为空的函数。

前面的int,就是这个函数指针的返回值,a是变量名,最后一个()是参数列表。

赋值

直接将一个已经定义的函数名,赋值给函数指针就可以:a = function;

当然,直接把声明定义和初始化写在一起也可以,只是平常不多见这么写:int (*a)() = function;

和上面先声明再赋值是等价的。

调用

函数指针的变量,可以当做函数名一样被调用,所以直接:a();就相当于调用了函数。

注意这是声明的一个函数指针的变量,和函数的声明有所区别。

因此你不能像定义一个函数一样定义一个函数指针,你只能声明出这个指针,然后给他赋值一个函数签名匹配的已经定义好的函数名:

int function()  // 正确的函数声明
{
    return 0;
}
 
int (*a)()      // 错误:这是一个变量,不能当函数一样定义
{
    return 0;
}

//你只能这样:
int (*a)();     //声明一个函数指针变量a,
int main()
{
    a = function;   //给函数指针赋值。
    a();            //通过函数指针调用
    
    // 也可以直接把声明和赋值写在一起:这就像是 int i;和int * p = i;的区别
    int (*b)() = function;
    b();

    return 0;

稍微复杂一些的函数指针

给函数指针赋值的时候,对应的函数签名(返回值和参数列表)必须是和他的相匹配的。

如果对应的函数原型比较复杂,相对应的函数指针的写法也会复杂一些。

这里循序渐进地举几个相对复杂一些的例子:

// 最简单的函数及其对应的函数指针:
void f();
void (*f_ptr)();
// 复杂点的,带返回值和参数列表,但都是基本类型
int f(double b, int i);
int (*f_ptr)(double b, int i);
// 返回值和参数带上指针,再加上几个const混淆一下
const double * f(const double * b2, int m);
const double * (*f_ptr)(const double * b2, int m);
// 再复杂一点点,参数里加个函数指针 也不是很复杂,基本只要把函数名换成(*函数名) 就可以了
int f(int (*fp)(),int a );
int (*f_ptr)(int (*fp)(),int a ); 
// 稍微再复杂一点点,返回值是一个函数指针:(光是普通函数返回函数指针,语法就有点费劲。我们一步一步来:)
//// 首先搞一个返回void的普通函数:
void f();
//// 假设返回一个函数指针,这个函数指针返回值和参数都为空。我们用一个函数指针替换掉返回值void就可以了
//// 感觉应该写成这样:void (*fp)() f();
//// 但是这个样子显然过不了编译的,得要变一下:
void (* f())();         //这就是一个参数为空,返回函数指针的函数。
void (*(*f_ptr)())();    //把f替换成(*f_ptr),这就成了返回函数指针的函数指针。

// 其实写成上面这个样子,大多数人已经懵逼了。
// 再往复杂的搞,真就彻底花了,比如返回值和参数里整上函数指针数组,函数指针参数里套函数指针,返回的函数指针返回值是个函数指针等等
// 这种的我们就不研究了。一方面项目中这么写会挨骂,另一方面太复杂的我也不会。

从一开始的void f();,到最后成了这个void (*(*f_ptr)())();鬼样子

说真的最后这种写法我是正向推导过来的,如果是你维护别人的代码,上来看到一个这void (*(*f_ptr)())();,恐怕得先骂一会儿娘才能正式开始工作

然而这却只是返回函数指针的函数指针的最简单的写法,参数全为空,返回全为void,也不涉及指针数组,还完全没有进行太多反人类的语法变种。

好在,我们还是有办法给他整的简化一点的

把函数指针弄成一个自定义类型

我们把关注点聚焦到上面最后一个函数指针上,定义一个返回值是函数指针的函数,完整的声明加调用应该是这样的:

#include <iostream>
using namespace std;

void aaa()
{
 cout << "aaa" << endl ;
}

void (* f())()  // 返回函数指针的函数f
{
 return aaa;
}

int main()
{
 void (*(*f_ptr)())() = f;   // 返回函数指针的函数指针f_ptr
 //f_ptr() 返回一个函数指针,所以可以再跟一个()调用这个被返回出来的函数
 f_ptr()(); 
    return 0;

和我们平时返回int double不同,返回函数指针的这种语法实在太过抽象。

所以,我们能不能想办法,把函数指针给搞成一种类型,然后就像int double一样去使用?

当然是可以的,这也是我们最常见的函数指针的玩法。我们可以使用typedef,直接将此函数指针处理成一个类型:

  • void (*f_ptr)();:这是定义了一个名为f_ptr的函数指针「变量」
  • typedef void (*f_ptr)();:这是定义了一个名为f_ptr的函数指针「类型」,这个类型代表返回值为空,参数为空的函数指针类型。
  • 有些地方觉得f_ptr的名字起得不好,还会再用#define FUNC_PTR f_ptr这样搞一下,后面代码中统一使用FUNC_PTR代表这个函数指针类型。

区别是什么呢?如果类比我们熟悉的普通变量类型int:

  • 那上面的第一行,就相当于int a;,a是一个整型变量;
  • 第二行呢,就相当于typedef int a,这样一来a,就相当于是int,可以用a i; a j;'的方式声明整型变量i,j

有了这个f_ptr类型,上面很多复杂的定义写法就可以简化,而且语义一下子就清楚很多了:

  • 声明一个函数指针并赋值:
// void (*fp)() = func;
f_ptr fp = func ; 
  • 函数参数里包含函数指针:
//int f(int (*fp)(),int a );
int f(f_ptr fp, int a);
  • 返回值是函数指针,我们直接把上面那段完整的代码通过typedef重写一下:
//函数定义:
#include <iostream>
using namespace std;
typedef void (*f_ptr)();
void aaa()
{
    cout << "aaa" << endl ;
}

// void (* f())()
f_ptr f()   //返回值是函数指针的函数定义, 语义一目了然
{
    return aaa;
}

int main()
{
    // void (*(*f_ptr)())() = f;
    // f_ptr()(); 
    f_ptr (*ff)() = f; //返回函数指针的函数指针 
    ff()();
    return 0;

当然还可以写的更抽象一些,把返回函数指针的函数指针也typedef一下:typedef void (*(*F_PTR)())();

这下定义的时候直接把上面的f_ptr (*ff)() = f;换成:F_PTR ff = f ;,更是简洁明快。

到这里呢,我们就基本掌握了函数指针的写法和用法,其实很简单。

稍微总结一下上面的内容:

  • 如何声明一个简单的函数指针:void (*f_ptr)()
  • 给函数指针赋值:fp = function; function是一个已经定义的函数名
  • 通过函数指针调用函数:fp();
  • 复杂一些的函数指针:
    • 复杂的返回值
    • 多个参数
    • 参数里带函数指针
    • 返回值是函数指针的情况。
  • 这种写法太麻烦了怎么办?把函数指针搞成一个类型:typedef void (*f_ptr)();
    • 用这个类型声明一个函数指针:f_ptr fp;
    • 返回这个类型函数指针的函数f_ptr f();
    • 参数包含这个类型函数指针的函数:int f(f_ptr fp, int a);
    • 套娃函数指针————返回函数指针的函数的函数指针:f_ptr (*ff)();

再把数组扯进来

之所以一直不扯,是因为函数指针和数组结合在一起的话,可读性一下下降了好几个数量级

掌握了上面的写法,我们再把复杂度提升亿点点:定义一个长度为10数组,数组中的元素是函数指针:

  • 长度为10的数组:int a[10];
  • 那么长度为10的函数指针数组,就先把int换成函数指针:void (*f_ptr)() a[10];
  • 当然函数指针的声明时,函数指针名就是变量名,所以这个a就没用了,应该写成这样:void (*f_ptr)()[10]

遗憾的是这种想当然的写法当然过不了编译,一个数组声明的时候,[]要紧跟在变量名之后

所以正确的声明、赋值与调用写法是:

void (*f_ptr[10])();    // 定义一个长度为10的数组,数组中的元素类型是函数指针
f_ptr[3] = function;    // 每一个元素都可以指向一个函数,我们赋值给第数组中的第四个元素函数function的地址
f_ptr[3]();             // 通过数组下标拿到函数指针,通过函数指针调用函数。 这里相当于调用了function();

当然,上面提到了typedef大法,可以帮助我们简化上面这种写法:(说是简化,其实写的更多,但是可读性更好)

typedef void (*f_ptr)();
f_ptr f_tpr_arrya[10];      //把f_ptr当做一种类型后,声明函数指针数组,就可声明普通的int数组看上去没啥区别了。
f_tpr_arrya[3] = function;
f_tpr_arrya[3]();             

这是最基本的函数指针数组,他里面存放的元素是签名最为简单的函数指针。

如果这个数组里记录的函数指针签名复杂一些,一旦套起娃来那画风将可以用恐怖来形容。

这里不深入探讨了,举几个例子:(主要摘录自:https://www.xuebuyuan.com/1238896.html)

  • const char *(*f_ptr[10])(int a[], double * b) 长度为10的数组,数组元素为返回const char *,参数(int [],double *)的函数指针。
  • const char *(*f_ptr[10])(double * (*b[10])(int ,int )):长度为10的数组,数组元素为返回const char *,参数为“返回double*参数为int,int的函数指针数组”的函数指针。
  • Void * (* ( * fp)(int))[10]:fp是一个函数指针,它指向的函数带有一个int型的参数,返回值为一个指向含有10个void指针数组的指针。
  • void * (* ( * fp[10])(int))[10]:fp是一个长度为10的函数指针数组,元素里的函数指针指向的函数带有一个int型的参数,返回值为一个指向含有10个void指针数组的指针。
  • Void * ( * fp)(int)[10]:fp是一个函数指针,它指向的函数带有一个int型的参数,返回值为一个指向含有10个void类型的数组的指针。
  • Void ( * fp)(int)[10]:fp是一个函数指针,它指向的函数带有一个int型的参数,返回值为一个有10个void类型的数组。
  • double (*(*(*fp)())[10])():fp是一个函数指针,它指向的函数不带参数,返回值是一个指针,该指针指向一个指针数组,该指针数组容量为10。指针数组中的指针又是函数指针,该指针指向的函数不带参数,返回值为double。
  • int (*(*fp())[10])();:fp的返回值是一个指针,该指针指向含有10个函数指针的数组。数组中的指针指向的函数不带参数,返回值为int。

可以看到函数指针一和数组扯到一起,写法抽象程度一下子就上了一个量级。

平时写代码的时候,最好还是用typedef把函数指针的类型定义一下,不要写的太花。

虽然我从来喜欢大道至简,但是函数指针数组这种搞法确实还是有一定的应用场景的。

比如我们后面将要提到的转移表

类的函数指针

函数指针是指向函数的指针,而我们上面提到的函数,一直都是面向过程的函数,对于面向对象的函数还只字未提。

我们下面仅仅讨论一下c++中类的函数指针的最简单的语法规范,上面那些高深莫测的套娃函数指针,就不和类函数指针扯到一起了。

面向对象的编程中,函数被新搞出了两种花样:「静态函数和成员函数」

关于静态函数和成员函数这两种函数的区别也是老生常谈的问题,我们关于函数指针的讨论,在这里只需要记住一句最核心的一句话:「静态函数没有this指针。」

类静态成员函数指针

类的静态成员函数没有this指针,它的存储方式和普通的函数是一样的,可以取得的是该函数在内存中的实际地址

所以静态的成员函数指针的声明和调用,和普通函数指针没有任何区别:

  • 声明:void (*static_fptr)();
  • 调用:static_fptr();

唯一有区别的,就是赋值。因为要传的是一个类的静态成员函数的地址,所以赋值的时候,要加上类名限定:

  • void (*static_fptr)() = &Test::staticFunc;

同样,通过typedef把它搞成类型用法和之前也一样,可以使代码更清晰。

类成员函数指针

与静态函数不同,成员函数在被调用时,必须要提供this指针。

因为在它被调用之前,自己也不知道哪个对象的此函数被调用。所以通过&拿到的不是实际的内存地址。

只有调用的时候,C++才会结合this指针通过固定的偏移量找到函数的真实地址调用。

为了支持这种调用方式,这里C++给专门提供了特殊的几个操作符:::* .* ->*

  • 声明:void (Test::*fptr)();,类成员函数指针的声明,就必须加上类名限定,这就声明了一个函数指针变量fptr,他只能指向Test类的成员函数。
  • 赋值:fptr = &Test::function
  • 调用:类的成员函数是无法直接调用的,必须要使用对象或者对象指针调用(这样函数才能通过对象获取到this指针)。
    • (t.*fptr)();,t是Test类的一个实例,通过对象调用。
    • (pt->*fptr)();,pt是一个指向Test类对象的指针,通过指针调用。

C++成员函数的调用需要至少3个要素:

  1. this指针;
  2. 函数参数(也许为空);
  3. 函数地址。

上面的调用中,->*.*运算符之前的对象指针提供了this(和真正使用this并不完全一致)

参数在括号内提供,fptr则提供了函数地址。

指向虚函数的函数指针

虚函数其实就是一种特殊的成员函数,所以指向虚函数的函数指针写法,同上。

不一样的是:「虚函数函数指针同样具有虚函数的特性——多态:基类的成员函数指针可以赋值给继承类的成员函数指针。」

另外,指向虚函数的函数指针在涉及到多继承和指针强转的问题时,使用不当会踩到大坑:

  1. 不要使用static_cast将继承类的成员函数指针赋值给基类成员函数指针,如果一定要使用,首先确定没有问题。(这条可能会限制代码的可扩展性。)
  2. 如果一定要使用static_cast, 注意不要使用多继承。
  3. 如果一定要使用多继承的话,不要把一个基类的成员函数指针赋值给另一个基类的函数指针。
  4. 单继承要么全部不使用虚函数,要么全部使用虚函数。不要使用非虚基类,却让子类包含虚函数。

这里我们只提一下结论,具体这些坑出现的原因,感兴趣的可以看看这篇比较深入的文章:https://blog.csdn.net/ym19860303/article/details/8586971

能否搞出指向构造函数和析构函数的函数指针?

我反正是没听说过有这么用的

我知道你想都没这么想过

但是总有SB面试会这么问你......

答案是不行,C++标准明确规定:The address of a constructor or destructor shall not be taken.

也可以随便写一个验证一下,编译报错也很明确:

语法总结

类函数指针的语法相当严格:

对于类内成员的函数指针的使用和获取,要注意的是:

  1. 不能使用括号:例如&(ClassName::foo)不对。
  2. 必须有限定符:例如&foo不对。即使在类ClassName的作用域内也不行。
  3. 必须使用取地址符号:例如直接写ClassName::foo不行。(虽然普通函数指针可以这样)

所以,必须要这样写:&ClassName::foo

对于类内成员函数指针的调用,还要注意:(t.*fptr)();(pt->*fptr)();必须要加括号

因为调用的优先级比.*->*高,不加括号就成了:t.*fptr();,这其实相当于:t.*(fptr());

把后面当成一个整体,然而fptr并不是一个函数,编译会直接失败。

::* .* ->*并不只是针对函数指针,如果在类外部声明指向类内成员「变量」的指针的话,也要用这几个操作符才行。

一个非常简单的实例

class Test
{

public :
    void function (){cout << "member function " << endl;}           // 类成员函数
    static void s_function(){cout << "static function " << endl;}   // 类静态成员函数
};

int main()
{
    Test t;             // 类对象
    Test *pt = &t;      // 对象指针
    t.function();       // 通过对象调用成员函数
    Test::s_function(); // 调用静态成员函数
    void (*s_fptr)() = &Test::s_function;           // 静态成员函数指针
    s_fptr();                                       // 通过 静态成员函数指针调用静态成员函数
    void (Test::*fptr)() = &Test::function;         // 成员函数指针
    (t.*fptr)();                                    // 经由对象的成员函数指针调用函数
    (pt->*fptr)();                                  // 经由对象指针的成员函数指针调用函数                    
    return 0;

应用场景

函数指针的应用在生产实践中其实是非常广泛的。

网上很多关于函数指针的应用场景的讲解都会自己设计个场景讲解一小段。

我这里就不班门弄斧了,给大家找几个我工作中遇见过的开源项目,看看他们的函数指针是怎么用的:

应用场景一、转移表:

玩过linux的同学一定都用敲很多命令,有些命令行工具特别强大,比如像什么sedawk等等。

这些工具无一例都可以对复杂的命令行参数进行精准解析。

如果你自己写过命令行解析的程序就会发现这并不是一件容易的事情。

我在研究多线程打包的时候有看过dpkg的源码。这里可以简单讲一下:(代码来源:https://git.dpkg.org/git/dpkg/dpkg.git)

dpkgLinux Debian系系统自带的包管理工具,管理整个系统的安装包安装卸载,常见的用法有:

  • dpkg -i 包名 或 dpkg --install 包名安装
  • dpkg -l 列出所有包详细信息
  • dpkg -l 包名 列出指定包详细信息
  • dpkg --purge 软件名 或者dpkg -P 软件名 卸载软件
  • 复杂一点的组合用法:dpkg -D2 --ignore-depends=libgtk --force -i 包名 等等。

像这种命令工具的逻辑如果让我写,指定满屏幕的if else把自己也绕晕。

但是在dpkg的源码里,就用了一种比较高端的玩法

(其实大多数命令行工具在解析命令参数的时候都有用这种办法,这里我为了好懂一点有所改动,源码比这个还要晦涩很多,纯C的项目属实有点难啃):

struct cmdinfo {                // 命令结构体,每一种命令对应一个实例,存放命令本身的字符串以及执行的函数指针等
  const char *olong;
  char oshort;

  /*
   * 0 = Normal    (-o, --option)
   * 1 = Standard value   (-o=value, --option=value or
   *      -o value, --option value)
   * 2 = Option string continued (--option-value)
   */

  int takesvalue;
  int *iassignto;
  const char **sassignto;
  void (*call)(const struct cmdinfo*, const char *value);

  int arg_int;
  void *arg_ptr;

  action_func *action;
};
// ........
//两个宏,就是简化一下写法而已。
#define ACTION(longopt, shortopt, code, func) \
 { longopt, shortopt, 0, NULL, NULL, setaction, code, NULL, func }

#define ACTIONBACKEND(longopt, shortopt, backend) \
 { longopt, shortopt, 0, NULL, NULL, setaction, 0, (void *)backend, execbackend }


// 指令的结构体数组,dpkg所有支持的参数都收录在这里。
static const struct cmdinfo cmdinfos[]= {
#define ACTIONBACKEND(longopt, shortopt, backend) \
 { longopt, shortopt, 0, NULL, NULL, setaction, 0, (void *)backend, execbackend }


  ACTION( "install",                        'i', act_install,              archivefiles    ),
  // ......
  ACTION( "remove",                         'r', act_remove,               packages        ),
  ACTION( "purge",                          'P', act_purge,                packages        ),
  ACTIONBACKEND( "list",                    'l'"dpkg-query"),
  // ......
  { "ignore-depends",    0,   1NULL,          NULL,      set_ignore_depends, 0 },
  // .......
  { "debug",             'D'1NULL,          NULL,      set_debug,     0 },
  { "help",              '?'0NULL,          NULL,      usage,         0 },
  { "version",           0,   0NULL,          NULL,      printversion,  0 },
  // .......
  { NULL,                0,   0NULL,          NULL,      NULL,          0 }
};

乍一看有点眼晕,没事,一步一步来:

ACTIONACTIONBACKEND都是宏,最后他们都变成了一个cmdinfo结构体的定义。所以可以看做和它下面的一样。

这段程序为了能实现不同的参数对应不同的处理,用了一个结构体数组

每一个结构体里面,存了固定的命令行参数和他对应的处理函数的「函数指针」。比如说这行:

ACTION( "install",                        'i', act_install,              archivefiles    ),

这个ACTION是个宏定义,它替换后的样子就是:

"install"'i'0NULLNULL, setaction, act_install,NULL, archivefiles },

其他不用管,你只需要知道程序会自动解析这个结构体

第一个install代表如果匹配到--install的写法,第二个i表示匹配到-i的写法。所以命令里-i--install是一样的操作

最后一个参数archivefiles就是如果匹配到前面的参数,要执行的函数(这是个「函数指针」,所以可以直接传递函数名进去)。

至于解析的具体的实现,其实你都不用太关注细节,你只需要知道这么写能实现功能就可以。

dpkg在执行的时候,main函数把接收到的所有参数都交给解析函数处理

解析函数就会拿出每一组参数,并且遍历这个结构体数组去比对

如果匹配到了。直接调用对应的函数指针。

最后的效果就是,当程序检测到你传递了-i或者--install参数时,就调用archivefiles执行相应的功能

那么现在如果让你给dpkg命令行添加一个参数的支持,比如说打印一句hello world你怎么做?

你只需要写一个名为hello的函数,然后把参数和函数名添加在这个结构体数组里就可以

解析是全自动而且可灵活扩展的,你根本不需要知道太多细节,也不需要做任何多余的改动:

int hello_world(const char * const *argv) // 函数签名要和定义好的函数指针保持一致
{
  printf("hello world!\n");
  exit(0);          // 因为只打印信息,阻止dpkg的后续代码执行,这里直接退出
}
// ...... 
static const struct cmdinfo cmdinfos[]= {
  // .......
  { "hello",            'H',  0NULL,          NULL,      hello_world, 0 }, // 新添加的一行,位置只要在结尾行上面就行
  // .......
  { NULL,                0,   0NULL,          NULL,      NULL,          0 }
};

运行结果:

在这里函数指针就为这种灵活的调用方式提供了强有力的支持!

这个功能实现的核心,就是在结构体里存放了一个函数指针变量。

在代码执行的时候,通过匹配到不同的参数,就找不同的函数调用来执行不同的功能。

相比于写if else switch case,这种写法不仅高端而且灵活高效,扩展性又非常好,而且还很简洁易读(对于有一定基础的同学而言)

很多网上的资料对于转移表的讲解,都是一个单纯的函数指针数组,这里是一个相对复杂点的“包含函数指针的结构体数组”,我也把他归为转移表里面了。
我个人认为这么归类是合理的,但是因为没找到官方有“转移表”的说法和明确定义,不知道这里这么归类是否合适。关于这一点欢迎感兴趣的小伙伴调研补充。

应用场景二、回调函数

二.1 函数指针回调

linux系统编程中,可以使用signal函数让程序具备处理内置系统信号的能力。

比如像这样一个程序(linux上玩,windows编不过哦):

#include <iostream>
#include "signal.h"
using namespace std;

void ctrl_c_is_pressed(int signo)
{
 cout << "小朋友,你是否有很多问号?" << endl;
}

int main()
{
 signal(SIGINT,ctrl_c_is_pressed);
 while(true);
    return 0;

它执行起来效果会非常诡异,你会发现万能的Ctrl+C停不掉它:

这就是一个经典的回调函数的应用,我们通过signal函数给信号SIGINT(也就是Ctrl+C被按下时,系统实际发送的信号)注册了一个处理函数ctrl_c_is_pressed

每当程序收到SIGINT信号时,它就会执行我们注册的这个函数。(如果我们没有注册,他会执行系统内置的默认行为,也就是中断程序)

我这里说的回调函数,就是通过函数指针来实现的,你可以看到我在注册的时候直接传了函数名称进去,并把它和SIGINT信号绑定到了一起。

然后每当程序收到SIGINT信号的时候,他就会调用我们注册好的函数。(回调回调,就是这个意思)

其实在Linux系统源码中,signal的函数原型是这样的(Ubuntu 16.04,不同系统会有差异):

/* Set the handler for the signal SIG to HANDLER, returning the old
   handler, or SIG_ERR on error.
   By default `signal' has the BSD semantic.  */

__BEGIN_NAMESPACE_STD
#ifdef __USE_MISC
extern __sighandler_t signal (int __sig, __sighandler_t __handler)
     __THROW;
#else

抛去你不认识的部分,只看函数声明:__sighandler_t signal (int __sig, __sighandler_t __handler);这个__sighandler_t你再往下挖就会惊喜的发现:

/* Type of a signal handler.  */
typedef void (*__sighandler_t) (int);

这下认识了吧,signal就是一个返回函数指针的函数,他还包含两个参数,一个是int,另一个是函数指针。

这个函数指针可以指向一个参数为int,返回为空的函数,所以我们上面写的ctrl_c_is_pressed可以直接传进去

在很多文章里或者有些旧版的代码里写的都是这样的:

void (*signal(int signo, void (*func)(int)))(int);

其实就是上面,没有typedef的版本。

二.2 类成员函数指针回调

上面这个是函数指针回调,下面看一个类成员函数指针的回调。

相信不少小伙伴在大学的时候多多少少玩过cocos2dunity3d之类的做过小游戏。

这里简单拉出cocos2d-x的按键回调的代码看看它是怎么应用函数指针的:

使用cocos2d做游戏,如果你想在游戏屏幕上加一个按钮,你需要这么写:

CCMenuItemImage *pCloseItem = CCMenuItemImage::create(
                                    "CloseNormal.png",                              // 正常状态显示的图片
                                    "CloseSelected.png",                            // 被按下时显示的图片
                                    this,                                           // 回调的执行者
                                    menu_selector(HelloWorld::menuCloseCallback));  // 回调执行的操作。

这里最重要的是后面两个参数,分别是回调的执行者和执行的函数名。

你可以从功能上来理解:我们点击一个按钮,就要触发某个功能,比如开始游戏,关闭游戏等等。

这个功能的触发需要两个要素:「【谁】【做什么事情】」

所以这里每一个按钮生成的时候,都需要指定两个必要的参数,一个是“谁”,另一个就是“做什么”。

只要你指定过这两个参数,代码底层会自动处理,在按钮被点击的时候,就让“谁”执行“指定操作”。

比如我们上面的代码,就是让“当前窗体”执行“关闭操作”。

和上面的signal注册回调本质上是一样的,不同的是,这里的回调是跨类回调,你需要在CCMenuItemImage这个类里,调用其他类里面的某个函数

上面我们也讲了,非静态的成员函数在指针调用,必须要传递this指针。所以这种回调机制至少要传两个参数,一个是函数地址,一个是this指针。

这种跨类回调也是函数指针的一个经典应用,而且在编程实践中的应用可以说非常广泛。

这里只简单说明一下这种跨类回调的场景下,用到了函数指针。至于他底层的实现的机制,详解的话足够单拉一篇文章了,这里先留个坑,后期写好补上。

上面看到的是cocos2d-x 2.X版本的写法,这也是官网上可以下载到的第二代中最新的2.2.6的版本。官方早就已经不再维护,不过用作代码的研读和学习非常有用。

如果你能看懂我上面的讲解就会明白,cocos2d-x 这个版本的代码可读性非常好,我感觉非常适合我这种稍微有点基础的初学者学习。

到了3.x版本里(我下的3.17.2),这种跨类的回调机制玩法也早已换成了风骚万倍的C++11的玩法:

auto closeItem = MenuItemImage::create(
                        "CloseNormal.png",
                        "CloseSelected.png",
                        CC_CALLBACK_1(HelloWorld::menuCloseCallback,this));

感觉写法上差别好像不太大,其实底层的实现完全换了一种机制。上面2.X版本,使用的跨类函数指针进行回调。下面这种CC_CALLBACK_1写法,底层已经是C++11的bind+std::function

应用场景三、反射

上面这段cocos2d创建按钮的代码,如果有同学用过cocos2d-java的话就会知道,在java里等价的写法应该是这样的:

CCMenuItemImage closeMenu = CCMenuItemImage.item(
                                    "CloseNormal.png"
                                    "CloseSelected.png"
                                    this
                                    "close");

注意这个地方最后一个参数,在C++中它要传一个函数指针,不过到java里,它传一个函数名的字符串就可以了,这个close就是函数名。

这里就是用了java的反射机制,可以直接把字符串映射成真正的函数地址并实现调用。

在C++当中,语言本身并不提供反射机制。但是仍然可以通过函数指针实现,在很多C++实现的中间件中都有反射的实现,我平时了解到的,使用C++实现的最完善的动态反射机制当属Qt的QMetaObject::invokeMethod();

反射最大的好处,就是让你的代码一般人轻易看不懂,IDE里Ctrl+鼠标左键跳转不过去。
维护难度一上来,你的价值就体现出来了,等待你的将是升职加薪,迎娶白富培走向人生....扯远了。

反射最大的好处,是让你的代码灵活度和可扩展性大大提升。不过相对的,可维护性也有一定的损失。

有了反射之后,你完全可以通过QMetaObject::invokeMethod("function_name");来进行函数调用。

之所以说这么做灵活,是因为字符串足够灵活。

比如你写了十个函数,名字分别是function_1function_2function_3function_4.....

为了实现分别调用,没有反射你就需要写十次调用或者用转移表

有了反射,你可以用字符串拼接的方式"function_"+i 拼出函数名,然后invokeMethod来调用。

和上面的cocos2d一样,这里就先了解一下反射这个函数指针的应用场景就好,就不深入讲实现原理了。

(实在是因为Qt这个invokeMethod的实现机制啃了一次不得要领,就不敢深入瞎讲了。)

最后

以上就是本篇关于C++函数指针讲解的全部内容,一篇典型收藏吃灰系列的文章

就是简单捋了一下函数指针的写法、功能以及应用

没什么深度,所以应该也没什么严重的误导和错误

上面提到了在cocos2d-x的新版本中用std::function代替了函数指针,这也是现在C++框架和应用的主流写法

C++11提供的std::function将从语法层面为函数指针的使用提供强大的支持,并且代码的可读性也明显提升。

计划将在近期再写一篇文章对std::function进行一个简单的梳理,会和本篇一样没什么难度深度,欢迎关注。

最后额外补充一个彩蛋:如果你需要一个声明函数指针指向某个函数,但这个函数实在太过复杂以至于它的函数指针声明你不会写

那你可以直接:auto f = functionname(仅限C++11以上)

参考链接:

  • https://blog.csdn.net/qq_42128241/article/details/81610124
  • https://www.cnblogs.com/yangyuliufeng/p/10720417.html
  • https://www.cnblogs.com/hellscream-yi/p/7943848.html
  • https://blog.csdn.net/tangyangyu123/article/details/89978915
  • https://blog.csdn.net/zhuxiufenghust/article/details/6543652?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2
  • https://www.cnblogs.com/yangjiquan/p/11465376.html
  • https://www.xuebuyuan.com/1238896.html
  • https://blog.csdn.net/shenhuxi_yu/article/details/75948887
  • https://blog.csdn.net/qq_28773183/article/details/78262444
  • https://isocpp.org/wiki/faq/pointers-to-members
  • https://stackoverflow.com/questions/2402579/function-pointer-to-member-function
  • https://www.codeguru.com/cpp/cpp/article.php/c17401/C-Tutorial-PointertoMember-Function.htm
  • http://www.bubuko.com/infodetail-996525.html

参考书目:

  • C Primer Plus
  • C++ Primer


每日打卡赢积分兑换书籍入口

👇🏻👇🏻👇🏻


●  人人都欠微软一个正版?

●  积分兑换,来就“兑”了

●  收藏|Java 面试题全梳理

● 漫画:Java如何实现热更新?

● 2020年开发者生态报告:Python超越Java

 小白,你要的Java抽象类,操碎了心!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存