文章目录 [隐藏]

新年第一更!之前群友问了一个 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

#include

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。如果声明时没有显示指定维数,那编译器将会推导一个维数。另外还需注意的是,函数参数中的数组将会被视为指针。

指针类型…

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

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如是表示语法:

::=

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

::= 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)

void (*  [3]

) (…)

一个指向 一个

元素是 返回整型且不接受参数的

函数指针 的3宽数组

的指针

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

PA4_A3_Pi

一个元素是 一个指向

一个元素是 整型指针

的3宽数组

的 指针

的4宽数组

One more thing…

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

int *(*p());

首先,我们并没有看到象征函数指针的 (*p)()

。好像还有点不明白?那按照优先级,我们去除一对多余的括号。

int **p();

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

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

Reference

c语言中很多中括号由外向里,浅谈C语言中的类型声明相关推荐

  1. c语言 去掉双引号_技术分享|浅谈C语言陷阱和缺陷

    良好的软件架构.清晰的代码结构.掌握硬件.深入理解C语言是防错的要点,人的思维和经验积累对软件可靠性有很大影响.C语言诡异且有种种陷阱和缺陷,需要程序员多年历练才能达到较为完善的地步.软件的质量是由程 ...

  2. c语言函数调用参数调用的太少,浅谈C语言函数调用参数压栈的相关问题

    参数入栈的顺序 以前在面试中被人问到这样的问题,函数调用的时候,参数入栈的顺序是从左向右,还是从右向左.参数的入栈顺序主要看调用方式,一般来说,__cdecl 和__stdcall 都是参数从右到左入 ...

  3. 教师教学质量评价系统c语言,教师课堂教学评价大全_浅谈C语言课堂教学方法

    摘要:编程类课程是计算机系学生普遍感觉较难的学科,课堂气氛往往显得比较沉闷,令学生感觉枯燥无味.为了提高学生的学习兴趣,本文提出"五环节教学法",将学生变为学习的主体,让学生从被动 ...

  4. css中如何实现帧布局_浅谈web前端中的表格布局与CSS盒子布局

    在web前端设计排版时我们可能会用到表格布局和div+CSS布局,但现在主要使用后者,为何?今天我们来谈一谈两者之间的发展和原理. 话不多说下面来干货 发展过程 上个世纪Web开发人员流行使用表格进行 ...

  5. c程序语言的常量变量和标识符,浅谈C语言中的常量与变量.pdf

    课程教育研究 CourseEducationResearch 2014年4月 上旬刊 教学.信息 浅谈C语言中的常量与变量 刘 星 (青 岛工学院商学院 山东 青岛 266300) [摘要]在任何一种 ...

  6. c语言弱符号与函数指针,浅谈C语言中的强符号、弱符号、强引用和弱引用【转】...

    首先我表示很悲剧,在看<程序员的自我修养--链接.装载与库>之前我竟不知道C有强符号.弱符号.强引用和弱引用.在看到3.5.5节弱符号和强符号时,我感觉有些困惑,所以写下此篇,希望能和同样 ...

  7. c语言指针很危险,浅谈C语言中指针使用不当的危险性.doc

    浅谈C语言中指针使用不当的危险性.doc 第 19 卷 Vol . 19 第 2 期 No . 2 洛阳师专学报 Journal of Luoyang Teachers College 2000 年 ...

  8. python中内置的四种数值类型为_浅谈python语言四种数值类型

    Python语言支持四种不同的数值类型,包括int(整数)long(长整数)float(浮点实际值)complex (复数),本文章向码农介绍python 四种数值类型,需要的朋友可以参考一下.希望对 ...

  9. 浅谈V8引擎中的垃圾回收机制

    浅谈V8引擎中的垃圾回收机制 这篇文章的所有内容均来自 朴灵的<深入浅出Node.js>及A tour of V8:Garbage Collection,后者还有中文翻译版V8 之旅: 垃 ...

最新文章

  1. 面试官:说说 Java 中的 Unsafe 和 CAS
  2. pythonexcel表格教程_python对excel表格的操作
  3. 如何增加服务器磁盘空间,linux 服务器如何扩展磁盘空间
  4. SpringCloud相关概念介绍
  5. SQL数据库语言基础之SQL Server自带数据类型、自定义数据类型与使用、创建修改数据表
  6. LeetCode刷题(47)--Gray Code
  7. python从入门到精通 明日科技 电子书-Python从入门到精通(明日科技出版) 源代码+课件+视频 全套...
  8. Eclipse清除SVN的账号信息
  9. python join用法
  10. 免备案二级不死域名制作教程大全
  11. 违反GPL协议赔偿50万,国内首例!
  12. 基于协同过滤的电影推荐
  13. 微信朋友圈这样招生,才不会被屏蔽!(附实操案例)
  14. 压测——普通接口压测
  15. 7-1 sdut-Collection(Map)-1 读中国载人航天史,汇航天员数量,向航天员致敬
  16. 元数据“人行横道”MC
  17. datastage 如何把db2的varchar列数据抽取到mysql的longtext列
  18. 苹果手机也可以开启电信VoLTE!
  19. backtrack回溯算法
  20. 图书管理系统 C语言链表实现 学校大作业功能齐全(书籍信息以及用户信息保存在附带的txt文件中)

热门文章

  1. 2020年全国大学生智能汽车竞赛山东赛区比赛专家组工作方案
  2. ssm 上传图片到mysql_ssm(Spring+Spring MVC+MyBatis)+Web Uploader开发图片文件上传实例,支持批量上传,拖拽上传,复制粘贴上传...
  3. hadoop和python的关系_Python 的 map 和 reduce 和 Hadoop 的 MapReduce 有什么关系?
  4. python微信红包代码_Python实现的微信红包提醒功能示例
  5. python在线编辑器编译excel_python在线编译器的简单原理及简单实现代码
  6. redis 支持 json_项目开发中如何使用redis-dump进行Redis数据库合并?
  7. matlab篮球队需要五名队员,MATLAB应用与数学欣赏.doc
  8. linux telnet远程登录工具,Linux 远程登录(telnet ssh)
  9. 虚拟机的联网模式正确的选择
  10. matlab单元刚度矩阵整合成整刚,求结构总刚矩阵Matlab源代码