1. 预处理的步骤

现在我们全面了解一下C编译器做语法解析之前的预处理步骤:

1、把第 2 节 “常量”提到过的三连符替换成相应的单字符。第 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步预处理。如果遇到宏定义则做宏展开。

我们早在第 2 节 “数组应用实例:统计随机数”就认识了预处理指示这个概念,现在给出它的严格定义。一条预处理指示由一个逻辑代码行组成,以#开头,后面跟若干个预处理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代码中只不过是一种空白字符,在做语法解析时所有空白字符都已经丢掉了。