现在的位置: 首页 > 综合 > 正文

16 预处理指令

2013年05月23日 ⁄ 综合 ⁄ 共 11913字 ⁄ 字号 评论关闭
 

16 预处理指令
1、预处理指令由预处理标记序列组成。序列的首标记为#预处理标记,它或者为源文件中的第一个字符(可位于不包含换行符的空白之后),或者位于至少包含一个换行符的空白之后。序列的最后一个标记是位于序列首标记之后的第一个换行符(因此,预处理指令通常被称为“行”。这些“行”没有其他的句法意义,在预处理中,除了某些情况之外(例如,参见16.3.2的字符串文字创建操作符#),所有的空白都是等价的)
preprocessing-file:
groupopt
 
group:
group-part
group group-part
 
group-part:
pp-tokensopt new-line
if-section
control-line
 
if-section:
if-group elif-groupsopt else-groupopt endif-line
 
if-group:
# if constant-expression new-line groupopt
# ifdef identifier new-line groupopt
# ifndef identifier new-line groupopt
 
elif-groups:
elif-group
elif-groups elif-group
 
elif-group:
# elif constant-expression new-line groupopt
 
else-group:
# else new-line groupopt
 
endif-line:
# endif new-line
 
control-line:
# include pp-tokens new-line
# define identifier replacement-list new-line
# define identifier lparen identifier-listopt ) replacement-list new-line
# undef identifier new-line
# line pp-tokens new-line
# error pp-tokensopt new-line
# pragma pp-tokensopt new-line
# new-line
 
lparen:
the left-parenthesis character without preceding white-space
 
replacement-list:
pp-tokensopt
 
pp-tokens:
preprocessing-token
pp-tokens preprocessing-token
 
new-line:
the new-line character
2、位于预处理指令的预处理标记之间(从#预处理标记之后到终止换行符之前)的空白字符只能是空格和水平制表符(包括在翻译步骤3中替换了注释或其它空白字符的空格)。[例如:
       # /**/ include “test.h”
       /**/#include “test.h”
       #include /**/ “test.h”
上述3条预处理指令均正确,不过vc6的词法高亮显示模块无法正常识别第1条预处理指令,但编译器还是能正常识别。]
3、实现可以有条件地处理或跳过源文件的某部分、包含其它的源文件或进行宏替换。这些机制称为预处理,因为从概念上讲它们发生在对最终的翻译单元进行翻译之前。
4、预处理指令中的预处理标记不进行宏展开,除非另作说明(见16.14款:组成条件包含指令中的常量表达式的预处理标记需要进行宏展开;见16.24款:组成包含指令参量的预处理标记也需要进行宏展开)
16.1 条件包含
1、控制条件包含的表达式必须是一个整型常量表达式,但要求:不能包含类型转换操作;标识符(包括那些词法上等同于关键字的标识符)按下文将描述的方式进行解释(由于控制常量表达式在翻译的第4阶段进行计算,此时所有的标识符只存在宏名和非宏名之分,还不能识别关键字、枚举常量等特殊的标识符);可为如下形式的一元操作符表达式(把defined看作一元操作符)
defined identifier
defined ( identifier )
如果标识符当前是一有效的宏名字(也就是说,如果标识符被预先定义,或标识符曾被预处理指令#define定义且中间不曾被#undef指令取消定义),上述一元表达式的结果为1,否则为0。
2、在所有宏替换进行之后,每个预处理标记在词法上应是一合法的标记( 2.6)。
3、以下形式的预处理指令
# if constant-expression new-line groupopt
# elif constant-expression new-line groupopt
检测控制常量表达式的结果是否为0。
4、在计算控制常量表达式之前,需要像处理普通文本一样,对组成表达式的预处理标记中的宏调用进行替换(除了那些被一元操作符defined修饰的宏名字)。如果上述替换过程生成了defined标记,或在宏替换之前一元操作符defined的使用不符合1中指定的两种形式,那么程序的行为是未定义的。在执行完所有的宏替换和一元操作符defined之后,余下的标识符和关键字(候选标记(2.5)不属于标识符,尽管其拼写全部为字母和下划线,因此它不参与替换),除true和false之外,统统被替换为预处理数字0,接下来将每一个预处理标记转换为标记。根据5.19的规则对结果标记集合构成的控制常量表达式进行算术运算,运算所用值域不能小于18.2中指定的范围,此外,运算过程将int和unsigned int类型分别看成具有同long和unsigned long相同的表示。在控制常量表达式包含字符文字时,需对其进行解释,并把转义序列转换为执行字符集的成员。标准并不强制要求预处理器对控制常量表达式中的字符文字的处理与编译器对一般常量表达式中的字符文字(不出现在#if和#elif指令中)的处理一致。因此对于相同的字符文字,预处理器和编译器可能得出不同的数值结果。同样,单字符字符文字是否能取负值也是由实现定义的。在处理每一个类型为bool的子表达式之前需对其进行整型提升。
5、具如下形式的预处理指令
# ifdef identifier new-line groupopt
# ifndef identifier new-line groupopt
检测标识符当前是否是一个有效的宏名字。它们的条件部分分别等价于#if defined identifier和#if !defined identifier
6、每个指令的条件按顺序进行检测。如果条件的计算结果为false(零),其所控制的分组将被忽略:不过为了跟踪嵌套条件的层级,还是需要对该分组中的预处理指令进行处理,但只要识别出能确定指令类型的指令名字即可,对于指令中余下的预处理标记,将同分组中的其它预处理标记一样被忽略。只对第一个控制条件结果为true(非零)的分组进行处理。如果所有控制条件的计算结果均为false,那么当存在一个#else指令,对#else所属的分组进行处理;否则,跳过#endif指令之前的所有分组。(从句法结构可看出,在指令#else#endif之后,结束换行符之前,不允许出现预处理标记。然而,注释可以出现在源文件中的任何地方,包括在预处理指令中)
16.2 源文件包含
1、#include指令必须识别出能被编译器处理的头或源文件。
2、具如下形式的指令
# include <h-char-sequence> new-line
对一系列由实现定义的目录位置进行搜索,以定位被<和>间的字符序列唯一标识的头文件,并将该指令替换为头文件的内容。目录位置如何指定以及头文件如何标识都是由实现定义的。
3、具如下形式的指令
# include "q-char-sequence" new-line
将指令自身替换为被"间的字符序列标识的源文件内容。命名源文件按实现定义的方式进行搜索。如果此类搜索不被支持或失败,那么按如下形式对指令重新进行处理:
# include <h-char-sequence> new-line
此处的h-char-sequence(可能包含>字符)等同于q-char-sequence
注:一种常见的源文件包含实现模型如下:
当用户使用2中的指令形式时,如果h-char-sequence为路径限定文件名,在由其确定的位置搜索文件;如果h-char-sequence为没有路径限定的文件名,编译器在其预定义的位置搜索由h-char-sequence标识的头文件。预定义的位置通常是编译器运行时库的头文件位置,预定义位置可以有多个,且可由用户设置。
当用户使用3中的指令形式时,如果q-char-sequence为路径限定文件名,同上进行处理;如果q-char-sequence为没有路径限定的文件名,编译器首先尝试在当前被编译文件所在的目录搜索由q-char-sequence标识的头文件,如果搜索失败,则继续在预定义的位置进行搜索(即按2中的指令形式处理)。
4、形式为
# include pp-tokens new-line
的预处理指令(与前面两类源文件包含指令的形式不同)是允许的。按普通文本的形式对include之后的预处理标记进行处理(对其中的每一个宏名字进行替换)。如果在所有的替换之后,指令的形式仍不匹配前两类源文件包含指令形式(注意:此时相邻的字符串文字并不会被连接成单个字符串文字(参见2.1中的翻译步骤,预处理指令和宏替换在步骤4中执行,而字符串文字连接在步骤6中执行);因此,宏展开结果为多个字符串文字的指令是非法的),程序的行为是未定义的。如何将<和>或双引号字符"之间的预处理记号序列转换为单个的头名字预处理记号是由实现定义的。[注:此处有一种特殊的情况需要处理,代码如下:
#define HEADER <test.h”>
#include HEADER
]
5、从被<和>或双引号限定的字符序列到外部源文件名字的映射是由实现定义的。对于由单个或多个nondigits(2.10)组成,并后跟一个点号(.)和单个nondigits的字符序列,实现将提供唯一的映射(个人认为上述文字的意思是实现必须为单字符后缀名的文件提供唯一映射)。实现可以忽略大小写字母的区别。
6、#include预处理指令可出现在被另一个文件的#include指令包含的源文件中,源文件嵌套包含的层级限制由编译器定义。
7、例如:#include预处理指令的最常使用方式如下:
#include <stdio.h>
#include "myprog.h"
8、例如:下面是一个宏替换#include指令:
#if VERSION = = 1
#define INCFILE "vers1.h"
#elif VERSION = = 2
#define INCFILE "vers2.h" /* and so on */
#else
#define INCFILE "versN.h"
#endif
#include INCFILE
16.3 宏替换
1、当且仅当两个替换序列中的预处理标记具有相同的数目、顺序、拼写和空白分隔符时,它们才是等价的,其中所有的空白分隔符都被看成是等同的。
2、被定义为对象式宏(不具有lparen的宏)的标识符可被另外一个#define预处理指令重定义,只要第二个定义也是对象式宏,且它们的替换序列等价;否则程序是非法的。
3、被定义为函数式宏(具有lparen的宏)的标识符可被另外一个#define预处理指令重定义,只要第二个定义也是函数式宏,并同第一个函数式宏具有相同数目和拼写的参数,且它们的替换序列等价;否则程序是非法的。例如:
       #define a(b) b
       #define a(c) c
vc中,上述定义是合法的,因为它们的定义在逻辑上完全等价。但依据标准,两个函数式宏的参数拼写不同(第一个为b,第二个为c),所以非法。
4、函数式宏调用的实参数目应该同宏定义中的形参数目一致,调用以预处理标记 ) 终止。[注:在程序文本中,只要一个函数式宏名的后面跟一个左括号,便认为是对此函数式宏的调用,而不对调用参量的数目进行检查匹配,这也是预处理器中需要向前看一个预处理标记的地方。如果函数式宏调用的实参数目与宏定义中的形参数目不一致将如何处理呢,不同的预处理器应该有不同的处理方法,这也是标准中没有明确的地方。下面以vc6为目标对上述情况进行测试,测试代码如下:
       #define sum(b, c) b+c
       int sum;
       sum = sum(1, 2, 3);
在上述代码中,sum宏调用的实参数目多于其定义的形参数目,这种情况下vc6会忽略多于的实参,仅产生一个警告信息:warning C4002: too many actual parameters for macro 'sum'
如果将上述代码中的最后一行改写为
sum=sum(1);
这种情况下sum宏调用的实参数目少于其定义的形参数目,vc6将同时报告一个警告信息和一个错误信息:
warning C4003: not enough actual parameters for macro 'sum'
rror C2059: syntax error : ';'
]
5、函数式宏的参数标识符在其作用域内必须是唯一声明的。例如:
       #define a(b, b) b
是非法的,因为函数式宏a的参数b在其作用域内重复声明了两次。
6、紧跟在define后面的标识符称为宏名。对于宏名,存在一个独立的名字空间。上述任何形式的宏(对象式或函数式),预处理标记替换序列之前或之后的空白字符都将忽略而不作为替换序列的一部分。
7、如果标识符位于#预处理标记之后,且在词法上#位于合法的预处理指令起始点,则不对此标识符进行宏替换。例如:
       #define test if
       #test 1
在第二行中,test位于#之后,且#位于合法的预处理指令起始点,所以不将test替换为第一行定义的if
8、具有如下形式的预处理指令
# define identifier replacement-list new-line
定义了一个对象式宏,并且致使每一个后续的宏名实例(在宏替换期间,所有的字符文字和字符串文字都是预处理标记,而不是可能包含标识符子序列的序列,所以不对其进行扫描以搜索宏名或参数)被替换为预处理指令中的替换序列(候选标记不是标识符,即使其拼写完全由字母和下划线组成。所以无法定义一个名字等于候选标记的宏)。之后,按下文将描述的方式对替换序列进行二次扫描以搜索更多的宏名。
9、具有如下形式的预处理指令
# define identifier lparen identifier-listopt ) replacement-list new-line
定义了一个带有参数的函数式宏,在句法上,其类似于函数调用。宏的参数由圆括号中可选的标识符列表指定,参数的作用域始于在标识符列表中的声明,止于#define预处理指令的终止符——换行符。对于每一个后续函数式宏名实例,如果其之后的预处理标记为(,那么该宏名实例引入了一组将被宏定义中的替换列表所替换的预处理标记序列(一次宏调用)。
被替换的预处理标记序列止于同lparen配对的 ) 预处理标记。在触发函数式宏调用的预处理标记序列中,换行符被当作普通的空白字符对待。例如:
       #define sum(a, b) a+b
       int m, n, s;
       m = 11;
       n = 23;
       s = sum(
                     m,
                     n
                     );
上述代码中对sum函数式宏的调用是合法的,因为宏调用中的换行符等同于普通的空白字符。
10、被最外层配对圆括号界定的预处理标记序列构成了函数式宏的参量列表。参量由逗号预处理标记分隔,但位于内层配对圆括号内的逗号不起分隔参量的作用。如果(在参量置换之前)任一参量为空,程序的行为是未定义的。另外,如果参量列表中的预处理标记序列能构成预处理指令,程序的行为也是未定义的。[例如:
#define sum(a, b) a+b
int s = sum((1, 2), 3);
vc6中,s的结果为5
]
16.3.1 参量置换
1、在识别出函数式宏调用的参量之后,进行参量替换操作。除了前置#或##的预处理标记或后置##的预处理标记的参数之外,替换序列中的参数在所有宏展开之后被替换为相应的参量。在置换发生之前,对每一个参量预处理标记进行完全的宏替换,就好像它们独自构成了翻译单元的后续部分,而不存在其它的预处理标记(由此导致的一个结果是:在对参量预处理标记进行宏替换之后,对于替换结果的最后一个预处理标记不再进行函数式宏检测)。
16.3.2 #操作符
1、在函数式宏的替换序列中,每一个#预处理标记后面都必须跟一个参数。
2、在替换序列中,如果一个参数紧跟在#预处理标记之后,那么它们将被一字符串文字预处理标记所替换,字符串文字的内容等于相应参量预处理标记的拼写。在字符串文字中,用单个空格替换参量的预处理标记间的每一处空白(见下面的例子)。构成参量的第一个预处理标记之前和最后一个预处理标记之后的空白将被删除。另外,参量中的每一个预处理标记的拼写被原封不动地保留在字符串文字中,但为了保留原始拼写需要进行一些特殊处理:
在字符文字或字符串文字中的每一个”和/字符之前插入/字符。如果替换结果不是一个有效的字符串文字,程序的行为是未定义的。#和##操作符的处理顺序是未指定的。例如:
#define x(s) #s
char *s = x(a   b/**/ c); // s = “a b c”;
char *p = x(a/nb);  // p = “a//nb”;
16.3.3 ##操作符
1、对任一形式的宏定义(对象式宏或函数式宏),##预处理标记都不能出现在替换序列的开始或末尾。
2、在替换序列中,如果一个参数紧位于##预处理标记之前或之后,它将被相应参量的预处理标记序列替换。
3、不论是对象式宏调用还是函数式宏调用,在对替换序列进行再次扫描以搜索更多的宏名之前,替换序列中的每一个##预处理标记实例(而不是由参量引入的)将被删除,##预处理标记的前置与后置预处理标记被连接在一起形成单个预处理标记。如果结果不是一个有效的预处理标记,程序的行为是未定义的。结果标记对于二次宏替换是有效的。##操作符的处理顺序是未指定的。
16.3.4 重扫描和二次替换
1、当替换序列中所有参数被置换之后,所得的预处理标记同源文件中的所有后续预处理标记一起被重扫描,以搜索更多的宏名进行替换。
2、如果在扫描替换序列时,发现了正被替换的宏名,不再对该宏名进行替换(防止直接递归,如下例中,在扫描y的替换序列y, +, x时,首先找到了正被替换的宏名y,此时不再对其进行展开,否则会造成死循环)。再者,如果在任意嵌套的替换中遇到了正被替换的宏名,也不再对其进行替换(防止间接递归)。这些没有被替换的宏名预处理标记在进一步的替换中不再有效,即使在稍后的二次检测中该宏名预处理标记是可替换的(参见16.3.5中的第5款)例如:
       #define y y+x
3、不对宏替换后的预处理标记序列进行预处理指令的识别和处理。例如:
#define o(s) s
o(#define g 100)
2行的宏展开应该为#define g 100,但按上述要求,不对宏替换后的预处理标记序列进行预处理指令的处理,所以预处理器不能正确识别此条宏定义指令,从而把它留给编译器处理。毫无疑问,编译器处理这样的语句是会发生错误的,vc对上述定义的报错信息如下:
… : error C2121: '#' : invalid character : possibly the result of a macro expansion
… : error C2146: syntax error : missing ';' before identifier 'g'
… : error C2501: 'define' : missing storage-class or type specifiers
… : fatal error C1004: unexpected end of file found
16.3.5 宏定义的作用域
1、宏定义(独立于块结构)在遇到相应的#undef指令之前或(没有遇到相应的#undef指令)在翻译单元结束之前一直有效。
2、形式如下的预处理指令
# undef identifier new-line
导致指定的标识符不再被定义为宏名。如果指定的标识符当前本来就不是宏名,该指令被忽略。
3、[注:宏定义的最简单用法是用来定义“明示常量”,如下所示:
#define TABSIZE 100
int table[TABSIZE];
4、下面的代码定义了一函数式宏,其值取参量的最大值。宏定义的优点有:对于任何类型兼容的参量均适用;产生内联代码从而避免了函数调用的额外开销。其缺点有:可能对其中的某个参量计算多次(包括副作用);如被多次调用的话,产生的代码多于函数。另外,无法取得宏定义的地址,因为它根本就没有地址。
#define max(a, b) ((a) > (b) ? (a) : (b))
宏定义中的圆括号保证参量和结果表达式都被正确地界定。
5、为了说明重定义和重检测规则,参看如下序列:
#define x 3
#define f(a) f(x * (a))
#undef x
#define x 2
#define g f
#define z z[0]
#define h g( ˜
#define m(a) a(w)
#define w 0,1
#define t(a) a
f(y+1) + f(f(z)) % t(t(g)(0) + t)(1);
g(x+(3,4)-w) | h 5) & m
(f)ˆm(m);
其结果为
f(2 * (y+1)) + f(2 * (f(2 * (z[0])))) % f(2 * (0)) + t(1);
f(2 * (2+(3,4)-0,1)) | f(2 * ( ˜ 5)) & f(2 * (0,1))ˆm(0,1);
       需要说明的是结果1中的最后一项t(1),预处理器并没有进一步将其替换为1,这是因为在对t(t(g)(0) + t)进行替换的时候,预处理器并没有对最后一个t进行替换。
6、为了说明字符串文字创建和标记连接规则,参看如下序列:
#define str(s) # s
#define xstr(s) str(s)
#define debug(s, t) printf("x" # s "= %d, x" # t "= %s", /
x ## s, x ## t)
#define INCFILE(n) vers ## n /* from previous #include example */
#define glue(a, b) a ## b
#define xglue(a, b) glue(a, b)
#define HIGHLOW "hello"
#define LOW LOW ", world"
debug(1, 2);
fputs(str(strncmp("abc/0d", "abc", ’/4’) /* this goes away */
= = 0) str(: @/n), s);
#include xstr(INCFILE(2).h)
glue(HIGH, LOW);
xglue(HIGH, LOW)
其结果为
printf("x" "1" "= %d, x" "2" "= %s", x1, x2);
fputs("strncmp(/"abc//0d/", /"abc/", ’//4’) = = 0" ": @/n", s);
#include "vers2.h" (after macro replacement, before file access)
"hello";
"hello" ", world"
进一步进行字符串文字连接之后的结果为
printf("x1= %d, x2= %s", x1, x2);
fputs("strncmp(/"abc//0d/", /"abc/", ’//4’) = = 0: @/n", s);
#include "vers2.h" (after macro replacement, before file access)
"hello";
"hello, world"
宏定义中#和##标记周围的空白是可选的。
7、最后,示范一下重定义规则,以下序列是合法的:
#define OBJ_LIKE (1-1)
#define OBJ_LIKE /* white space */ (1-1) /* other */
#define FTN_LIKE(a) ( a )
#define FTN_LIKE( a )( /* note the white space */ /
a /* other stuff on this line
*/ )
但如下的重定义是非法的:
#define OBJ_LIKE (0) /* different token sequence */
#define OBJ_LIKE (1 - 1) /* different white space */
#define FTN_LIKE(b) ( a ) /* different parameter usage */
#define FTN_LIKE(b) ( b ) /* different parameter spelling */
]
16.4 行控制
1、如果#line指令存在串文字,其必为字符串文字。
2、在对源文件进行处理并生成当前标记的过程中,当前源码行的行号比在翻译步骤1中(2.1)读取的换行字符数大1。
3、如下形式的预处理指令
# line digit-sequence new-line
致使实现将紧跟其后的源码行的行号设置为digit-sequence(被当作十进制数)。如果数字序列指定的值为零或大于32767,程序的行为是未定义的。
4、如下形式的预处理指令
       # line digit-sequence "s-char-sequenceopt" new-line
同3款所述的那样设置行号,并且将源文件的名字更改为字符串文字"s-char-sequenceopt"的内容。
5、如下形式的预处理指令
       # line pp-tokens new-line
(与上述两种形式均不匹配)是允许的。要象处理普通文本一样处理指令中位于line之后的预处理标记(每一个当前被定义为宏名的标识符都被其预处理标记替换列表所替换)。如果在所有替换之后,所得的指令仍不匹配上述两种形式,程序的行为是为定义的;否则,按适当的方式对结果进行处理。
16.5 出错指示
1、形式如下的预处理指令
# error pp-tokensopt new-line
导致编译器产生一条包含指定的预处理标记序列的诊断信息,并标记程序出错。例如:
       #error “this is a test error message”
vc中的报告信息如下:
… : fatal error C1189: #error : “this is a test error message”
vc在遇到一个出错指示后,立刻终止编译程序,不再对后面的代码进行编译。
16.6 pragma指令
1、编译器按其自定义的方式处理如下的预处理指令:
# pragma pp-tokensopt new-line
任何编译器不能识别的pragma指令都被忽略。
16.7 空指令
1、形式如下的预处理指令
       # new-line
无任何效用。
16.8 预定义的宏名
1、实现必须定义以下几个宏名:
       __LINE__ 当前源代码行的行号(十进制常量)。
       __FILE__ 源文件名(字符串文字)
       __DATE__ 源文件的翻译日期(形式为“Mmm dd yyyy”的字符串文字,其中月份的名称同asctime函数所产生的相同;如果dd的值小于10,其第一个字符为空格)。如果翻译日期不可用,由编译器提供一自定义的有效日期。
       __TIME__ 源文件的翻译时间(形式为“hh:mm:ss”的字符串文字,同asctime函数产生的时间一致)。如果翻译时间不可用,由编译器提供一自定义的有效时间。
       __STDC__ 是否预定义__STDC__及其取值都是由编译器决定的。
       __cplusplus 当编译一个C++翻译单元时,名字__cplusplus被定义为199711L。
2、预定义宏的值在整个翻译单元内保持不变(__LINE__和__FILE__除外)。
3、如果上述任一预定义宏名或标识符defined被预处理指令#define或#undef限定,程序的行为是未定义的。例如:
#define __LINE__ 1
#undef __LINE__
#define defined 1
#undef defined
       对于上述预处理指令,vc都发出
… : warning C4117: macro name '???' is reserved, '#define' ignored
警告信息(其中???__LINE__defined)。不过奇怪的是,在vc6中,虽然提示信息报告说忽略了#undef __LINE__,但随后语句中对__LINE__的引用却会触发
… : error C2065: '__LINE__' : undeclared identifier
错误信息。这应该是vc6的一个小bug,我没有在6.0以上的版本对上述代码进行测试,不知后续版本有没有改进。
       另外需要说明的一点是,define可以被定义为宏,所以下述代码是合法的:
       #define define 2
       int a = define;
       虽然上述代码将define定义为2,当并不影响后续宏定义指令的正确识别,对于#后面的define不进行宏替换(参见16.3中的第7款)。

 

抱歉!评论已关闭.