内容简介:文章目录新年第一更!之前群友问了一个C语言问题,即int(*(*p)())、int *(*p)()和int *(*p())的区别在哪里。确实,有时C语言的类型声明是很魔性的,看着也很令人头疼。不过如果拆分开来看其实还挺好理解的。首先还是要从最根本的结构来看。这里各举一些C语言中函数指针、指针、数组声明的例子:
文章目录 [隐藏]
新年第一更!之前群友问了一个 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
- typeid 运算符 – cppreference( https://zh.cppreference.com/w/cpp/language/typeid )
- std::type_info::name – cppreference( https://zh.cppreference.com/w/cpp/types/type_info/name )
- Scope Encoding – Itanium C++ ABI( https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangle.substitution )
- 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 )
- 巴科斯范式 – 维基百科( https://zh.wikipedia.org/wiki/%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F )
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Go语言笔记 | 04-短变量声明
- Go语言学习教程之声明语法(译)
- Go语言笔记 | 03-变量的声明和初始化
- Fanx 编程语言 3.0 版发布,支持两种变量声明风格
- Go 语言函数式编程系列教程(一) —— 变量声明、初始化、赋值及作用域
- Go 语言函数式编程系列(十一) —— 数据类型篇:字典类型的声明、初始化和基本使用
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Servlet和JSP学习指南
Budi Kurniawan / 崔毅、俞哲皆、俞黎敏 / 机械工业出版社华章公司 / 2013-4-14 / 59.00元
本书是系统学习Servlet和JSP的必读之作。由全球知名的Java技术专家(《How Tomcat Works》作者)亲自执笔,不仅全面解读Servlet 和JSP 的最新技术,重点阐述Java Web开发的重要编程概念和设计模型,而且包含大量可操作性极强的案例。 本书共18章:第1章介绍Servlet API和几个简单的Servlet;第2章讨论Session追踪,以及保持状态的4种技术......一起来看看 《Servlet和JSP学习指南》 这本书的介绍吧!