您的位置:首页 > 编程语言 > C语言/C++

C编译器剖析_3.3 语法分析_C语言的外部声明(1)

2015-02-05 14:15 260 查看
3.3 C语言的外部声明_ExternalDeclaration
在分析完表达式和语句之后,《K&R》附录中关于标准C的6页文法,我们就已经差不多已经理解了一半,另外的3页文法基本上都是关于声明Declaration的,与声明相关的代码在ucl\decl.c中。不少编程语言的书籍中,对声明与定义是有严格区分的,在编译器遇到定义时,需要为之分配内存;而遇到声明时,则不必为之分配内存,因为该声明对应的内存可能已在其他地方进行过分配。例如,以下代码中,”extern
int a;”被当作对变量a的声明;而”int b = 5;”则被当作定义,需要为变量b分配存储空间,并作相应初始化;”void f(void);”被当作对函数的声明,不需要占任何代码区空间;而函数g则是定义,需要在代码区中占用一片内存来存放函数代码。
extern int a;
int b = 5;
void f(void);
void g(void){
//…..
}
不过,从C标准文法的角度出发,上述“需要占内存的定义definition”或者“不占内存的声明”统统是被当作“外部声明”ExternalDeclaration来处理的。是否需要占内存,其实已经涉及“语义”。从C语言的语义出发,上述外部声明才有“是否要占内存”之分。换句话说,在语法分析阶段,我们暂时只在乎C程序在形式上是否符合C标准文法,我们在这个阶段只管“形”,而不管“含义”或“内容”。在语法分析阶段,一个预处理后的C文件就对应一个翻译单元TranslationUnit,它由若干个外部声明ExternalDeclaration构成,如图3.3.1第1415至1417行的产生式所示。



图3.3.1 ParseTranslationUnit()
图3.3.1第1428行的函数调用ReadSourceFile()用于读入预处理后的C文件,第1430和1431行用于初始化当前读头的坐标位置,第1432行创建的向量TypedefNames用于存放通过typedef关键字命名的类型名,而接下来的OverloadNames则用于记录“另作他用”的typedef类型名,我们在3.1节时举例介绍过这两个向量的用法。第1435行创建了一个kind域为NK_TranslationUnit的语法树结点,对外部声明ExternalDeclaration的分析主要是通过第1450行的函数调用ParseExternalDeclaration()来实现的。而一个翻译单元是由若干个外部声明构成的,所以我们需要第1443行的while循环。引入第1446至1449行的代码,主要是为了允许形如”;”这样的空语句。第1436行、1450行和1451行的代码则用于把各个外部声明所对应的语法树结点链接到一起。执行到第1460行,我们就已经完成了对一个预处理后的C文件的语法分析工作,通过CloseSourceFile()关闭相关文件即可。图3.3.2给出了一个预处理后的C文件及其对应的语法树示意图。



图3.3.2 翻译单元TranslationUnit对应的语法树
图3.3.2中,我们通过在while()循环中调用函数ParseExternalDeclaration(),依次创建了kind域为NK_Declaration或NK_Function的4个语法树结点,其中NK_Function对应的是structastFunction结构体对象,描述了函数定义g的相关信息;而NK_Declaration对应的是structastDeclaration结构体对象,描述了变量声明a、变量定义b和函数声明f。而用于分析“外部声明”的相关代码如图3.3.3所示。



图3.3.3 ParseExternalDeclaration()
图3.3.3第1254至1256行告诉我们,外部函数ExternalDeclaration由函数定义FunctionDefintion和声明Declaration这两个候选式构成。由第1258至1262行,我们可以发现,这两个候选式有完全一样的前缀DeclarationSpecifiers,即”声明说明符”。以下代码中,static、const和int都被称为声明说明符DeclarationSpecifier。语法上,声明说明符DeclarationSpecifier可分为存储类说明符、类型限定符和类型说明符这3类。
static const int a1 = 3, * a2, a3[5],a4(void); //对应图3.3.3第1261行的Declaration
static void a5(void){ //对应图3.3.3第1258行的FunctionDefinition

}

(1)关键字static被称为存储类说明符StorageClassSpecifier,与之类似的还有auto、register和extern,这些说明符暗示了变量的存储位置,例如auto表明变量是“自动变量”,所有局部变量对应的栈空间是在函数返回后自动回收的,不过,在编写C程序中,我们没有必要显式地把局部变量声明为auto;而关键字register则建议C编译器尽量把局部变量或形参往寄存器中存放,只是建议而已,实际上C程序员也很少需要显式地使用关键字register。现代的C编译器在优化方面下足了功夫,总是想方设法地想重用寄存器中的值,因此,保留register关键字更多的是为了兼容旧代码。
(2)关键字const则被称为类型限定符TypeQualifier,与之地位相似的还有volatile。
(3)关键字int则被称为类型说明符TypeSpecifier,与之类似的还有void、char、short、float、double和结构体说明符等。通过结构体说明符,C程序员可以定义结构体等用户自定义的类型。由此我们也能看到,在语法上,用户自定义结构体类型与C语言内置的int等类型的地位是相当的。
而跟在声明说明符”static const int”之后的,则是由若干个“有初始化或者无初始化的声明符Declarator”构成的“声明符列表DeclaratorList”,例如上述的”a1 = 3”,”*a2”、”a3[5]”和”a4(void)”。其中的”a1=3”是带初始化的声明符,在文法上被称为“初始化声明符”InitDeclarator。声明符Declarator的概念实际上,我们在1.3节时就介绍过。在形如*a2的声明符中,我们声明了指针类型,在UCC内部,*a2会对应一个kind域为NK_PointerDeclarator的语法树结点;而在形如a3[5]的声明符中,我们声明了数组类型,在语法树上,a3[5]会对应一个kind域为NK_ArrayDeclarator的结点;而在形如a4(void)的声明符中,我们声明了函数类型,与之对应的是一个kind域为NK_FunctionDeclarator的语法树结点。我们结合具体的C程序,介绍了C标准文法中出现的DeclarationSpecifier和Delclarator等非终结符的含义,由以上分析,我们也能发现,C语言的类型信息就是由声明说明符DeclarationSpecifier和声明符Declarator这两者构成。C语言中的int等基本类型、结构体struct、联合体union和枚举enum会出现在DeclarationSpecifier中;而数组、指针和函数类型则是由Declarator来生成。

存储类说明符、类型限定符和类型说明符等概念一开始接触时,似乎不太容易记住,但至少我们可以有一个大的概念,那就是这些都是声明说明符,然后我们记住有形如”static const int ”这样的声明说明符就OK了,接下来我们的大脑就很容易想到与static地位相同的有哪些,与const地位相当的有哪些,而与int类似的又有哪些。就有了这些概念后,我们再来看图3.3.3第1259行,我们看到在函数定义FunctionDefinition中出现的只是Declarator,而非第1262行的InitDeclaratorList,而Declarator只描述了一个不带初始化的声明符,通过InitDeclaratorList我们才能描述多个“有初始化或无初始化的声明符”。因此,以下用于函数定义的代码则是非法的,其中出现了a6和a7两个声明符,且a6还进行了非法的初始化。
void a6(void)=3, a7(void){
}
由于Declaration和FunctionDefinition有共同的前缀,UCC编译器为了简化语法分析的工作,就把出现在FunctionDefintion中的Declarator也当作InitDeclaratorList来处理,这当然不够精确,但我们会在后面进行修正。这样处理后,这两者的公共前缀就如下所示,下标opt表示该“非终结符”是可有可无的。
DeclarationSpecifiersopt InitDeclaratorListopt
图3.3.3第1289行通过调用ParseCommonHeader()函数,完成了这部分公共前缀的分析。此时我们会误把” void a6(void)=3, a7(void)”当作合法的输入。第1297行用来判断一下在公共前缀的存储类说明符中,是否出现过typedef。如下代码所示,在语法上,static和typedef的地位相当,所以C标准文法把typedef关键字也当作一种存储类说明符来进行分析,当然在语义上,typedef和static是风马牛不相及。按C的文法,如果遇到了typedef关键字,我们可以百分之百确定这不是一个函数定义。
typedef const int ABC;
static const int a8;
对于输入” static const int a1= 3, * a2, a3[5],a4(void);”,我们期望通过函数调用ParseCommonHeader()能为之构造一棵形如图3.3.4的语法树。



图3.3.4 声明Declaration所对应的语法树
在图3.3.4中,在kind域为NK_Declaration的语法树结点记录了上述声明的相关信息,主要有这么两部分,一部分由是由specs域所指向的声明说明符相关信息,另一部分是initDecs所指向的初始化声明符链表,该链表由4个kind域为NK_InitDeclarator的语法树结点构成。在kind为NK_InitDeclarator的结点上,由dec域指向其声明符,而由init域指向其初始化值,若不存在初值,则init域为NULL。
如果我们要分析的正是一个声明Declaration,则通过图3.3.3第1289行的ParseCommonHeader()函数调用,我们就构成了形如图3.3.4的语法树,就已经基本完成了一个声明的分析工作,但如果要分析的是一个函数定义,则需要作进一步的修正。首先我们需要检查一下所构造的语法树上,是否存在kind域为NK_FunctionDeclarator的结点,如图3.3.4所示,图3.3.3第1300行调用函数GetFunctionDeclarator(initDec)完成了这个检查,该函数对应的代码如图3.3.5所示,实参initDec指向图3.3.4中的由NK_InitDeclarator结点构成的单链表。



图3.3.5 GetFunctionDeclarator()
因为函数定义FunctionDefintion的产生式中只允许出现一个声明符Declarator,且不带初化化值,所以由kind域为NK_InitDeclarator结点构成的链表只能包含一个结点,图3.3.5第1232行的if语句按这个要求进行了判断。接着我们想沿该NK_InitDeclarator结点的dec域去查找,看看有没有kind域为NK_FunctionDeclarator的结点,且该结点的后继应该是一个NK_NameDeclarator结点,如图3.3.4所示。图3.3.5第1243至1249行的while循环实现了这个查找。第1236至1241行的注释举了一个具体的例子,用来说明为什么我们仅仅去查找kind域为NK_FuctionDeclarator的结点是不够的,而要再判断一下该结点的后继结点是否对应一个函数名,即NK_NameDeclarator结点。如果我们找不到满足条件的NK_NameDeclarator结点,则返回NULL,这说明我们刚刚遇到的肯定不是一个函数定义;否则返回该结点的地址,但这只能说明我们可能找到了一个函数定义,也可能这个结点只对应一个函数声明。这还需要再往前分析,看看接下来由词法分析器返回的token是不是左括号,如果是左括号,则说明我们遇到了函数体,此时我们可以确定我们正在分析的是一个函数定义,而非函数声明。图3.3.3只给出了函数ParseExternalDeclaration()的部分代码,我们删去了该函数中的一些注释后,图3.3.6给出了ParseExternalDeclaration()的其余代码,这部分代码主要是用于处理函数定义FunctionDefinition的。



图3.3.6 FunctionDefinition
对照着如下所示的函数定义FunctionDefinition的产生式,我们可以较好地理解图3.3.6中的代码。通过ParseCommonHeader()和GetFunctionDeclarator()这两个函数,我们实际上已经分析了产生式中的“declaration-specifiers declarator”。
function-definition:
declaration-specifiers declarator [declaration-list]compound-statement
接下来的是一个可选的声明列表DeclarationList,这部分主要是用于如下所示的旧式风格函数定义,其中的” int a;double b,c;”就是个可选的声明列表,图3.3.6的第23至27行完成了对此的分析。不论是新式风格,还是旧式风格的函数定义,其函数体都是一个以左大括号开头的复合语句,第29行调用ParseCompoundStatement()完成了对函数体的分析。
void fo(a,b,c) int a;double b,c;{

}

我们在2.4节介绍C语言的类型系统中介绍过,尽量远离旧式风格的C函数,只是作为C编译器,我们需要兼容已有的代码,所以还要不厌其烦地面对这些历史包袱。如果是如下所示的一个新式风格函数定义,通过ParseCommonHeader()我们就已经分析了” voidfn(int aa,double bb,double cc)”,接下来的就是应是左括号开头的函数体了。因为通过typedef我们可以命名一些类型,但是这些类型名可能和fn中的形参aa、bb和cc重名,我们需要把这些信息记录在向量TypedefNames和OverloadNames中,图3.3.6第16至22行的while循环通过调用函数CheckTypedefName()完成了这个工作。而图3.3.6第12行创建的struct
astFunction对象则用于记录与函数定义相关的所有信息。
int * fn(int aa,double bb,double cc){
return NULL;

}

图3.3.7给出了与函数fn所对应的语法树,结合这棵语法树,我们图3.3.6第1至31行对函数定义的分析就一目了然。



图3.3.7 函数定义所对应的语法树
在图3.3.7的kind域为NK_Function的结构体structastFunction对象中,记录了与函数定义fn()相关的所有信息,该对象的specs用来存放声明说明符,而dec域存放了如下所示的单链表,而fdec域则指向该链表中kind域为NK_FunctionDeclarator且后继结点为NK_NameDeclarator的结点,stmt域则指向函数体对应的复合语句。

astPointerDeclarator -- > astFunctionDeclarator -- > astDeclarator(fn)

如果逆序遍历(按从右至左)这条单链表,结合在图3.3.7的astSpecifiers对象中存放的类型说明符int,我们依次可以得到这样的类型信息

(1) 访问astDeclarator对象得到函数名fn
(2) 访问astFunctionDeclarator对象得到: fn is a function
(3) 访问astPointerDeclarator对象得到:fn is a function which returns pointer to

(4) 访问astSpecifiers对象得到:fn is a function which returns pointer to int

实际上,我们在第1.3节讨论ucc\examples\sc的声明符Declarator时,遇到过类似的单
链表。UCC在后续阶段会对这条单链表进行处理,最终通过函数DeriveType()导出fn的类型信息,并记录在“第2.4节 C语言类型系统”中介绍过的struct functionType对象中。而函数DeriveType()的代码,我们会在语义检查时再进行讨论。而函数fn的形参列表,则存放在图3.3.7中的astParameterTypeList对象中,通过astFunctionDeclarator对象的paramTyList域可找到该对象。而在kind域为NK_ParameterTypeList的语法树结点中,其ellipsis域为0表示fn不是一个变参函数,我们知道ellipsis是省略号的意思;其paramDecls域则指向了由若干个astParameterDeclaration对象构成的链表,每个astParameterDeclaration对象可用于记录一个形参信息,如图3.3.7所示。对比用于描述局部变量或全局变量声明的astDeclaration结构体,可以发现以下两个结构体几乎是一样的。
struct astDeclaration{

AST_NODE_COMMON

AstSpecifiers specs;

AstNodeinitDecs;

};
typedef struct astParameterDeclaration{
AST_NODE_COMMON
AstSpecifiersspecs;
AstDeclaratordec;
} *AstParameterDeclaration;

它们的差别在于C语言的形参声明不可以有初始化值,例如以下函数定义fe是非法的,而局部变量b和全局变量c的声明可以有初始化值。带初始化常量值的形参,在C++中被当作默认参数,但C语言视之为非法定义。所以,合不合法,还是制定规则的人说了算:)

void fe(int a =3){
int b = 4;
}
int c = 5;

当发现通过ParseCommonHeader()分析的只是一个声明Declaration,而不是函数定义时,我们会执行图3.3.6第32行的代码,此处要做的主要工作仍然是检查一下在声明中,各个声明符Declarator的名称是否与typedef定义的类型名重名,这通过调用PreCheckTypedef()函数来实现,其对应的代码如图3.3.8所示,结合图3.3.4所示的数据结构,我们不难理解第236至241行的代码。



图3.3.8 PreCheckTypedef()
对于输入” static const int a1= 3, * a2, a3[5],a4(void);”而言,通过第261行的GetOutermostID()函数调用,我们可以获得各个声明符的名称,例如此处的a1,a2,a3和a4。然后通过调用CheckTypedefNam()函数检查一下这些名称是否与typedef定义的类型名重名。CheckTypedefName()的代码已在3.2节讨论过。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: