上一节我们讲了函数,这次来聊聊函数指针

顾名思义,函数指针指的就是,指向函数的指针。也就是,指针解引用之后是一个可调用的函数。

先来看一段示例代码:

1
2
3
4
5
6
7
8
int addInt(int a, int b) {
return a + b;
}
int main() {
int (*pAddInt)(int, int);
pAddInt = addInt;
cout << pAddInt(3, 4) << endl; // 7
}

现在我们来仔细看看这段代码做了些什么。

声明函数指针

首先我们声明并定义了一个函数 addInt,它接受两个 int 类型的参数,然后返回它们的和。

然后我们来看今天的重点,main 函数。

1
int (*pAddInt)(int, int);

这就是我们所说的,函数指针的声明

我们来看看为什么它是函数指针:

  • pAddInt 是这个变量的名称,我们从这里往外读
  • 首先是括号,里面有一个 *,说明它是个指针
  • 右侧的括号中有两个 int 类型,说明这是个参数列表
  • 左侧的 int 表示一个返回值类型
  • 因此,它是一个函数指针,可以指向一个函数,这个函数需要接受 2 个 int 类型的参数,并返回一个 int

正如我们在介绍指针时所提到的,指针所指向的内容是有类型限定的,由指针的类型决定。函数指针也是如此,你看,上面的代码已经明确了允许指向的函数的要求了。

其实,函数指针就是函数加个星号而已,声明和正常的指针没什么差异:

1
2
int addInt(int, int);
int (*pAddInt)(int, int);

再提醒你一下,在 C++ 中,括号的优先级是比 * 更高的,因此,声明函数指针的时候,必须给星号加上括号。比如,下面的声明是错误的,会声明一个返回值指向 int 类型的指针函数,而非函数指针

1
int *pAddInt(int, int);

是不是看到了指向数组指针的影子?是的,由于括号问题,它们呈现出相似的视觉效果。

好了,现在我们声明了一个函数指针,但问题是,这个指针现在没有指向任何东西,行为是未定义的。

赋值函数指针

我们来看下一行:

1
pAddInt = addInt;

这一行代码将函数指针 pAddInt 指向了一个实际的函数 addInt

但是你或许注意到了一个问题——指针指向的是一个地址,但是为什么这里没有取地址呢?

首先我们明确一点,函数确实有一个地址,指针也正是指向了这个内存地址。指针本身存储了地址,是一个对象,但是函数并不是对象(也不能赋值给变量)。

然后我们再来看为什么没有取地址符号 & 这个问题。

其实你自己先试一下就会发现,加上这个符号也能通过编译,并正常运行:

1
pAddInt = &addInt;

这种灵活来自于 C++ 的退化性质,当一个表达式中存在函数名字的时候,这个函数会自动转换为函数指针

是不是很耳熟?没错,这和数组是一个道理!还记得吗,我们在数组与指针这一节中,曾经提到过,数组在表达式中也会自动转换为指针,指向的是第一个元素。

函数也是类似,函数名字在表达式中自动转换为指向该函数的函数指针。因此,取地址符号是可以省略的

调用函数指针

既然我们已经有了一个函数指针,是时候来看看怎么用了。

1
cout << pAddInt(3, 4) << endl;

看到了吗?我们正在调用函数指针,正如调用普通的函数一样。

我知道你要说什么。你肯定想这么干:

1
cout << (*pAddInt)(3, 4) << endl;

你自己试试就会发现无论是否解引用,表现都是一致的。

这是因为,编译器为你承担起了这一切——和自动退化相对称,解引用也并不是必须的,我愿称之为一组对称法则(提示,这不是官方说法,是我个人的总结)。

实际上,函数指针可以作为返回值和参数,就如通常的指针一样。但是,在继续讨论函数指针前,我们先来讲解几个好用的东西,然后再继续深入。

decltype

当我们涉及指针的时候,是不是发现声明语句越来越复杂了呢?一个变量的类型可能变得相当复杂:

1
2
3
4
int* addInt(int *a, int *b) {
*a = *a + *b;
return a;
}

要创建指向上面函数的指针,我们必须使用这种类型声明语句:

1
int* (*pAddInt)(int*, int*);

有没有什么简单的方法呢?当然,让我们隆重介绍 decltype

看看这个:

1
decltype(addInt) *pAddInt;

decltype 可以把一个名字对应的类型,直接偷过来!我们这里偷来了 addInt 这个函数的类型(包括参数列表类型和返回值)。通常情况下,这样会声明一个新的函数,但由于我们加上了一个 *,所以我们现在声明的是函数指针。

一个非常重要的事情是,decltype 不发生退化,所以记得带上 *。decltype 只是负责推算里面表达式的类型而已,不会帮你转换。

因此,使用 decltype 可以极大简化代码编写流程,让我们免去书写指针的类型的麻烦。

注意这个当然不局限于函数指针,而是对于任何指针都是有效的!

比如,指向数组的指针:

1
2
3
int a[10];
decltype(a) *p;
int (*p2)[10];

第二、三行声明的指针,类型都是一样的,都是指向含有 10 个 int 的数组的指针哦。

类型别名

明白了 decltype 能够偷来类型,我们再来讲一个能够把类型起个别名的东西,类型别名

类型别名,从名字上来看,就是用另外一个名字,替代原来的类型名字。

typedef

传统的类型别名使用方法是 typedef,比如:

1
2
3
typedef double d;
d num = 3.14;
cout << num << endl;

我们给 double 起了个别名叫做 d。

但这个太简单了,当类型变得非常复杂的时候,别名才会发挥出作用:

1
2
3
4
typedef double** d; // 指向指针的指针类型
typedef int arrType[10]; // 包含10个int的数组类型
typedef int (*arrType)[10]; // 一个指针类型,指向的是包含10个int的数组类型
typedef int *arrType[10]; // 包含10个指向int的指针的数组类型

你现在可能非常疑惑,为什么第一条和后面三条的语法看起来完全不一样(你看看,第一句的语法好像是 typedef 类型 新的名字,但后面的语法完全不是这样)?换句话说,typedef 到底是如何工作的?

别被迷惑了,让我们先把 typedef 本身移除掉——

1
2
3
4
double** d; // 指向指针的指针类型
int arrType[10]; // 包含10个int的数组类型
int (*arrType)[10]; // 一个指针类型,指向的是包含10个int的数组类型
int *arrType[10]; // 包含10个指向int的指针的数组类型

现在你可以看出些端倪了:给类型取别名,和创建变量的本质没什么区别。

奶奶都知道可以这么创建变量:

1
2
3
4
double** dp;
int arr[10];
int (*arr)[10];
int *arr[10];

嗯?是不是发现了什么呢?语法完全一致

也就是说,typedef 和创建变量的区别只有一个,就是前者创建的名字是一个可以直接使用的类型

比如:

1
2
d myd;
arrType myArr; // 直接当作类型使用!

好了,既然你已经知道如何创建类型别名,那么我们回到函数指针。同理,你可以这么创建函数指针类型:

1
2
typedef int (*funcPType)(int*, int*);
funcPType p1; // 这是一个函数指针

这样可以节省大量时间。

using

在新版的 C++ 中,你可以用另一种方式创建类型别名:

1
2
3
using d = double;
d d1 = 3.14;
cout << d1 << endl; // 3.14

这个 using 会把等号后面的类型,起别名,别名名字为等号前的内容。我们可以把上面的 typedef 全部转换为 using:

1
2
3
4
using d = double** ; // 指向指针的指针类型
using arrType = int[10]; // 包含10个int的数组类型
using arrType = int (*)[10]; // 一个指针类型,指向的是包含10个int的数组类型
using arrType = int*[10]; // 包含10个指向int的指针的数组类型

很简单吧?只是把名字前置了,相比 typedef,这种方式看起来更加方便。

当然,也别忘了我们的主角函数指针:

1
using funcPType = int (*)(int*, int*);

更强大的是,你可以把类型别名和 decltype 组合使用,避免写出复杂的类型:

1
2
using funcType = decltype(func); // 现在,你给函数起了别名
funcType funcP = *funcType; // 根据函数别名,创建对应的函数指针

传递函数指针

既然函数指针本身,是个指针,那么它就变成了一个对象,自然可以传递,就如其它指针一样。

作为参数传递

那这有什么用呢?其实,这样可以让同一个函数根据传入的函数指针,执行不同的操作。

比如,下面的代码,将运算的函数指针传入,然后调用函数执行自定义操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int sum(int a, int b) {
return a + b;
}
int diff(int a, int b) {
return abs(a - b);
}
using funcPType = decltype(sum)*;
int mathOperation(int a, int b, funcPType funcP) {
return funcP(a, b);
}
int main() {
int a = 3, b = 4;
cout << mathOperation(a, b, sum) << endl; // 7
cout << mathOperation(a, b, diff) << endl; // 1
}

花点时间好好理解一下上面的代码。

首先,写了两个执行算术操作的函数。

然后,我们创建了一个叫做 funcPType 的类型别名,它代表的类型是一个函数指针,指向接受 2 个 int 参数的返回 int 的函数。

之后,写了一个算术操作的函数,接受函数指针类型,在内部调用这个函数指针,执行相对应的操作。

最后写了 main 函数,两次传入不同的函数名字。函数名自动退化为函数指针,作为参数传入。你可以看到,我们的 a b 都没有变化,但是结果却不同。这正是因为传入了不同的函数指针导致的。

也就是说,函数指针可以把不同操作打包,交给其它部分操作。也就是,把行为作为数据传递。函数指针只记下了传入什么、返回什么,不关心实际执行了什么操作——也正因为如此,我们可以在需要函数指针的地方传入不同的函数,这极大地提升了我们程序的灵活性。

另外我要提醒你一点,不要给函数名字后面加上括号。这样会变成直接调用函数,传递的就是返回值了,会发生类型不匹配直接报错。我们要传递的,是会自动退化成函数指针的函数名字

作为返回值

不止参数。函数指针也可以作为返回值

来看看下面的示例代码(略去和上述相同的 sum 和 diff 函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using funcPType = decltype(sum)*;
funcPType chooseOperation(char c) {
if(c == '+') return sum;
if(c == '-') return diff;
return nullptr;
}
int main() {
char c1 = '+';
char c2 = '-';
funcPType choice = chooseOperation(c1);
cout << choice(1, 3) << endl; // 4
choice = chooseOperation(c2);
cout << choice(1, 3) << endl; // 2
}

仔细看看,我们的函数根据传入的参数不同,返回了不同的函数指针。返回的指针可以作为函数调用——在两条输出语句那里,我们用的是同一个函数指针名称,但是它们指向的是不同的函数,也就导致了不同的执行结果。

这是一个非常现实的例子,同样也是把行为作为数据传递的体现。掌握了这种写法,你就可以大大提升你的程序的灵活度。

此外,我们也可以不用类型别名,但是这会出现一些非常复杂的东西,我们在下面的尾置返回类型中,进行进一步的探讨。

尾置返回类型

关于函数指针我们说的够多了,现在我们再来补充一点 C++ 新版本的知识,尾置返回类型

顾名思义,尾置返回类型允许我们把函数返回值的类型放在末尾。来看看这个:

1
2
3
4
5
auto chooseOperation(char c) -> funcPType {
if(c == '+') return sum;
if(c == '-') return diff;
return nullptr;
}

这是上面的代码的另一种写法。原本用于声明函数返回类型的首位被 auto 代替了,真正的返回类型写在最后。

你可能在想,这不是多此一举吗?其实,只有当你不想用类型别名,但是却想返回函数指针(或任何复杂的类型,比如指向数组的指针)的时候,你会发现这大大被简化了:

1
2
3
4
5
6
7
int (*chooseOperation(char c))(int, int) // 一般情况
auto chooseOperation(char c) -> int(*)(int, int) // 尾置返回类型,这下看懂了
{
if(c == '+') return sum;
if(c == '-') return diff;
return nullptr;
}

也补充一下,如果不用尾置返回类型,第一行

1
int (*chooseOperation(char c))(int, int)

的含义是:

  • 从内向外阅读。首先 chooseOperation(char c) 说明此处是个函数。
  • 其次 (*...) 表示它的返回值是个指针。
  • 再次,int(...)(int,int) 表示这个指针指向的是个函数,函数接受 2 个 int,返回一个 int。

如果你有些混乱,不妨翻到文章开头,看看函数指针是如何声明的,对比看看:

1
2
int (*pAddInt)(int, int);
int (*chooseOperation(char c))(int, int)

现在是不是发现了点规律?只是把变量名字换成函数名字+参数列表而已。

但是,我完全不建议你这么写。请一定要使用尾置返回类型、类型别名和 decltype,让你的代码更加可读。

关于函数指针和简化代码的方法,我们已经涉及了相当深入的内容了。下一节,我们将离开函数这一篇章,进入新的一部分——