0. 什么是函数指针

指针是C++中的一种复合类型,是“指向(pointer to)”另外一种类型的复合类型,实现了对其他对象的间接访问。

函数指针指向的不是对象,而是函数。它基于函数类型的定义。

回想函数的三要素:返回类型、函数名、形参类型,函数类型指的就是由返回类型和形参类型共同决定的。

bool shorter(const string &, const string &);
// 函数的类型是
// bool(const string &, const string &)
// 是这样的一类函数:
// 返回值是bool类型,需要两个const string&类型的函数

此时,可以将函数名位置用指针替换,就声明了指向上述函数类型的指针:

bool (*pf)(const string &, const string &);
// 未初始化

解析上面的声明,依旧是按照从内向外、先右后左的口诀。

很多读者看到函数指针的声明和定义方法就望而却步了,但其实记住从内向外、先右后左以及学会类型处理,理解函数指针很简单。

关于函数指针,我们需要了解:

  1. 函数指针的声明&定义
  2. 函数指针的使用
  3. 类型处理(typedef、using、auto、decltype)

1. 函数指针的声明&定义

// 声明了一个函数
bool shorter(const string &, const string &);

// 声明并初始化了一个函数指针
bool (*pf)(const string &, const string &) = shorter;

// 可以省略取地址运算符&,也可以使用
// bool (*pf)(const string &, const string &) = &shorter;

注意:(*pf)的括号必不可少。

上述代码在初始化pf时,并没有在等号=右侧使用&,原因是①把函数名作为一个值使用时,函数自动转换成指针(除了decltype)。

2. 函数指针的使用

2.1 函数指针的调用

定义了一个有明确指向的函数指针后,可直接通过调用运算符()调用该函数:

pf("hello", "goodbye");

可以不解引用指针,也可以解引用,但是必须加括号:

(*pf)("hello", "goodbye");

其实,带有明确指向的函数指针属于可调用对象(callable object)

2.2 函数指针作为形参

我们可以声明一个具有函数指针形参的函数:

void callCompare(bool pf(const string &, const string &), int count);
// 第一个参数是函数类型为
// bool(const string &, const string &)
// 的函数指针

注意:此处虽然看起来是函数类型的声明,但实际上②函数类型作为形参会自动的转换为相应类型的函数指针。当然写成指针类型也是可以的。

void callCompare(bool (*pf)(const string &, const string &), int count);
// 和上面等价,显式添加了*

在使用callCompare时:

callCompare(shorter, 1);

这里也没有用取地址运算符&,和1.1叙述的原因一样。

2.3 函数指针作为返回值

假如我想定义一个返回与shorter同类型函数指针的函数,该如何声明呢?

从里向外,先右后左。

首先确定我想定义的函数的三要素:

  1. 函数名,假设retFunc
  2. 参数列表,假设接受一个int
  3. 返回值,一个与shorter同类型函数指针bool (*)(const string &, const string &)

自然而然想到可以这么写:

// ERROR,这是错误的
bool (*)(const string &, const string &) retFunc(int);

这么写一定是不行的,因为左边一大坨不符合编译器编译的原理,从retFunc向左看到了参数列表,完全不知道是什么。

因此必须把指针*放在retFunc的左边。

bool (*retFunc(int))(const string &, const string &);

①从标识符开始,向右看到了参数列表,因此retFunc是一个函数。

②右括号,不能向右了,向左,看到了指针*,因此返回的是一个指针。

③看到左括号,向外,继续向右,看到参数列表,因此返回的是函数指针。

④再向左,看到了bool,可以确定了返回的函数指针的类型。

3. 类型处理

若每次都像上面一样使用这么复杂的类型定义,程序一定很乱。

这时,typedef、using、auto、decltype就派上了用场。

我们可以用typedefusing给函数类型声明一个简单的名字;在明确知道使用的是哪个函数时,可以使用autodecltype

例如:

// 声明了一个函数类型
using FuncType = int(int &, int);
// typedef int FuncType(int &, int);
// using PFuncType = int (*)(int &, int);
typedef int (*PFuncType)(int &, int);

// 下面的函数就是上面的类型
int add_to(int &des, int ori) {
    return des += ori;
}
int minus_to(int &des, int ori) {
    return des -= ori;
}
int multiply_to(int &des, int ori) {
    return des *= ori;
}
int divide_to(int &res, int ori) {
    return res /= ori;
}

int main() {
    FuncType *pf0 = add_to;
    auto *pf1 = minus_to;
    decltype(multiply_to) *pf2 = multiply_to;
    PFuncType pf3 = divide_to;

    int a = 4;

    // 通过函数指针调用
    pf0(a, 2);  // 6
    pf1(a, 3);  // 3
    pf2(a, 4);  // 12
    pf3(a, 3);  // 4
}

这样的声明就简单多了。

注意:decltype和auto不会自动的获得函数指针类型,都需要添加*

4. 总结

函数类型是根据函数定义的返回值和形参列表抽象出来的一类函数。

可以使用函数类型定义指向同种类型的函数指针

使用函数指针可以间接调用函数,且可以实现函数指针作为形参、函数指针作为返回值等操作。

在使用函数指针时,使用类型别名(typedef、using)和类型推断(auto、decltype),可以使代码更清晰。