如何分析c和cpp的声明

主要素材是c99的文档,有一些cpp的内容但不是很全面,cpp的类型
声明和c的相比还是多了很多东西的,名字空间(namespace)、静态多
态类型(template)等都带来很多不同于c的新的课题,理解不是很深,
所以只好舍弃了。

从一个问题开始:怎么分析
typedef int (*PF) (const char *, const char *); 
PF Register(PF pf);  

int (*Register (int (*pf)(const char *, const char *)))
(const char *, const char *);
是相同的声明。

1. 类型、类型检查和类型描述

说这两个是相同的声明的一种准确解释是两者具有相同的`类型
描述符'(type descriptor),类型描述是编译器用来表示标识符以及
表达式的类型的数据结构。

cpp的typeinfo可以理解为类型描述符的一种具体形式,c不支持类型
描述,虽然编译器里必须保存这个信息,但并没有提供给程序员。

这里刚好可以用typeinfo来检验题目中的两个声明类型上是等价的:

// 文件:d.cpp,用vc编译
#include <stdio.h>
#include <typeinfo>

typedef int (*PF) (const char *, const char *);
PF Register_1(PF pf);

int (*Register_2 (int (*pf)(const char *, const char *)))
(const char *, const char *);

int main()
{
        printf("typeof(Register_1) is --/n%s/n", typeid(Register_1).name());
        printf("typeof(Register_2) is --/n%s/n", typeid(Register_2).name());
        return 0;
}

gcc的typeof扩展使得程序员可以间接的使用类型描述:

int (*PF) (const char *, const char *); 
typeof(PF) (*Register)(typeof(PF));

有点背景知识是,c++里面,每个标识符、表达式都有一个类型,
编译器需要知道这个类型才能执行类型检查,类型检查的含义很
丰富,通俗的说,编译器发现:

int a;
char *b;
a=b;

会报错(vc 7.1):

c.cpp(5) : error C2440: “=” : 无法从“char *”转换为“int”
        该转换要求 reinterpret_cast、C 样式转换或函数类型转换
        
这个错误就是编译的类型检查阶段发出的,因为编译器能够发现
标识符a和表达式b的类型不匹配,不能执行赋值。很明显,编译
器需要一个数据结构来存储,这个数据结构就是所谓的类型描述
的具体化,理解这个数据结构的作用和编码方式对于理解c的类型
声明是很有帮助的。

2. 类型和语法的关系

首先容易理解,类型最终来源于标识符的声明(declaration, 包括
定义即definition,定义是一种特殊的声明),因为表达式的类型
是根据其中包含的标识符和操作符(函数调用可以看作一种用户
自定义的操作符)来确定的。所以,说到底分析类型要从分析声
明语句开始。

类型,作为标识符和表达式的一个重要属性,和语法结构的其它
属性一样,完全是由程序的静态内容,主要是语法,决定的(有的
语言的类型是在运行时确定的,不过c和cpp都不是这样)。实际上,
c和cpp的有些声明之所以难以分析,主要是语法上的困难,pascal
的声明语法简单的多,相应的,很少有来自pascal程序员的类似抱怨。

3. c的声明

3.1 基本类型

c的类型有两种:基本类型(fundamental type)、导出类型(derived
type)。基本类型包括各种数值类型、void等,基本类型的声明很
简单:

int a; /*读作:type of identifier `a' is `int' */
 float b; /*读作:type of identifier `b' is `float' */
 _Bool c; /*读作:type of identifier `c' is `_Bool' */
 _Complex d;/*读作:type of identifier `d' is `_Complex' */
 
等都是基本类型声明。_Bool/_Complex是一个c99中提到的标准类
型,不过我只看到gcc支持。注释中提到的`读作...',是为了便于和
下面分析导出类型的方法一致。

上述几个类型声明的语法可以写成EBNF的形式,这是我们得到的
最简单的类型声明,后面几个例子会稍微复杂一些,

declaration : type-specifier identifier

意思是一个declaration可以由一个type-specifier后面跟个identifier
组成。常用的extern、static、typedef、auto、register等称为storage 
class specifier,c的语法规定它们可以和type-specifier出现在同样的
位置。即:

extern int a;
static int b;

等,这样得到的语法实际上是:

declaration : declaration-specifiers identifier
declaration-specifiers : (type-specifier | storage-class-specifier)*

第二个语法表达式中,`|'表示选择,即type-specifier、storage-
class-specifier都可以出现在这个位置,后面的*表示之前的语法
成分可以重复出现0到n次,看上去有些奇怪,但这的确是c99
类型声明语法的一部分。眼尖的话,应该能看出上面居然允许
一个类型声明中出现多个type-specifier,也就是说:

int double char a;

居然在语法上是正确的(当然语法上正确并不保证程序是正确的,
类型检查会出错),我的理解是这是为了简化语法分析程序的设计,
考虑到``long int''等类型声明(correct me?)。更奇怪的是,下面
居然是一个合法的类型声明,可以用gcc编译通过:

int int double char float;

只有一个警告,呵呵,跑题了,不过我想这个例子说明了精确的
定义实际一个可用的语言是件很辛苦的事情。

在declaration-specifier位置上还可以摆放一个标准的东西,即
const/volatile,通常称为`cv-qualifier'。

vc的编译器的扩展__declspec(naked/dllexport/dllimport)等等,
在语法上和上述type-spec/storage-class-specc是处于同等位置的,
我们从微软为这个语法元素取的名字上就能(事后诸葛亮的)猜出来。

c和cpp实际上还允许这样的声明:

int a=5;

也就是声明的时候指定变量的初始值,这个语法并不是很难理解,
而且初始化的部分不影响标识符的类型。考虑到这个因素,声明
的语法实际上要作如下改造:
declaration : declaration-specifiers init-declarator
declaration-specifiers : (type-specifier | storage-class-specifier)*
init-declarator : declarator | declarator '=' initializer

declarator是个有点抽象的东西,用来表示在类型构造中间阶段的
语法结构,cpp draft 96专门花了一章来讲述这个,上面

int a;

中`a'就是一个declarator,实际上就是由于这个东西使得c和cpp
的类型声明有时候变的很难懂,不像pascal,所有的类型声明都
是:

identifier : type-name ';'

这样的形式。declarator麻烦的地方在于它可以通过组合的方式
变的很复杂,见下面关于derived type的部分。另外一个麻烦的
地方在于ansi-c和cpp在函数的参数列表里允许使用所谓abstract
declarator,也就是不出现identifier的类型声明:

int f(int /* an abstract-declarator in which the identifier is ommited*/);

多说一句,实际上可以在一个声明里说明多个标识符,所以上面
init-declarator其实应该是init-declarator-list,不过这并不是很关键。

现在可以考虑如何用什么数据结构编码基本类型的描述符了,显然,
基本类型的描述符可以用一个枚举常数表示:

enum basic_type{
        int_type,
        float_type,
        ...
};

3.2 导出类型

3.2.1 简单导出类型

导出类型在cpp的文档里称为compound type(组合类型),这个概念
是递归定义的,是基本类型和组合类型通过类型组合得到的类型。
组合有几种形式:数组、函数、指针、引用、类、联合、枚举,
这些称为type constructor,可以不是很恰当的翻译为`类型构造子'。

最容易造成分析困难的组合是数组、指针和函数,尤其是这些组合
混合运用的时候。三者的语法分别可以简写为:

declarator:
        | pointer declarator /* 指针声明的语法 */
        | declarator '[' constant_expr ']' /* 数组声明的语法 */
        | declarator '(' parameter_identifier_list ')' /* 函数声明的语法 */

上面有意忽略了一些情况,例如不说明长度的数组`int a[]',带abstract
declarator的参数列表的函数:`int f(int,float)'等,也没有说明指针和数组、
函数声明的优先级。实际上指针声明的优先级比函数、数组的优先级低,
所以要声明指向数组、函数的指针的时候,需要用括号把指针声明括起来:

int (*f)(int a);

首先就最简单的情况来看,关于:

int *a;

的含义,显然是:

typeof(a)=POINTER(INT)--------(1)

POINTER(INT)的含义希望可以不用解释,就像pascal的`^int'一样,这种
表示其实和指针类型的类型描述已经很接近了,大家可以参考一下gcc
的文档中表示types的数据结构的介绍。

也可以把这个理解分为两步:

typeof(*a)=int---------(2.1)

利用一个不难理解的等价变换(可以看作公理):

typeof(*xxx)=yyy ==> typeof(xxx)=POINTER(yyy) ----------(公理1)

得到:

typeof(a)=POINTER(int)------(2.2)

也就是说,把类型声明看成一个方程,typeof(a)是个要求的未知数,利用
一些等价变换的关系得到。看上去似乎有些多余,马上可以看到,后面这
种方法的好处是在多个type constructor的时候不容易想错。上面的公理1
是个重点,类似的还有:

typeof(xxx[yyy])=zzz; => 
typeof(xxx) = ARRAY(zzz); -----(公理2)


typeof(xxx(t1 a, t2 b))=yyy; => 
typeof(xxx)=FUNCTION(yyy; t1, t2); ------(公理3)

其中ARRAY(zzz)表示长度为zzz的数组,FUNCTION(yyy;t1,t2)表示返回值为
yyy类型,参数依次为t1、t2类型的函数,zzz/yyy/t1/t2都是类型变量。

3.2.2 复杂导出类型

多个type constructor的时候,困难在于声明的语法。看这个之前,
我觉得我们可以参考一下数学表达式的分析:

5+3  ---------(1)
3+3*6 -------(2)
b=3, a = 1 ? 3<<4+-3 : c = ~a++ | 2; --------- (3)

(3)显然不是那种一眼就能知道结果的表达式,虽然估计不会有人写出
那样风格的代码来,仔细观察一下可以发现,(3)的分析困难来源于:
*) 前缀、后缀、中缀、distfix运算符的混用
*) 一元、二元、三元运算符的混用
*) `-'号同时作为前缀操作符和中缀操作符

其中`distfix运算符'是指那些操作符散布在操作数当中的情况,c的
` ?  : ' 就是典型的distfix运算符。

如果把类型声明也看成是表达式的话,复杂类型声明的分析困难
实际上和这个很类似。指针、引用的标志`*'、`&'相当于一元前缀
运算符,数组和函数的标志`['、`]'、`('、`)'相当于后缀运算符。
而`('、`)'既用来界定函数参数列表,又用来override优先级限制。
还好,倒是没有distfix的运算符。

和算术表达式类似,优先级在分析类型声明的时候也起到了很大
的作用。c语法规定,在类型声明表达式中,指针和引用的优先级
低,数组、函数以及表示override优先级的`('和`)'的优先级是同样
的。
所以:
int * f[5];

int *(f[5]);
等价,表示f是个数组,其元素是int指针,而若想表示指向int数组
的指针,则必须用括号来override这个优先级,也就是这么写:
int (*f)[5];

下面的cpp程序可以验证这一点:
// c.cpp, 用vc编译
#include <typeinfo>
#include <stdio.h>
int main()
{
        int *f[5];
        int *(g[5]);
        int (*h)[5];
        printf("%s/n", typeid(f).name());
        printf("%s/n", typeid(g).name());
        printf("%s/n", typeid(h).name());
        return 0;
}

类似的:
int * f ();

int * (f());
等价,而和
int (*f)();
则不等价。

btw,把类型声明看成表达式的观点还可以和类型描述的概念很
好的结合起来:编译器求值这个表达式,得到一个类型描述,正
如人或者程序求值算术表达式得到一个数值结果一样。但是如上
所述,一个重要的区别是,类型声明表达式的分析需要类似解方
程的技巧。

继续说语法,形式语言的理论和实践已经形成很多有效的算法来
作这件事情,但对程序员来说,那些算法都有些枯燥,实际上更
多的还是靠直觉和试错。这里有个好消息是,c和cpp的语法都是
基本没有二义性的,也就是说除了极少的几个情况,只要我们能
分析出类型声明的一种语法结构,那么就可以放心的使用这个结
构来解释类型声明的含义,c标准委员会的专家们会保证不会有第
二种解释。就像下面的分析表现的那样,直觉、试错的缺点就是
不严格,但是多加练习、熟练之后就很有效了(试想一下,有哪
个小学毕业生不会凭直觉分析复杂的数学表达式?)

拿:

int (*Register (int (*pf)(const char *, const char *)))
(const char *, const char *);

作例子,首先把前面的`int'拿掉,因为显然这是一个type-specifier,
把后面的declarator部分记为:declarator-1,即:

declarator-1=(*Register (int (*pf)(const char *, const char *))) /
(const char *, const char *)

现在我们已经可以写出:

typeof(declarator-1)=INT ------------------------(1)

一旦我们分析出declarator-1的内部的结构,我们就可以按照前面的三个
公理来改写这个式子并最终得到整个类型声明的含义。ok,现在开始分
析declarator-1,很明显,declarator-1由两段括号括起来的部分组成,
先看末尾的一串括号括起来的东西:

(const char *, const char *)

我们意识到这里要出现一个function type constructor(不然还能是什么?),
令declarator-2表示剩下的部分,即:

declarator-2=(*Register (int (*pf)(const char *, const char *)))

结合1,由于:
declarator-1=declarator-2 (const char *, const char *)

所以现在我们可以写出:

typeof(declarator-2 (const char *, const char *))=INT

于是根据公理3,我们有:

typeof(declarator-2)=FUNCTION(INT; const char *, const char *); ----(2)

意思是declarator-2的类型是返回值为int,带两个类型为const char *
类型的参数的函数。其中`const char *'也许写成`POINTER((CONST(CHAR)))'
更合乎前面的习惯一些。

继续,declarator-2外头有一对括号,这个可不是表示函数类型构造,
而是保护Register前面的`*'的,按照c的语法的定义,分析到这个地步,
这样的括号是可以扔掉的。现在类型声明所要定义的标识符已经暴露
出来了,就是`Register',它前面是pointer,后面的一对括号表示函数
构造,即:`* A (B)'的形式,这里优先级就要起作用了,参看前面的例
子,因为函数构造的优先级高,所以函数声明部分先和Register结合,
再和`*'结合,令:

declarator-3=Register (int (*pf)(const char *, const char *))

这样:

declarator-2=( * declarator-3  )

根据(2),以及公理(1),有:

typeof(declarator-3)=POINTER(FUNCTION(int; const char *, const char *))

这样太罗嗦了,我们把等式右面的东西简写为PF,即:

PF=POINTER(FUNCTION(int; const char *, const char *))
typeof(declarator-3)=PF

再来分析declarator-3的结构,很简单,是个函数声明

typeof(Register (int (*pf)(const char *, const char *)))=PF

根据公理(3):

typeof(Register)=FUNCTION(PF; typeof(int (*pf)(const char *, const char *))));

而:typeof(int (*pf)(const char *, const char *)))这个类型表达式可以
按照和刚才完全一样的思路分析,巧了,刚好也等于已经分析过的PF:

typeof(int (*pf)(const char *, const char *)))=PF

这样,最后的结果就出来了:

typeof(Register)=FUNCTION(PF; PF)

4. 总结

和pascal的类型声明相比,c的类型声明的语法的确复杂了一些,甚至有点
让人疑心是完全没必要的复杂,尤其是考虑到可以通过引入typedef(类似
pascal的type equivalence语句)简化类型声明。不过为了阅读现有代码,
还是要适应这种情况的。

c的类型声明的难点在于类型构造的组合应用的情形,对这种情况,只要
成功的分析出其语法结构,就可以按照一种机械的方式翻译出所声明的
类型。语法分析的方法类似算术表达式的分析,需要考虑不同类型构造
的优先级。 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值