盘一盘C++的类型描述符(二)

先序文章请看
盘一盘C++的类型描述符(一)

稍微组合一下的复杂类型

数组指针类型的数组类型

数组的指针类型我们已经了解了,那么,以这种类型作为元素的数组类型怎么搞?

using type = int (*)[3];
// 元素类型是数组指针的数组类型
type arr[4]; // 直接写是什么样呢?

为了说明问题,这里用了using语句,后面章节会详细介绍。对于arr,它应该是什么类型呢?其实应该是:

int (*arr[4])[3];

同理,从里向外,最内层的括号里出现了*[4],表示arr是拥有4个元素的数组类型,并且数组元素为指针(没有括号分辨内外的情况下,切记,左为外,右为内)。而外层的int [3]就表示指针的解类型,是一个数组类型。所以我们说arr是一个数组,其元素是指针类型,指针的解类型为另一种数组类型。

以此为引子我们还能拓展出很多其他更BT的组合,不过在继续发散之前,我们需要先来看另一个问题。

没有变量名,单说类型

如果我问,上一节示例中的arr是什么类型呢?很简单,去掉变量名,剩下的部分就是它的「类型描述符」,也就是int (*[4])[3]。正推很容易,但是反过来可能就让人有点摸不着头脑,比如说,请问下面类型表示什么含义:

int (**)[3];
int *(*)[3];
int **[3];

当去掉变量名后,这个单纯的类型描述符就显得很诡异,尤其是这个小括号,搞得乍一看跟什么奇怪的密文一样。遇到这种情况其实也不用慌,我们需要「脑补」一个变量名,再就好解释了。

变量名添加的位置一定是最内层,我们找到最内层的部分,分别把变量名补进去,再来解释它的类型就好:

int (**p1)[3];
int *(*p2)[3];
int **p3[3];

所以,p1是指针类型,其类型为另一个指针类型,其解类型为拥有3个int元素的数组类型。或者说,p1是「(数组的指针) 的指针」类型(数组的二级指针)。

p2是指针类型,其解类型是一个数组,含有3个指针类型元素,其解类型是int。或者说,p2是「(指针数组) 的指针」类型(数组的一级指针)。

p3是数组类型,拥有3个指针类型的元素,其解类型是另一个指针类型,其解类型是int。或者说,p3是「(指针的指针) 的数组」类型(二级指针的数组)。

typedef与using

类型复杂了以后可读性会直线下降,所以这时候更加推荐在合适的层级进行类型重命名。typedef是从C语言继承来的语法,但它有一个缺点,就是新的类型名在类型描述符中处于「变量名」的位置,看起来会有些奇怪,比如说:

typedef int type1[3]; // type1其实就是int[3]类型
typedef int (*type2)[2]; // type2其实就是int (*)[2]类型
typedef int *(*type3[4])[5]; // type3其实就是int *(*[4])[5]类型

using则是完全把新的类型名从原本的类型说明符中提取出来了,看上去会更直观一些:

using type1 = int [3];
using type2 = int (*)[2];
using type3 = int *(*[4])[5];

需要注意的是,两种用法的区别仅仅在于描述方式,其效果是完全等价的。实际使用中不用太过纠结,选哪一种都行。

多维数组类型

前面我们介绍了多级指针,其本质并没有几级,而是说指针的解类型还是一个指针罢了。

同理,所谓的「多维数组」并不会真的给出一个多维空间, 其本质是「数组的数组」,或者说「元素为数组类型的数组类型」。

举例来说:

int arr1[2][3];

我们说,arr1是一个拥有2个元素的数组类型(最内层才是本质),元素类型是一个有3个int元素的数组类型(外层表示的是数组的元素类型)。如果我们拆开来写,应该是这样的:

using ele_t = int [3];
ele_t arr1[2];

所以,我们搞清它的本质,那么其他的问题就容易解释了,比如说:

auto p = arr1;  // 请问p是什么类型?

既然,数组类型可以隐式转换为首元素的指针,那么,这里的p应该就是「(数组的元素)的指针 类型」。那么arr1的元素应该是ele_t类型,所以,p应该是这种类型的指针类型,也就是ele_t *类型,也就是int [3]的指针类型,也就是int (*)[3]类型。

再比如说:

auto p2 = &arr1; // 请问p2是什么类型?

这里并不是用数组直接转换,而是取了地址,那么p2就应该是一个二维数组的指针类型了。因为arr1ele_t [2]类型,那么p2就应该是它的指针类型,也就是ele_t (*)[2]类型,也就是int (*)[2][3]类型。

希望读者可以搞清指针和数组的类型描述方法,对于由他们组合的复杂类型也就能见招拆招,迎刃而解了。

函数类型和函数指针类型

对于纯C语言程序员来说,可能都不容易意识到「函数类型」这种类型,毕竟你不会用一个函数的类型去定义另一个函数,也不会关心它的大小之类的事情。但是有了C++的模板以后,这件事就变得值得关注了。

「函数类型」也是一种类型,它跟数组类型的描述比较类似,包括两部分,函数返回值类型和函数的参数类型。

int f1(int, double); // f1是函数类型,返回值类型为int,接收2个参数,分别为int、double类型
void f2(); // f2是函数类型,返回值类型为空,参数为空
int f3(int); // f3是函数类型,返回值类型为int,接收1个参数,是int类型

有一个需要注意的是,如果函数无返回值,那么必须要用void占位,而不可以省略,在早期版本标准里,返回值为int是可以省略的,也就是说:

f();
// 相当于
int f();

不过这种标准已经废除,也不被推荐,但自始至终void都不能省略的。

而如果一个函数不接收参数,那么小括号中可以空着,或者写上void,也就是说:

void f(); 
// 相当于
void f(void);

函数类型之所以也能成为一个类型,主要是由于冯·诺依曼体系的计算机中,在存储上并不区分「数据」和「指令」。函数在编译后其实就会成为一段指令,同样会加载到内存中,同样会拥有地址。那么从本质上来说,它就跟变量是一样的,这就是函数类型。

有了前面复杂类型的洗礼,那么这里我们就可以稍微添加一点难度了。如果要写一个返回值为数组指针类型的函数,要怎么办?我们观察到,在函数类型的说明符里,返回值在左边,参数在右边。而前面我们已经介绍过,没有括号区分的情况下,左边是外边,右边是里面,也就是说,返回值应该写在外面,参数应该写在里面。那么既然返回值也是一个复杂类型,那么就应当把这个类型套在函数类型的外面,用于表示返回值类型:

int (*f())[3]; // 表示f是一个函数,无参数,返回值类型是一个指针,指针的解类型是int [3]
// 分开来写就是
using ret_t = int (*)[3];
ret_t f();

通俗来讲就是说,f是一个返回值为「数组指针」类型的函数。

既然函数类型也是一种数据类型,函数也拥有内存地址,那么自然我们就可以取这种类型的指针,也就是我们常说的「函数指针」类型。

书写的思路一样,本身是一个指针,所以星号在最内层,紧贴变量名,解类型是函数类型,那么就把函数类型的描述符写在外层:

void (*p)(int); // 表示p是一个指针,解类型是函数类型void (int)
// 分开来写就是
using func_t = void (int);
func_t *p;

不过有一点比较特殊的是,函数类型可以隐式转换为函数指针类型,而函数取地址也能得到函数指针类型。这样就会造成下面这种很有意思的情况:

void f();

void Demo() {
  auto p1 = f; // p1是void (*)()类型
  auto p2 = &f; // p2也是void (*)()类型
  auto p3 = *f; // p3也是void (*)()类型
  auto p4 = *****************f; // p4也是void (*)()类型

  auto p5 = &f; // p5是void (**)()类型「二级指针」
}

离谱归离谱,但……C++嘛,DDDD~

上面也展示了函数指针的指针了,那我们再来个难一点的,一个函数的返回值是函数指针的情况,应该怎么写呢?

using ret_t = void (*)();

ret_t f(); // f的类型是?

思路都没有变,返回值套在外面就好,不再赘述了:

void (*f())(); // 外层的void ()表示指针的解类型,内层的*表示f的返回值是指针,f后面的()是f的参数列表。

非静态成员函数类型

这个标题已经比较自洽了,所谓「非静态成员函数(non-static member function)」又有地方管它叫「方法(method)」,指的就是类(或结构体/共合体)中的成员函数,并且没有用static修饰的。举例来说:

struct Test {
  void f();
};

此时的f就是一个非静态成员函数,它的类型是:

void (Test::)();

先别急,我们来慢慢解释~~

首先我们要明确一件事,所谓非静态成员函数之所以是一个独立的类型,主要是因为它会隐含一个函数参数,也就是this指针所指的对象。非静态成员函数不能够直接调用,而是要通过一个「对象」作为发起方。用上面的例子来说:

void Demo() {
  Test::f(); // 这种调用方法是错误的
  
  Test t;
  t.f(); // 必须要有发起方
}

那么既然有「发起方」,那么这个发起方一定有自己的类型。发起方的类型需要与所调用的函数所在类型相匹配,也就是说,一个对象只能调用自己类,或自己的父类(包括间接父类)中的非静态成员函数,并且当符合默认情况时可以省略类名,我们来看一个具体的例子:

struct Base {
  void f1();
};

struct Test1 : Base {
  void f2();
};

struct Test2 {};

void Demo() {
  Test1 t1;
  Test2 t2;
 
  t1.Base::f1(); // 调用父类的成员函数,OK
  t1.Test1::f2(); // 调用自己类的成员函数,OK
  t1.f1(); // 省略类名,则会向上查找到继承链中最近的方法实现,这里相当于t1.Base::f1()
  t1.f2(); // 省略类名,同理,这里相当于t1.Test1::f2();

  t2.f1(); // 报错,应为Test2的继承链中找不到f1函数
  t2.Base::f1(); // 同样报错,因为t2不属于Base类的继承链中的类型,不能够用它来调用Base类的成员函数
}

所以,对于非静态成员函数来说,它的发起方类型(也就是隐含参数的类型)也应当体现在它的类型描述符中。因此,对于「一个Test1类中的成员函数,返回值为void,参数为空(其实是有一个隐含参数的,这里说的是不包括隐含的,只看明面的情况,它是空)」这种类型的函数,其类型描述符是:void (Test1::)()

或者也可以从另一个角度来解读,就是隐含参数是Test1类型,而隐含参数的类型要放在函数名的前面(也就是最内层的部分)。总之,这里希望读者知道的是,非静态成员函数的类型描述符,需要出现这个类型名,用以表示隐含参数的类型(当然对于继承链下游的类型也同样支持,这种情况我们认为做了一次static_cast就好,也可以解释)。

再多啰嗦一句,我们刚才说的都是非静态成员函数的情况,而静态成员函数由于不含隐含参数,因此它的描述方式跟普通函数是一样的:

struct Test {
  static void f1(); // f1的类型是void ()
  void f2(); // f1的类型是void (Test::)()
};

那么,如果非静态成员函数还含有一些其他的属性(比如说const&noexcept)的话,它的声明位置同样在类型的最后,也就是参数列表的后面:

struct Test {
  void f1() const;
  void f2(const int *) &&;
  int f3() noexcept;
  void f4(int) & noexcept;
};

上面例子中,f1~f4的类型分别是:

void (Test::)() const; // f1的类型
void (Test::)(const int *) &&; // f2的类型
int (Test::)() noexcept; // f3的类型
void (Test::)(int) &noexcept; // f4的类型

那么,如果一个非静态成员函数的返回值是一个复杂类型呢?同理,放到外层就好了(只要把握住内外层,那么所有的问题都能迎刃而解),比如说:

using type1 = int (*)[3];
using type2 = void (*)(int);
struct Test {
  type1 f1() const;
  type2 f2(double) noexcept;
};

对应的类型是:

int (*(Test::)() const)[3]; // f1的类型
void (*(Test::)(double) noexcept)(int); // f2的类型

不过需要大家知道的是,「非静态成员函数」类型只是一个概念上的类型,实际情况下我们是没法用这种类型来定义变量的,甚至都没法直接定义这种类型:

using type = void (Test::)(); // 报错
void (Test::f)(); // 报错

而能够使用的类型则是它的指针类型,下一节来介绍。

非静态成员函数指针类型

既然非静态成员函数基本可以等价于一个隐藏了调用方这个参数的函数类型,那么它本质还是一个函数,也就是一个代码段,自然也是要入内存的。所以,它同样含有内存地址,也就含有对应的指针类型,也就是非静态成员函数指针类型。

同样,在最内层加一个指针符号即可表示函数指针类型:

struct Test {
  void f();
};

void Demo() {
  auto fp = &Test::f;
  // fp的类型就是非静态成员函数指针类型,等价于:
  void (Test::*fp)() = &Test::f;
}

在上面的例子中,fp的类型是void (Test::*)()类型。

照例,我们还是做一些组合,下面直接给出一些例子,就不再赘述:

struct Test {
  void f1() const;
  int f2() noexcept;
  int f3(double, int) &;
};

void Demo() {
  using type1 = void (Test::*)() const;
  using type2 = int (Test::*)(double, int) &;
  using type3 = decltype(&Test::f2);

  type1 *p1; // void (Test::**)() const
  type3 p2; // int (Test::*)() noexcept;
  type2 arr[2]; // int (Test::*[2])() noexcept;
  type3 (*p3)(int); // int (Test::*(*)(int))(double, int) &;
}

顺便啰嗦一句,非静态成员函数指针的大小为2个指针的大小,在64位环境中应该是16个字节(读者可以自行sizeof来验证),这部分详细解释可以看深入C++成员函数及虚函数表。

【第三篇待更】

你可能感兴趣的:(C++代码,c++)