如何阅读C/C++中的声明



如何阅读CC++中的声明:

声明,可能是C++中最为常见的语句了。例如:

int x;

这样的声明自然是简单易懂,但如果是一个复杂结构的声明,可就没有这么直观了。

例如: 
//你能认到第几个?
int a;					//整型变量 : a
int a10[10];				//大小为10的整型数组 : a10
int(&ra10)[10] = a10;			//对大小为10的整型数组的引用:ra10
int(&frra10(int, int))[10];		//返回一个 对大小为10的整型数组的引用 的接受两个整型作为参数的函数frra10

本文就来这些复杂的符号游戏内在的逻辑与规律:如何阅读CC++中的声明?

答案很简单:解方程


一些定义

首先需要明确讨论中用到的定义或概念:

声明(declaration):将一个名字与一个类型关联起来的过程。

名字(name):        名字可以是标识符,比如x,也可以是一个表达式,比如x.y

类型(type):        类型决定了一个名字所指称的实体

举个栗子:int x;这条语句就将x这个名字与int这个类型建立关联,这个过程就是声明。当然这一条语句本质上除了声明还有其他的作用,比如存储分配。声明只是告诉编译器有这么一个名字,仅此而已。顺带一提,定义在下图中,就是为变量赋初值的过程。

咳咳,跑偏了,再讲就变成编译原理导论了。对于这些概念有兴趣,可以参考:

http://msdn.microsoft.com/zh-cn/library/csdhb88k.aspx>

 

声明的构成

回到正题,那么一条声明又是由哪些东西所构成的呢?

四部分:描述符(modifier)   基础类型(specifier)    声明符(declarator)   可选的初始式(initializer specifiesinitializerspecifiesInitializer )

修饰符、说明符和声明符

描述符就是类似前面virtual externstatic这种东西,在此我们并不关心。

可选的初始式属于定义的一部分,也不在讨论的范畴。

我们所关心的,就是声明中的基础类型(specifier),与声明符(declarator)

如:static int (&ra10)[10] = a10;

在这个例子中static是描述符,int是基础类型,(&ra10)[10]是声明符,= a10是初始式。

x,在这里就是被声明所引入的名字。它的类型是:int[10] &,即对长度为10整型数组的引用。

 

如何推断名字的类型——一个实例

好吧,那么应该怎么看出来ra10到底是什么类型呢?很简单,就是解方程,令方程左右两边的类型分别为类描述符的类型与声明符的类型,求解声明符中的名字的类型。

左边的类型= 右面名字对应类型

在上面的例子中:

 

int = (&ra10)[10]	//假设T为名字ra10的类型,则
int = (&T)[10]		//对类型T先取别名,再索引,得到int类型 //它表达了这样一层意思:对于类型T,先取它的别名,再用下标运算符进行索引,得到的类型就是int。
int[10] = &T		//对类型T取别名,会得到一个int[10]类型
int[10] & = T		//类型T,是一个对int[10]的引用。

千万要注意:& * [], (), 这些,在声明中和在表达式中代表不同的含义的。

在表达式里,&叫做取址运算符,而在声明里,它叫做 引用声明符。他们仅仅是长的一样而已,在语义上有可类比之处,但实际功能是完全不同的,千万不要混为一谈。

 

另外,如果我们把上式中左侧的类型int 换为int*,解法依然是一样的。

int * = (&ra10)[10]
int *[10] = &ra10
int *[10] & = ra10

这样我们就知道了名字ra10的类型。

读法从左向右:

首先看到一个int,哦这是一个int

然后看到一个*,哦,不对,这是一个int*

然后又看到一个[10],哦,不对,这是一个里面放着int*的大小为10的数组。

最后看到&,哦,这也不是数组嘛,这是对数组的一个引用。

但是,聪明的人会敏锐的发现一个问题:优先级。这么多修饰符,我怎么知道先拆哪一个呢?

 

声明运算符的优先级

先说一下声明运算符:

*

指针

前缀

*const

常量指针

前缀

&

引用

前缀

[]

数组

后缀

()

函数

后缀

也就是说,这些声明运算符都是用来修饰一个名字的,这些声明运算符和名字一起构成了声明符。并且,它们去修饰名字的优先级和它们作为普通运算符的优先级是一致的!这一点非常重要。

比如

int *p[10];      
int(*p)[10];

第一个声明int *p[10],解方程,因为下标运算符[]的优先级高于间接寻址运算符*。因此解方程时,名字p和数组声明符[]之间的结合,要比p和指针声明符*之间的结合更加紧密。 按照柿子先捡软的捏的原则,先把*移到左边得到 int* = p[10],哦原来p这个数组中的每一个元素都是int* ,再把[]移到左边,得到了int* [10],读作:存放着十个整型指针的数组。

那么第二个声明依此类推:括号的优先级最高,把名字p*紧紧地绑在一起,只好先拆散(*p)[]之间的关系了。得到 int[10] = *p ,哟,p这个指针是指向一个int[10]的数组啊,也就是 p = int[10] * 读作:指向存有十个整型的数组的指针。

 

好吧,经过了以上的分析,下面我们来处理最为棘手的函数声明吧。

int(&frra10(int,int))[10];

这个声明所引入的名字是: frra10. 我起这个名字的意思是Function ReturnReference Array[10]

那么怎么分析呢?

int = (&frra10(int, int))[10]
int[10] = &frra10(int, int)		//函数调用运算符()的优先级,高于取址运算符&,所以相比联系较强的函数声明运算符(),应当先拆散联系较弱的引用声明运算符&。于是得到:
int[10] & = frra10(int, int);	//啊哈,结果出来了,这是一个函数,接受两个int作为参数,返回一个对int[10]的引用。

思考

如果我们考虑到在C#Java中所采用的统一声明模式,就会发现,C/C++的声明方式,存在一种内在的不一致性:即,声明运算符可以同时出现在类型方程的左侧或者右侧。这就意味着:对于同一个类型int *,int* x; int *x;存在两种声明方式。而对于一个更加复杂的类型,上述类型方程中的每一步,都可以作为一种合法的声明方式。对于复杂的声明来说,这样的设计极大地降低了可读性。

我认为C#Java的做法,即将类型说明符统一放在类型方程左侧,而方程右侧只放名字的做法是最好的,这也是我们解类型方程最终得到的结果,便于人类阅读。C/C++的推荐做法则恰恰与此相反,将修饰符与名字放在一侧。这个设计最初的动机可能是为了写出形如:int x,*p这样一行定义多个甚至多种类型变量的丑陋代码。除了省了几个代码文件字节(说不定还给客户省了钱)之外估计找不出什么好处了。这种设计有着很大的问题,不过让人高兴的是,C/C++提供的typedefC++11提供的尾置返回类型,一定程度上缓解了这一问题。

 

解决方案。

事实上,读复杂的声明跟吃屎一样难受。

作为一个文艺的程序员,应当不断加强自己的代码品味。利用typedef将一个复杂的类型定义成一个新的类型,方便而简洁。

注意:

typedef int[10]  ReturnType //这么写是错的

typedefint (&ReturnType)[10] //这么写才是正确的。本质还是解方程。

于是上面那个函数声明就可以写成:ReturnType frra10(int,int);了。

当然这里还是能看到类型方程的余毒。

所以C++11还有一种更为优雅的解决方案,那就是尾置返回类型:比如:

Auto frra10(int,int)-> int[10] & 这下就更清楚了。可惜适用面还是比较小。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值