现在我们全面了解一下C编译器做语法解析之前的预处理步骤:
1、把“常量”一节提到过的三连符替换成相应的单字符。“继续Hello World”一节还提到过,Windows平台的文本文件用\r\n
做行分隔符,而Linux平台用\n
做行分隔符,C编译器要能够处理这种差别,不管是哪种行分隔符,以下统称为换行。
2、把用\
字符续行的多行代码接成一行。例如:
#define STR "hello, "\ "world"
经过这个预处理步骤之后接成一行#define STR "hello, " "world"
。这种续行的写法要求\
后面紧跟换行,中间不能有其它空白字符。
3、把注释(不管是单行注释还是多行注释)都替换成一个空格。
4、经过以上两步之后去掉了一些换行,有的换行在续行过程中去掉了,有的换行在多行注释之中,也随着注释一起去掉了,剩下的代码行称为逻辑代码行。然后预处理器把逻辑代码行划分成Token和空白字符,这时的Token称为预处理Token,包括标识符、整数常量、浮点数常量、字符常量、字符串、运算符和其它符号。继续上面的例子,两个源代码行被接成一个逻辑代码行,然后这个逻辑代码行被划分成Token和空白字符:#
,define
,空格,STR
,空格,"hello, "
,Tab,Tab,"world"
。
在划分Token时可能会遇到歧义,例如a+++++b
这个表达式,既可以划分成a
,++
,++
,+
,b
,也可以划分成a
,++
,+
,++
,b
。C语言规定按照从前到后的顺序划分Token,每个Token都要尽可能长,所以这个表达式应该按第一种方式划分。其实按第一种方式划分Token是不合语法的,因为++
运算符的操作数必须是左值,如果a
是左值则a++
是合乎语法的,但a++
这个表达式的值就不再是左值了,所以a++
再++
就不合语法了,按第二种方式划分Token反倒是合乎语法的。即便如此,C编译器对这个表达式做词法分析时还是会按第一种方式划分Token,然后在语法和语义分析时再报错。
5、在Token中识别出预处理指示,做相应的预处理动作,如果遇到#include
预处理指示,则把相应的源文件包含进来,并对源文件做以上1-4步预处理。如果遇到宏定义则做宏展开。
我们早在“数组应用实例:统计随机数”一节就认识了预处理指示这个概念,现在给出它的严格定义。一条预处理指示由一个逻辑代码行组成,以#
开头,后面跟若干个预处理Token,在预处理指示中允许使用的空白字符只有空格和Tab。
6、找出字符常量或字符串中的转义序列,用相应的字节来替换它,比如把\n
替换成字节0x0a。
7、把相邻的字符串连接起来。继续上面的例子,如果代码中有:
printf( STR);
经过第4步处理划分成以下Token:printf
,(
,换行,Tab,STR
,)
,;
,换行。经过第5步宏展开后变成以下Token:printf
,(
,换行,Tab,"hello, "
,Tab,Tab,"world"
,)
,;
,换行。然后把相邻的字符串连接起来,变成以下Token:printf
,(
,换行,Tab,"hello, world"
,)
,;
,换行。
8、经过以上处理之后,把空白字符丢掉,把Token交给C编译器做语法解析,这时就不再是预处理Token,而称为C Token了。这里丢掉的空白字符包括空格、换行、水平Tab、垂直Tab、分页符。继续上面的例子,最后交给C编译器做语法解析的Token是:printf
,(
,"hello, world"
,)
,;
。注意,把一个预处理指示写成多行要用\
续行,因为根据定义,一条预处理指示只能由一个逻辑代码行组成,而把C代码写成多行则不需要用\
续行,因为换行在C代码中只不过是一种空白字符,在做语法解析时所有空白字符都已经丢掉了。