36

浅谈C语言中的类型声明

 3 years ago
source link: https://blog.kaaass.net/archives/1039?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

文章目录 [隐藏]

新年第一更!之前群友问了一个C语言问题,即int(*(*p)())、int *(*p)()和int *(*p())的区别在哪里。确实,有时C语言的类型声明是很魔性的,看着也很令人头疼。不过如果拆分开来看其实还挺好理解的。

从基本结构开始

首先还是要从最根本的结构来看。这里各举一些C语言中函数指针、指针、数组声明的例子:

// 一维数组
int arr[5];
 
// 二维数组
 
int arr[4][5];
int arr[][5];
 
// 指针
 
int *ptr;
 
// 函数指针
 
int (*func_ptr) (int, int); // 接受2个整型参数,返回值整型
int (*func_ptr) (); // 不接受参数,返回值整型

可以看到,上述的例子都是十分直观的。所以,以这些简单直观的类型为基础来理解复杂的类型就不是那么复杂了。我们尝试将上述的类型进行组合。比如,声明一个元素是整型指针的一维数组:

int *arr[5];

还挺直观的。那如果声明一个指向 一维整型数组 的指针?

int (*ptr)[5];

没错,我们使用括号以表示ptr是一个指针。而声明一个 指向一维整型指针数组 的指针也就是将上述两者组合了。

int *(*ptr)[5];

现在考虑函数。简单的就不说了,讲些容易混淆的。比如,一个指向 函数指针 的指针应该如何声明?参考数组指针的声明,我们可以这么写:

int (*(*ptr)) ();

还可以进一步简化成:

int (**ptr) ();

现在思考声明一个 返回类型为指针的函数 的指针。

int *(**ptr) ();

这样一分析,群有问题中的1、2的含义就很明显了——都是一个 返回类型为整型指针且不接收参数的函数 的指针。

C语言的类型读法可以总结为 外向内表内向外 。我来解释一下这句拗口的话。引刚刚的例子:

int *(**ptr) ();

即 int *( (* (*ptr) ) () );

从外向内读,最外是*即 指针 ,向内是 函数指针的声明 ,再向内就是 指针声明 。现在 从内向外理解 ,这是一个 指针 ,指向一个 函数指针 ,函数指针指向函数的返回值是 指针

再看个例子:

int *(*ptr)[5];

即 int *( ( *ptr )[5] );

从外向内读,最外是*即 指针 ,向内是 数组的声明 ,再向内就是 指针声明 。现在 从内向外理解 ,这是一个 指针 ,指向一个 数组 ,数组的元素是 指针

如何验证

空口无凭。不实际测试一下也无法说明刚刚分析的准确性。但是验证并不容易,有什么能直观表示变量类型的呢?答案还是有的。

还真就有这么一个测试方法,不过是在C++中——RTTI(运行时类型信息)。好在C++基本兼容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 <iostream>
#include <typeinfo>
#include <cxxabi.h>
 
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 <number> _ 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 <source-name> 第三方扩充类型

数组类型

数组类型的编码包括维数和元素类型,格式为:

A <维数> _ <类型>

二维数组将会被编码为“数组的数组”。比如int arr[3][4]的类型将会被编码为:A3_A4_i。如果声明时没有显示指定维数,那编译器将会推导一个维数。另外还需注意的是,函数参数中的数组将会被视为指针。

指针类型…

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

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)以一个非负整数以便区分。

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

int *(*a)[5]; => PA5_Pi

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

int(*(*a)()) => PFPivE

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

由于这部分内容较多,加之本篇更多侧重于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如是表示语法:

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

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

<decimal_bit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
<decimal> ::= {<decimal_bit>}+

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

<declaration> ::= {<declaration-specifier>}+ {<init-declarator>}* ;

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

<init-declarator> ::= <declarator>  | <declarator> = <initializer>
<declarator> ::= {<pointer>}? <direct-declarator>

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

<pointer> ::= * {<type-qualifier>}* {<pointer>}?

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

<direct-declarator> ::= <identifier>  | ( <declarator> )  | <direct-declarator> [ {<constant-expression>}? ]  | <direct-declarator> ( <parameter-type-list> )  | <direct-declarator> ( {<identifier>}* )

其中, ( <declarator> ) 保证了括号的优先运算, <direct-declarator> [ {<constant-expression>}? ] 对应数组声明, <direct-declarator> ( <parameter-type-list> ) 对应函数与函数指针的声明。而左递归保证了诸如多维数组的声明。

从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)
  • void (*  [3] ) (…)
  • 一个指向 一个 元素是 返回整型且不接受参数的 函数指针 的3宽数组 的指针

3.根据说明,写出下列类型。

  • PA4_A3_Pi
  • 一个元素是 一个指向 一个元素是 整型指针 的3宽数组 指针 的4宽数组

One more thing…

喂喂,你全篇都没有提到题目里的第三个吧!行,我们来看看第三个。

int *(*p());

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

int **p();

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

大家好,我是KAAAsS。真的好久没能写出一篇令我满意的文章了呢。这段时间尝试写过类型论,碰壁之后写无类型λ演算,还尝试写了其他文章,但都欠火候,所以暂存草稿箱。恍然首页已经变成每周歌词堆积最多的一段时间,再恍然9102年已至。我也终于在年末找到素材,有幸写出了这篇文章。虽然文章难说尽善尽美,但能写出来还是很令我欣慰了。对了,祝愿看到这篇文章的你,新年快乐~

Reference

  1. typeid 运算符 – cppreference( https://zh.cppreference.com/w/cpp/language/typeid
  2. std::type_info::name – cppreference( https://zh.cppreference.com/w/cpp/types/type_info/name
  3. Scope Encoding – Itanium C++ ABI( https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangle.substitution
  4. The syntax of C in Backus-Naur Form( https://cs.wmich.edu/~gupta/teaching/cs4850/sumII06/The%20syntax%20of%20C%20in%20Backus-Naur%20form.htm
  5. 巴科斯范式 – 维基百科( https://zh.wikipedia.org/wiki/%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK