C语言中的类型声明

Tue 01 January 2019

int((p)())、int (p)()和int (p())的区别在哪里?

typeid 在C++中,通过typeid运算符,我们能得到表示类型的std::type_info对象(需要引入头文件typeinfo)。

std::type_info对象有一个成员函数name,是类型名称的字符串。

#include <iostream>
#include <typeinfo>

using namespace std;

int main() {
    int *(*a)();
    cout << typeid(a).name() << endl;
    return 0;
}

看一看输出:

PFPivE

嗯?这是什么鬼?然而同一段代码在隔壁MSVC的输出却是:

int () ()

去重整 没错,因为std::type_info的实现是由编译器提供的,所以name的行为自然也随编译器差异而转移。其中,MSVC 、 IBM 、 Oracle等编译器会返回可读性良好的类型名(如:int () ()),而gcc与clang却会返回被重整(mangle)的名称。所谓的重整,即将C++源代码的标识符转换成C++ ABI的标识符。所以对应的,我们需要去重整(demangle)。对于GCC,我们可以使用API abi::__cxa_demangle 来完成这个工作。

include

include

include

using namespace std; string demangle(const std::type_info &ti) { int status; return abi::__cxa_demangle(ti.name(), 0, 0, &status); } int main() { int (a)();

cout << demangle(typeid(a)) << endl;
return 0;

} 于是输出就变成了:

int ()()

当然,也可以通过c++filt指令。

λ c++filt -t PFPivE int ()()

阅读重整化类型(GCC,cross-vendor C++ ABI) 不过,去重整完的类型名似乎并不太能提供多少关于这个类型的信息,反倒是重整过的类型名表达的更加清楚。所以,我们也有必要来了解GCC中的重整化类型名。由于GCC使用cross-vendor C++ ABI,那我们就来看看其关于类型重整的编码。

內建类型 内建类型的编码基本上可以用这个表格来概括。

重整化名 类型 v void w wchar_t b bool c char a signed char h unsigned char s short t unsigned short i int j unsigned int l long m unsigned long x long long, __int64 y unsigned long long, __int64 n __int128 o unsigned __int128 f float d double e long double, __float80 g __float128 z 变长参数 Dd IEEE 754r 十进制浮点数 (64 bits) De IEEE 754r 十进制浮点数 (128 bits) Df IEEE 754r 十进制浮点数 (32 bits) Dh IEEE 754r 半精度浮点数 (16 bits) DF _ ISO/IEC TS 18661 二进制浮点类型 _FloatN (N bits) Di char32_t Ds char16_t Da auto Dc decltype(auto) Dn std::nullptr_t (即 decltype(nullptr)) u 第三方扩充类型 数组类型 数组类型的编码包括维数和元素类型,格式为:

A<维数>_<类型>

二维数组将会被编码为“数组的数组”。比如int arr[3][4]的类型将会被编码为:A3_A4_i。如果声明时没有显示指定维数,那编译器将会推导一个维数。另外还需注意的是,函数参数中的数组编码比较特别。函数参数中,一维数组和多维数组的第一维将会被视为指针(即使给定维数),其余将会照常编码。举几个例子:

int [] => Pi int [3] => Pi int [4][5] => A5_Pi

指针类型… 指针类型的编码比较简单,即

P<被指类型>

同样类似语法的还有左值引用(R,C++)、右值引用(O,C++11)、复数对(C,C99)、虚数(G,C99)。

函数类型 函数类型通过P、E对来编码:

P<函数签名类型>E

其中函数签名类型为返回值类型后跟上参数类型。变长类型将会被编码为z,例如printf将会被编码为FiPKczE(返回整数i,参数为常量char指针、变长参数)。事实上这里介绍的格式只是一个简化版本,详细的还请查看文后的文档。

结构体类型 结构体类型通常只由一个简单的名字(source-name)构成:

<名称字符数><名称>

比如对于struct Test a;,a的类型将会被编码为4Test。匿名结构体的类型编码要复杂的多,而且还涉及到作用域的问题。由于比较复杂,这里简单提及下。匿名结构体的类型编码除了具有当前作用域的信息,还附带了一个辨别器(discriminator),即以一个非负整数来区分不同的匿名结构体。

压缩 注意:本部分内容较复杂,这里仅简单的说明

在诸如函数的参数列表中,很容易出现多个参数类型相同的情形。而较复杂的类型重整化后通常较长,完整重复十分占空间。所以重整化时会针对相同的类型进行压缩。重整化时会使用如下格式来代替之前出现过的类型:

S_ 或 S<序列号>_

其中序列号是以base36编码的序号。S_是第一位,S0_是第二位,SA_是第12位,S10_是第38位……以此类推。在编码时,部分首次出现的类型将会被放进待替换列表,若再次遇到重复者则进行替换。需要注意的是,以下类型并不会被替换:

除第三方扩充类型的内建类型 除extern “C”限定函数的函数和运算符名称 举个例子,对于函数指针

PFiPiS_PdS0_E => int ()(int, int, double, double*)

S_到S2_将分别替代:

S_ => int S0_ => double S1_ => int (int, int, double, double) S2_ => int ()(int, int, double, double*)

函数指针同样可以被整体替换。比如对于函数原型

int func(int , int ()(int ), int ()(int *));

它的类型将会被重整化为:

FiPiPFiS_ES1_E

验证 随便举两个例子以说明之前分析的正确性。

int (a)[5]; => PA5_Pi

一个指针(P),指向一个5宽数组(A5_),数组类型为指针(P),指向整型(i)。

int((a)()) => PFPivE

一个指针(P),指向一个函数(P..E),其返回类型为指针(P)指向整型(i),其不接受参数(v)。

BTW 由于这部分内容较多,加之本篇更多侧重于C语言,所以就不做过度深入了。感兴趣的话可以查看相关文档(https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangling-type)。如有机会,我可能会开个坑详细写一写2333

再进一步:BNF范式 之前我提出了外向内表内向外的阅读方法。不过这个仅仅是简单的总结,所以这一小节让我们再进一步深究下去,来从C语言的BNF文法中理解类型声明的语法。

BNF范式 如果你对BNF范式有一定了解,请跳过这一段直接去看“分析”节。

巴科斯范式(英语:Backus Normal Form,缩写为 BNF),又称为巴科斯-诺尔范式(英语:Backus-Naur Form,缩写同样为 BNF,也译为巴科斯-瑙尔范式、巴克斯-诺尔范式),是一种用于表示上下文无关文法的语言,上下文无关文法描述了一类形式语言。它是由约翰·巴科斯(John Backus)和彼得·诺尔(Peter Naur)首先引入的用来描述计算机语言语法的符号集。

——巴科斯范式 WIkipedia

简而言之,BNF如是表示语法:

<符号> ::= <使用符号的表达式>

表达式相当于一些字符串,多个表达式可以用’|’分隔。比如十进制数可以这么表示:

::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

::= {}+

分析 本分析基于,链接见文末。C语言的一个编译单元(translation unit)由数个外部声明组成(external declaration)。而一个外部声明可以是一个函数定义或者声明。其中,一个声明由1个或多个声明指定符(declaration specifier)和0个或多个初始声明子(init declarator)再加一个“;”构成。

::= {}+ {}* ;

声明指定符就是void、int等等类型指定符还有一些其他指定符。我继续跟踪下去:

::= | =

::= {}?

有点眉目了,我们来分析声明子(declarator)。首先就是指针

::= * {}* {}?

其中{}?递归(右递归)的定义了多重指针(如:**)。再来看直接声明子(direct declarator):

::= | ( ) | [ {}? ] | ( ) | ( {}* )

其中,( )保证了括号的优先运算, [ {}? ]对应数组声明, ( )对应函数与函数指针的声明。而左递归保证了诸如多维数组的声明。

优先级 从BNF范式中,我们可以看出指针声明和其他声明的优先级。其中,括号对优先级最高。其次,数组和函数指针的优先级相同,而指针的优先级最低。为了说明更加清楚,我们用经典的“数组指针”和“指针数组”来说明。

int *arr[3];

由于数组声明的优先级更高,所以arr是个数组,*的优先级较低所以arr的数组元素类型是整型指针。所以这是一个指针数组。

int (*arr)[3];

由于括号对优先级更高,考虑*,所以arr是个指针,数组声明的优先级较括号对低,所以指针指向的是一个数组。于是,这是一个数组指针。

总结 回到我们总结的规律。“从外向内”指的是优先级从低到高,“从内向外”指的是声明的语义逐渐“深入”。

练习 1.说出以下声明中变量a的类型,使用typeid验证。

int (a)(int); int * (a[5])(int); int ((a)[3])[4]; 2.写出下列类型重整化后的形式。

int () (double, int , int [], double) void ( [3]) (…) 一个指向一个元素是返回整型且不接受参数的函数指针的3宽数组的指针 3.根据说明,写出下列类型。

PA4_A3_Pi 一个元素是一个指向一个元素是整型指针的3宽数组的指针的4宽数组 One more thing… 喂喂,你全篇都没有提到题目里的第三个吧!行,我们来看看第三个。

int (p());

首先,我们并没有看到象征函数指针的(*p)()。好像还有点不明白?那按照优先级,我们去除一对多余的括号。

int **p();

龟龟,这不是函数原型嘛!

Category: 编程c