EasyC++30,函数指针进阶

326 阅读4分钟

大家好,我是梁唐。

这是EasyC++系列的第30篇,继续来聊聊函数指针。

想要追求更好阅读体验的同学,可以点击访问github仓库: EasyLeetCode

函数指针进阶

简单的函数指针比较简单,但对于复杂的情况则显得有些恐怖。下面我们来看下C++ primer当中提供的一些例子:

const double* f1(const double ar[], int n);
const double* f2(const double [], int);
const double* f3(const double *, int);

这三个函数看起来长得不一样,但其实是等价的。因为在函数参数列表当中,数组和指针是等价的。其次我们可以在函数的原型中省略掉变量名,因此const double ar[]可以简化成const double [],也可以写成const double *

有了这三个函数之后,假设我们要声明一个指针,指向这三个函数。根据我们前文当中说过的,可以将函数名替换成(*pt)来实现:

const double* (*pt)(const double *, int) = f1;

其实这个语句看起来就有些复杂了,整个语句的可读性很差。如果不是知道这里用的是一个函数指针,乍一看想要看明白估计不太容易。我们可以分成两个部分来理解,其中const double *是一个整体,表示函数的返回值类型是一个const double *也就是一个常量浮点数的地址。其次(*pt)是一个整体,代替了函数名,表示这是一个指向函数的指针。

在C++11当中提供了叫做auto的新特性,它可以帮助变量自动识别对应的类型,可以解决一些类型特别复杂的问题,比如:

auto p2 = f2;

函数指针有两种调用方法,除了可以使用(*p2)的方式调用之外,也可以直接使用名称调用:

const double* x = p2(ar, 3);
const double* y = (*p2)(ar, 3);

显然前者更好,更清楚。这里其实有一个疑问,为什么这两种方式都可以执行呢?这是因为当我们执行auto p2 = f2的时候,其实是执行的auto p2 = &f2,C++会隐式地将函数转换成函数的地址。因为函数的值本身就是一个地址,所以这两种方式才都能正确地运行。

问题还没有结束,假如我们要定义一个指向函数的指针数组呢?这应该怎么声明?

也就是const double* (*pt)(const double *, int)这样一个类型的数组,它应该怎么声明,这个方括号应该放在那里?

正确答案是放在括号里:

const double* (*pt[3])(const double *, int);

因为运算符[]的优先级高于*,因此*pt[3]表示pt是一个长度为3的指针数组。其他的内容表明了该指针的类型。

由于我们定义的是一个数组,所以这里不能使用auto,因为自动类型推断只能用于单值初始化而不能用于初始化列表。

到这里还没结束,还有更恐怖的,如果我们想要定义一个指向这个数组的指针,应该怎么办呢?如果使用auto可以写成:

auto ptr = &pt;

如果不使用auto呢?首先我们可以想到,这个声明是基于pt的,我们需要在pt的声明上加上一个*,但问题是加在哪里呢?

进一步分析,会发现我们需要指出这是一个指针,而不是数组。意味着核心的部分应该写成(*ptr)[3],表示这是一个指向长度为3的数组的指针。因为[]的优先级更高,所以需要使用括号。如果写成*ptr[3]表示这是长度为3的指针数组。

我们进一步倒推,(*ptr)[3]这个数组当中的元素是什么类型呢?是指向函数的指针,所以写出来结果是这样:

const double *(*(*ptr)[3])(const double*, int) = &pt;

很明显,这样的定义非常非常的难以理解。而且这还不是最复杂的情况,比如函数的返回类型又是一个指向一个函数的指针……明摆着告诉我们含义我们仍然要推敲一会,如果在一段不明的代码当中遇到,可能会直接抓狂吧……

也正因此,C++11当中推出了auto特性,可以简化这种情况。

多说一句题外话,golang语言当中将变量的类型放在变量的后面而不是前面,其中一个原因就是为了解决类似情况的复杂性。

如果是golang来定义同样的内容,会是这样的:

func f2(arr []float64, n int) *float64 {
    // todo
}

// 函数指针
var p1 func([]float64, int) *float64 = f2;
// 函数指针数组
var pt [3]func([]float64, int) *float64;
// 函数指针数组的指针
var ptr *[3]func([]float64, int) *float64 = &pt;

很明显,虽然变量类型写在变量后面刚开始会不太习惯,但是很明显这样要清晰很多。