奶奶都能看懂的 C++ —— 函数指针、decltype、类型别名和尾置返回
上一节我们讲了函数,这次来聊聊函数指针。
顾名思义,函数指针指的就是,指向函数的指针。也就是,指针解引用之后是一个可调用的函数。
先来看一段示例代码:
1 | |
现在我们来仔细看看这段代码做了些什么。
声明函数指针
首先我们声明并定义了一个函数 addInt,它接受两个 int 类型的参数,然后返回它们的和。
然后我们来看今天的重点,main 函数。
1 | |
这就是我们所说的,函数指针的声明。
我们来看看为什么它是函数指针:
pAddInt是这个变量的名称,我们从这里往外读- 首先是括号,里面有一个
*,说明它是个指针 - 右侧的括号中有两个
int类型,说明这是个参数列表 - 左侧的 int 表示一个返回值类型
- 因此,它是一个函数指针,可以指向一个函数,这个函数需要接受 2 个 int 类型的参数,并返回一个 int
正如我们在介绍指针时所提到的,指针所指向的内容是有类型限定的,由指针的类型决定。函数指针也是如此,你看,上面的代码已经明确了允许指向的函数的要求了。
其实,函数指针就是函数加个星号而已,声明和正常的指针没什么差异:
1 | |
再提醒你一下,在 C++ 中,括号的优先级是比 * 更高的,因此,声明函数指针的时候,必须给星号加上括号。比如,下面的声明是错误的,会声明一个返回值为指向 int 类型的指针的函数,而非函数指针:
1 | |
是不是看到了指向数组指针的影子?是的,由于括号问题,它们呈现出相似的视觉效果。
好了,现在我们声明了一个函数指针,但问题是,这个指针现在没有指向任何东西,行为是未定义的。
赋值函数指针
我们来看下一行:
1 | |
这一行代码将函数指针 pAddInt 指向了一个实际的函数 addInt。
但是你或许注意到了一个问题——指针指向的是一个地址,但是为什么这里没有取地址呢?
首先我们明确一点,函数确实有一个地址,指针也正是指向了这个内存地址。指针本身存储了地址,是一个对象,但是函数并不是对象(也不能赋值给变量)。
然后我们再来看为什么没有取地址符号 & 这个问题。
其实你自己先试一下就会发现,加上这个符号也能通过编译,并正常运行:
1 | |
这种灵活来自于 C++ 的退化性质,当一个表达式中存在函数名字的时候,这个函数会自动转换为函数指针。
是不是很耳熟?没错,这和数组是一个道理!还记得吗,我们在数组与指针这一节中,曾经提到过,数组在表达式中也会自动转换为指针,指向的是第一个元素。
函数也是类似,函数名字在表达式中自动转换为指向该函数的函数指针。因此,取地址符号是可以省略的。
调用函数指针
既然我们已经有了一个函数指针,是时候来看看怎么用了。
1 | |
看到了吗?我们正在调用函数指针,正如调用普通的函数一样。
我知道你要说什么。你肯定想这么干:
1 | |
你自己试试就会发现无论是否解引用,表现都是一致的。
这是因为,编译器为你承担起了这一切——和自动退化相对称,解引用也并不是必须的,我愿称之为一组对称法则(提示,这不是官方说法,是我个人的总结)。
实际上,函数指针可以作为返回值和参数,就如通常的指针一样。但是,在继续讨论函数指针前,我们先来讲解几个好用的东西,然后再继续深入。
decltype
当我们涉及指针的时候,是不是发现声明语句越来越复杂了呢?一个变量的类型可能变得相当复杂:
1 | |
要创建指向上面函数的指针,我们必须使用这种类型声明语句:
1 | |
有没有什么简单的方法呢?当然,让我们隆重介绍 decltype!
看看这个:
1 | |
decltype 可以把一个名字对应的类型,直接偷过来!我们这里偷来了 addInt 这个函数的类型(包括参数列表类型和返回值)。通常情况下,这样会声明一个新的函数,但由于我们加上了一个 *,所以我们现在声明的是函数指针。
一个非常重要的事情是,decltype 不发生退化,所以记得带上 *。decltype 只是负责推算里面表达式的类型而已,不会帮你转换。
因此,使用 decltype 可以极大简化代码编写流程,让我们免去书写指针的类型的麻烦。
注意这个当然不局限于函数指针,而是对于任何指针都是有效的!
比如,指向数组的指针:
1 | |
第二、三行声明的指针,类型都是一样的,都是指向含有 10 个 int 的数组的指针哦。
类型别名
明白了 decltype 能够偷来类型,我们再来讲一个能够把类型起个别名的东西,类型别名。
类型别名,从名字上来看,就是用另外一个名字,替代原来的类型名字。
typedef
传统的类型别名使用方法是 typedef,比如:
1 | |
我们给 double 起了个别名叫做 d。
但这个太简单了,当类型变得非常复杂的时候,别名才会发挥出作用:
1 | |
你现在可能非常疑惑,为什么第一条和后面三条的语法看起来完全不一样(你看看,第一句的语法好像是 typedef 类型 新的名字,但后面的语法完全不是这样)?换句话说,typedef 到底是如何工作的?
别被迷惑了,让我们先把 typedef 本身移除掉——
1 | |
现在你可以看出些端倪了:给类型取别名,和创建变量的本质没什么区别。
奶奶都知道可以这么创建变量:
1 | |
嗯?是不是发现了什么呢?语法完全一致。
也就是说,typedef 和创建变量的区别只有一个,就是前者创建的名字是一个可以直接使用的类型。
比如:
1 | |
好了,既然你已经知道如何创建类型别名,那么我们回到函数指针。同理,你可以这么创建函数指针类型:
1 | |
这样可以节省大量时间。
using
在新版的 C++ 中,你可以用另一种方式创建类型别名:
1 | |
这个 using 会把等号后面的类型,起别名,别名名字为等号前的内容。我们可以把上面的 typedef 全部转换为 using:
1 | |
很简单吧?只是把名字前置了,相比 typedef,这种方式看起来更加方便。
当然,也别忘了我们的主角函数指针:
1 | |
更强大的是,你可以把类型别名和 decltype 组合使用,避免写出复杂的类型:
1 | |
传递函数指针
既然函数指针本身,是个指针,那么它就变成了一个对象,自然可以传递,就如其它指针一样。
作为参数传递
那这有什么用呢?其实,这样可以让同一个函数根据传入的函数指针,执行不同的操作。
比如,下面的代码,将运算的函数指针传入,然后调用函数执行自定义操作:
1 | |
花点时间好好理解一下上面的代码。
首先,写了两个执行算术操作的函数。
然后,我们创建了一个叫做 funcPType 的类型别名,它代表的类型是一个函数指针,指向接受 2 个 int 参数的返回 int 的函数。
之后,写了一个算术操作的函数,接受函数指针类型,在内部调用这个函数指针,执行相对应的操作。
最后写了 main 函数,两次传入不同的函数名字。函数名自动退化为函数指针,作为参数传入。你可以看到,我们的 a b 都没有变化,但是结果却不同。这正是因为传入了不同的函数指针导致的。
也就是说,函数指针可以把不同操作打包,交给其它部分操作。也就是,把行为作为数据传递。函数指针只记下了传入什么、返回什么,不关心实际执行了什么操作——也正因为如此,我们可以在需要函数指针的地方传入不同的函数,这极大地提升了我们程序的灵活性。
另外我要提醒你一点,不要给函数名字后面加上括号。这样会变成直接调用函数,传递的就是返回值了,会发生类型不匹配直接报错。我们要传递的,是会自动退化成函数指针的函数名字。
作为返回值
不止参数。函数指针也可以作为返回值。
来看看下面的示例代码(略去和上述相同的 sum 和 diff 函数):
1 | |
仔细看看,我们的函数根据传入的参数不同,返回了不同的函数指针。返回的指针可以作为函数调用——在两条输出语句那里,我们用的是同一个函数指针名称,但是它们指向的是不同的函数,也就导致了不同的执行结果。
这是一个非常现实的例子,同样也是把行为作为数据传递的体现。掌握了这种写法,你就可以大大提升你的程序的灵活度。
此外,我们也可以不用类型别名,但是这会出现一些非常复杂的东西,我们在下面的尾置返回类型中,进行进一步的探讨。
尾置返回类型
关于函数指针我们说的够多了,现在我们再来补充一点 C++ 新版本的知识,尾置返回类型。
顾名思义,尾置返回类型允许我们把函数返回值的类型放在末尾。来看看这个:
1 | |
这是上面的代码的另一种写法。原本用于声明函数返回类型的首位被 auto 代替了,真正的返回类型写在最后。
你可能在想,这不是多此一举吗?其实,只有当你不想用类型别名,但是却想返回函数指针(或任何复杂的类型,比如指向数组的指针)的时候,你会发现这大大被简化了:
1 | |
也补充一下,如果不用尾置返回类型,第一行
1 | |
的含义是:
- 从内向外阅读。首先
chooseOperation(char c)说明此处是个函数。 - 其次
(*...)表示它的返回值是个指针。 - 再次,
int(...)(int,int)表示这个指针指向的是个函数,函数接受 2 个 int,返回一个 int。
如果你有些混乱,不妨翻到文章开头,看看函数指针是如何声明的,对比看看:
1 | |
现在是不是发现了点规律?只是把变量名字换成函数名字+参数列表而已。
但是,我完全不建议你这么写。请一定要使用尾置返回类型、类型别名和 decltype,让你的代码更加可读。
关于函数指针和简化代码的方法,我们已经涉及了相当深入的内容了。下一节,我们将离开函数这一篇章,进入新的一部分——类。
