C/C++的特别复杂的类型

那些C/C++的特别复杂的类型

首先来看如下的一个声明

1
float (*(*fp2)(int,int,float))(int);


是不是已经看晕了,但其实这个还不够复杂,我就不举更多的例子了
前排提示,为了您和您的家人健康,如果不是迫不得已,请不要写出这种反人类代码
除非你想被我##%&@&&%(数据删除)
但别说这个东西没用,有些底层的东西确实有这么复杂(但不一定复杂成这样)


前人的智慧

其实可以通过一个叫右左法则的东西来解决这个问题,不过我觉得后缀表达式还是看的有点头疼,有兴趣可以找找看,它可以将式子变成一个后缀表达式,采取编译器的形式进行处理。但我们不采用这种方法,原因是后缀表达式还是不够可读,原因是我不会用
补充:链接贴在这里B站:初中生也能看懂的C/C++类型声明规则教学,很简单的!


着手解决这个问题

我们先来看看一个声明,他到底是什么意思

1
char (*p)[] = NULL;//这是一个指向char数组的指针

其实无论是什么样的声明,都是同样的格式

1
2
3
Type_1 Type_2 = xxx;
//准确来说Type_2不是一种类型,而是对变量进行多种符号操作后的产物
//简单的变量声明如 "int a;" 中 a被0个符号修饰 从而a等价int

就是说,在如上这个声明里就是
Type_1char
Type_2(*p)[]
你可以把p就想象成一个已经定义的指向char数组的指针
对其解引用并取下标类型就是char

也就是说
char 在类型上等价于(*p)[]

也就是说这是通过一个Type_1在类型上等价Type_2的一个类型的等式来确定的要赋值的变量名的类型到底是什么
所以,我们可以对一个合法的变量声明在其内部任意位置进行分割,然后加括号保证可读性,用以区分Type_1Type_2

也就是说其实可以这么处理:

1
2
3
4
5
int **p[]; // 以下对表达式逐步进行分割
**p[] //是int
*p[] //是指向int的指针 即int*
p[] //是指向 指向int的指针 的指针 即int**
p //是指向 指向int的指针 的指针 的数组 即int**


现在找一个看起来更舒服的方法

让我们来看一个例子

1
2
3
char * const * ch2;
(const (char*))* ch2;
//这两个是等价的

仔细看看第二个式子绝对比第一个式子阳间,为什么?
因为这里的Type_2就是ch2
变量名ch2没有经过各种符号的修饰,直接等于左边的一坨Type_1
那所以说其实化成可读性强的式子就是把变量名从一堆修辞里拯救出来的过程
就是说把变量名的一堆枷锁找个简单的类型当替死鬼装它身上(奇妙的比喻增加了)
我们可以用一些看上去更数学的方法来做(滑稽)

需要强调的一点

以下的转化到其最后结果只是为了可读性,不一定能过编译!!!!!
以下的转化到其最后结果只是为了可读性,不一定能过编译!!!!!
以下的转化到其最后结果只是为了可读性,不一定能过编译!!!!!

让我们来开始着手做吧

首先一个声明里其实是只有一个简单类型的(这里的简单类型就是说的那些基础类型,intchardouble,或者是classstruct)
模板<>函数()内的简单类型其实是不会和其混淆的
模板<>有尖括号
函数()在变量名后,而简单类型一定在变量名前
int是类型声明,(int)是参数表

1
int fun(int);//int不会和(int)混淆

变量名并不好脱掉一堆枷锁嘛,所以我们要强迫让简单类型这个替死鬼自己扒了变量名的衣服自己穿上(What’s The Fuck)
1
float (*(*fp2)(int,int,float))(int); // 这是一个函数指针的形式

float替死鬼上线,先把和它处于同一优先级的”(int)“扒了套自己身上,并加个括号以示对它的主权(乐)
并暗示你被我扒了一层所以这下(*(*fp2)(int,int,float))最外层的括号可以脱了
1
(float (int)) *(*fp2)(int,int,float);

可以看到这个工具的强大了
(float (int))这个就是一个传入int返回float的函数
但是过不了编译!!!!!注意!!!!!
然后替死鬼现在是(float (int)),继续把*扒了
1
((float (int))*) (*fp2)(int,int,float);

虽然现在已经能看出来最外层是(float (int))的函数指针了,但我们再来一次
1
(((float (int))*) (int,int,float)) *fp2;

得到 返回值((float (int))*,参数表(int,int,float)Type_1
再来
1
((((float (int))*) (int,int,float)))* fp2;

这下结构也太清晰了
看懂了但是很难描述,但是我还是想尝试描述一下 (就算是中文也要靠多重括号来断句(悲
fp2是一个{以【返回值为float,参数表为(int)的函数指针】为返回值,(int,int,float)为参数表}的函数指针

实质上当中的过程覆盖了所有可能的情况了,我们可以用 着手解决这个问题 上面这一小标题所写的本质证明这样做的合理性,如指针无非就是一方解引用一方变指针做到的等价。


cv限定符

因为有constvolatile这样的限定符存在,故而这个方法进行开扒的时候,需先向替死鬼看一看有没有这些限定符,再结合后面距离Type_1最近的运算符
cv限定符应该置于Type_1之前以修饰Type_1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//例子一
char* const ch1;
(char*) const ch1;
const (char*) ch1;
//例子二
char **const *const ch2;
(char *) *const *const ch2;
((char *)*)const *const ch2;
const (const((char*)*))* ch2;
//例子三
int *a[][N];
((int *)[N])[] a;
//例子四
int a()[];//无法过编译,如vs2022会指出其为返回数组的函数
(int [])() a;//可以看到这是返回数组的函数,非法
//例子五
int a[]();//无法过编译,如vs2022会指出其为函数数组
(int ())[] a;//可以看到这是函数数组,非法

对于例子三和例子四还有例子五,实质上有一个整体考虑的办法

  • 例子三[][]可以当作二维数组来看,第一个[]转移过程中可以不给替死鬼加括号就清楚了
  • 对于例子四()[],其实我们完全可以把数组取下标看作一种特殊的函数,反过来也是
  • 函数其实和数组在这里可以不用特别区分,所以五和四和三一样
  • 同时这也启示我们()[] , []()如果出现在一块,这必定不可能是合法的
  • 因为其必定包含函数数组或是返回数组的函数,不合法
    *当然[]()因为优先级是最高的,所以和名字结合最紧,最后才能被扒掉,而()[]之间呢,与名字最靠近的当然结合更紧,所以在之后被扒

Ending

至此我们完成了这种特别复杂的声明的辨析,但其实这个工具还有一种应用就是依照这种格式,可以从可读性强的格式反向推得这种复杂的声明
Type_1在类型上等价Type_2先将变量名后缀移到Type_1上,再通过分割将变量名彻底弄干净(无符号修饰无后缀),最后变量类型即为Type_1,而开扒的顺序,关键看这些符号作用在变量名上的顺序,如果优先级高,则说明与之结合紧,越晚被扒,而括号里的部分可以看作一个整体开扒,这样就可以化简整体的结构
终于写完了,虽然如此,我只希望尽可能不要有人为了炫技而写出这种声明
脱帽子,yeah~~


editor 594飞飘
audit Serein