< <ISO/ANSI C标准译文与注解 C/C++预处理部分>>
内容简介:本文档完整翻译了C标准(99版)中预处理和相关章节的内容,并在许多必要之处附加了注解和程序示例,以帮助读者理解标准原文,同时制作了详细的中英文索引备查。
译者:胡彦
出处:http://blog.youkuaiyun.com/huyansoft
如果转载,请保留译者和出处信息,谢谢!
本文同时制作了CHM格式的文档,可在http://download.youkuaiyun.com/source/468852下载。CHM文档的好处在于,阅读时可以随时点击链接跳转、前进和后退。
郑重声明:
本文档之英文原版来自互联网,仅供个人学习﹑私下交流之用,版权仍归ISO/IEC所有,任何组织和个人不得公开传播或用于任何商业盈利用途,否则一切后果由该组织或个人承担!制作者不承担任何法律及连带责任!请自觉于下载后24小时内删除,如果需要,请向ISO购买英文原版.
-----------------------------------------------------------------------------------------------------------------
前言
ISO/ANSI C标准提供了对C语言完整的定义,是最准确﹑权威﹑详尽的C参考资料.其措辞之严谨,讨论特征之细致,覆盖内容之全面,是其它任何一部C书籍和文档无法比拟的.
C标准在给出语言定义的同时,几乎就是在提示读者,一个C编译器该如何实现.许多常被忽略的语言特征,对编译器的实现者来说,却是无法回避和必须处理的.如果你准备着手编写一个(哪怕很不完整的)C编译器,C标准会让你豁然开朗﹑少走许多弯路.
如果你是一个普通的C/C++程序员,虽然不需要通读标准,但在遇到一些争论不清的细节问题时,偶尔查阅一下它总可以找到令人信服的答案,纠正许多误解.同时,标准指明了哪些行为是未定义的(undefined),哪些是未确定的(unspecified),哪些是由实现定义的(implementation-defined),防止自己程序中出现这些不确定行为,可以避免写出坏代码,产生可移植性更强的程序.
本文档完整翻译了C99标准中预处理和相关章节的内容.在现行的ISO C++标准中,C语言子集部分主要采用的是C89版本,因此,本文的大部分内容也同样适用于C++.事实上,文中所有示例程序都是在Visual C++ 2005下调试通过的.为便于读者阅读时对比英文原版,文中的页码全部与原版一一对应,同时制作了详细的中英文索引,以利速查和对照.
C标准措辞十分严谨,大部分内容只有叙述,没有示例,初学者看起来会比较费力.因此,本文在许多必要之处增加了注解和例子,以帮助不熟悉的读者理解原文.注解分2种:一种是较短的﹑嵌在译文原句中的注解,以一对[]括起;另一种是较长的,包括详细的例程和解释,集中在每页下方,以①②③等样式标出.标准中原有注释仍以1)2)3)等样式标出.
现行C标准虽然只有5百多页,但由于其讨论的大部分内容,都是语言较深的细节,以及大多程序员不常关注的特殊用法,不少描述只有具有一定背景的读者才能心神领会.这些微妙之处如果要展开讨论,每句原话都要用上页的篇幅,文中的注解仅从语言使用的角度,为初学者理解原文作出有益的提示.
由于水平有限,在翻译过程中,笔者时刻感到如履薄冰,而这方面可供参考的资料又太少,因此,许多内容都要反复咀嚼上下文,详细查阅相关章节,理解比对原文示例,动手调试测试程序,最后才能一知半解.即便如此,文中错误和不妥之处仍然难免,肯请广大读者批评指正.以后的勘误和更新版,将发布在译者的blog上.
-----------------------------------------------------------------------------------------------------------------
导读
----有必要一读吗?
请先判断下面程序段中有无错误:
1
char* s="abcdefgh //hijklmn";
第1种说法是:它是正确的,定义了一个字符指针
第2种说法是:它是错误的,因为//注释掉了字符串右部的引号,以及语句终结符分号
(提示:见P66第2点和例子)
2
//Is it a /
valid comment?
第1种说法是:它是正确的注释,因为它用/拆成了2行
第2种说法是:它是错误的,因为//注释遇换行符便结束了,导致第2行出现了非法字符
(提示:见P66例子,或者P9第2点和P10第3点)
3
int/*...*/i;
第1种说法是:它是正确的,因为int和i之间用/*...*/隔开了
第2种说法是:它是错误的,因为注释被删除之后,原句会成为inti;
(提示:见P10第3点和注解6)
4
#define X 3
#define Y X*2
#undef X
#define X 2
int z=Y;
第1种说法是:z的初值为4
第2种说法是:z的初值为6
(提示:见P156例3)
5
int M=3;
#define M 2*M
int n=M;
第1种说法是:它是正确的,n的初值为6
第2种说法是:它是错误的,因为宏M的展开会造成无限循环
(提示:见P155页6.10.3.4节第2点)
其实上面几种说法,只有第1种是正确的.
如果你总是认同第2种,那你是应该继续朝下看了...
-----------------------------------------------------------------------------------------------------------------
前言
导读
目录
正文 P9--P161
5.1.1.1 程序结构 P9
5.1.1.2 翻译阶段 P9--P10
6.4 词法元素 P49--P50
6.4.9 注释 P66
6.10 预处理指令 P145--P161
语法 P145--P146
描述 P146--P147
6.10.1 条件包含 P147--P149
6.10.2 源文件包含 P149--P150
6.10.3 宏替换 P151--P158
6.10.3.1 实参的替换 P153
6.10.3.2 #操作符 P153
6.10.3.3 ##操作符 P154
6.10.3.4 重复扫描和进一步替换 P155
6.10.3.5 宏定义的作用范围 P155
宏替换例子 P155--P158
6.10.4 Line指令 P158
6.10.5 Error指令 P159
6.10.6 Pragma指令 P159
6.10.7 空指令 P160
6.10.8 预定义的宏名 P160--P161
6.10.9 Pragma操作符 P161
术语辨析
索引
参考文献
译注者简介
-----------------------------------------------------------------------------------------------------------------
正文
P9-----------------------------------------------------------------------------------------------------------------
5.1.1.1 程序结构(Program structure)①
1 一个C程序的所有部分不需要同时被编译.在这份国际标准文档中,存放在一起作为一个单位的程序文本称为源文件(source files),或预处理文件(preprocessing files).一个源文件,连同所有经用#include预处理指令包含的头文件和其它源文件,被认为是一个预处理翻译单元(preprocessing translation unit)②.在预处理过程结束后,一个预处理翻译单元转称为编译单位(translation unit)③.先前翻译过的编译单位可以单独地保存或存放在库中.一个程序内,各个分开的编译单位可通过一些方式通信,例如,通过函数的调用(函数名标识符已外部链接的)﹑或者对象的处理(对象名标识符已外部链接的)﹑或者对数据文件的处理.各个编译单位可以分开地编译,之后链接生成一个可执行的程序.
向前参考: 标识符链接(6.2.2),外部定义(6.9),预处理指令(6.10).
5.1.1.2 翻译阶段(Translation phases)④
1 下面的各个阶段指定了翻译语法规则之中的优先顺序.5)
1.[字符集转换]
如果必要的话,将物理源文件的多字节字符(multibyte characters)⑤,以实现⑥定义的方式映射到源字符集(source character set),并引入换行符(new-line characters)作为行结束指示符.三联符(trigraph)⑦被相应的单字符内部表示替换.
2.[断行连接]
每个紧跟一换行符的反斜线字符(/,backslash),连同后跟的换行符一起被删除,以将物理上的源代码行接合起来,形成逻辑上的源代码行.在任何物理源代码行中,只有最后一个反斜线字符,用作这种接合才符合条件⑧.
------------------------------
5) 尽管在实践中,有些阶段会典型地合并在一起,但实现应该以这样的方式行为,好像这些分开的阶段确实出现了⑨.
① 5.1.1.1--6.4.9节为后面的主要内容作必要的铺垫
② 直观地说,在某个IDE下,添加到某个project中的每个.c或.cpp文件,都会被用以形成一个预处理翻译单元
③ 编译器真正处理的对象
④ 本节内容十分重要,尤其当断行﹑注释﹑宏替换﹑条件编译﹑字符串等语言现象混杂在一起时
⑤ 例如,三联符(本页注解7)和大字符集(P10注解7)成员都是多字节字符
⑥ 指编译器,以下同
⑦ 例如对程序行int a ??( ??) = ?? < 1,2,3 ??>;其中??( ﹑??) ﹑?? < ﹑??>都是三联符,替换后原语句将成为int a[]={1,2,3};采用三联符主要是因为一些国家的键盘上没有{﹑}等字符
⑧ 例如,如果在/之后﹑换行符之前不小心键入了空格,那么这个/字符便不表示断行
⑨ 例如,有些实现可能会将1至4阶段合并在一起,作为预处理过程;将5至7阶段合并在一起,作为编译过程
P10-----------------------------------------------------------------------------------------------------------------
在上述接合过程发生之前,一个非空的源文件应该以一个换行符结束①,并且这个换行符不应紧跟在一个反斜线字符之后②③.
3.[处理注释和空白]
源文件被分解成若干预处理记号④6)和若干空白符⑤的序列(包括注释).一个源文件不应以某个预处理记号的一部分﹑或某个注释的一部分结束.每个注释被一个空格符所替换⑥.换行符仍保留.对于每段空白符(不含换行符)的非空序列,是保留还是替换成一个空格符,则由实现定义.
4.[预处理]
预处理指令被执行,宏调用被展开,_Pragma一元操作符表达式被执行.如果一段匹配大字符集(universal character name)⑦语法的字符序列是由记号连接(6.10.3.3)产生的,则行为未定义⑧.一条#include预处理指令导致指定的头文件或源文件从阶段1到阶段4递归地被处理.最后删除所有的预处理指令⑨.
5.[处理转义字符]
字符常量和字符串文字量中的每个源字符集成员与转义序列被转换成执行字符集(execution character set)中相应的成员.如果没有相应的成员,则转换成由实现定义的成员,而不是空(或宽)字符.7)
6.[合并邻近的字符串文字量]
邻近的若干个字符串文字量记号被连接在一起成为一个.
7.[词法分析,语法分析,语义分析,中间代码产生等]
分隔记号的空白符不再具有意义.每个预处理记号被转换成一个记号.所有结果记号作为一个编译单位,从语法上和语义上进行分析和翻译.
8.[链接外部库,生成可执行程序]
解决所有对外部对象和函数的引用.链接库部分,以确保当前编译单位中未定义的函数和对象的外部引用.所有这些翻译的输出被集中成一个程序的映象,之中含有需要在它的运行环境中执行的信息.
向前参考: 大字符集(6.4.3),词法元素(6.4),预处理指令(6.10),三联符序列(5.2.1.1),外部定义(6.9).
------------------------------
6) 正如6.4节所述,将源文件的字符分解成预处理记号的过程是上下文相关的.例如,见#include预处理指令中对 <符号的处理.
7) 一个实现不需要将所有非源字符集的成员转换成执行字符集的成员.
① 原因参见P146注解1
② 因为那样的话,接合之后该换行符便被删除了
③ 可见,断行连接作为翻译过程的第2个阶段,它的处理比注释﹑标识符﹑预处理指令﹑字符串等都要底层得多,这使得断行操作几乎可以出现在程序内的任何位置,例如:
//这是一条合法的/
单行注释
//
/这是一条合法的单行注释
#def/
ine MAC/
RO 这是一条合法的/
宏定义
cha/
r* s="这是一个合法的//
n字符串";
④ “记号”通常指关键字﹑标识符﹑常数﹑字符串﹑运算符等词法元素.在一个编译器中,词法分析器扫描源程序,每次从源程序中识别出一个记号,将记号的类别和属性返回给语法分析器,后者分析这些记号所构成的序列在语法上是否合法.简而言之,预处理记号就是预处理器所关注和识别的记号.见6.4第3点,P49
⑤ 空白符的定义见6.4第3点,P49
⑥ 这正是关键字(如while)﹑标识符(如printf)﹑数字(如3.14)﹑运算符(如++, < <=)中不能夹有注释的原因,因为上述记号中不能夹有空格.同时可以推论,在大部分空格符可以合法出现的地方,通常也可以出现一段注释(例外情况见6.4.9,P66),例如
/*这是*/#/*一条*/define/*合法的*/ID/*预处理*/replacement/*指*/list/*令*/
#define str(s) #s //由实参序列直接生成字符串(见6.10.3.2,P153)
#define str2(s) str(s) //用实参宏替换后的序列生成字符串
puts(str2(ID)); //原样输出宏ID的替换列表
按规定,define、ID与replacement三者间必须要有空白符作为分隔,这里只有注释,然而编译却通过了,由此猜想,由注释替换而来的空格充当了分隔符的作用.进一步运行程序,由输出结果是replacement list(而不是replacementlist)可知,注释确实被空格替换了
⑦ 它是一段形如/unnnn或/unnnnnnnn的序列(n是一位0至f内的十六进制数),用在标识符、字符常量、字符串文字量中,以指定不在基本字符集内的字符(见参考文献1的C3.3节).例如:
int /u0123_my_/u4567_int_/u89ab_var_/ucdef=10;//定义了一个整型变量,变量名中含有4个大字符集字符
printf("%d/n",/u0123_my_/u4567_int_/u89ab_var_/ucdef);//输出10
⑧ 未定义的行为(undefined behavior):在某些不正确情况下的做法,标准并未规定应该怎样做,实现可以采取任何行动,也可以什么都不做.例如,当一个有符号整数溢出时该采取什么行动.程序出现这种行为会导致坏代码.详查见索引
⑨ 因此,下列预处理符号并不作为C/C++语法上的关键字或操作符:#, ##,define,defined,elif,endif,error,ifdef,ifndef,include,line,pragma,undef,因为在后续编译阶段,它们已经不存在了。
P49-----------------------------------------------------------------------------------------------------------------
6.4 词法元素
语法
1 记号(token)定义为:
关键字(keyword)
标识符(identifier)
常量(constant)
字符串文字量(string-literal)
标点符号(punctuator)
预处理记号(preprocessing-token)定义为:
头文件名(header-name)
标识符(identifier)
预处理数字(pp-number)
字符常量(character-constant)
字符串文字量(string-literal)
标点符号(punctuator)
每个不属于上述各项的非空白符
约束①
2 每个被转换成一记号的预处理记号应是具有下词法形式之一:一个关键字﹑标识符﹑常量﹑字符串文字量或标点符号.
语义
3 一个记号是翻译阶段7和8期间,语言的最小词法元素.记号的种类有:关键字,标识符,常量,字符串文字量,以及标点符号.一个预处理记号是翻译阶段3到6期间,语言的最小词法元素.预处理记号的种类有:头文件名,标识符, 预处理数字,字符常量,字符串文字量,标点符号,以及在词法上不匹配其它预处理记号类别的非空白符.58)如果一个'或"字符匹配了最后一项类别,则行为是未定义的.预处理记号被下面二者或其中之一而分隔:空格(white space,包括后面介绍的注释②),空白符(white-space characters,包括空格,水平制表符,换行符,垂直制表符,换页符)③.正像6.10节描述的那样,在翻译阶段4期间的某些情况下,空格(或它的缺少)提供更多的作用,而不仅仅是分隔预处理记号.在一个预处理记号内部,空格仅可以作为头文件名的一部分出现,或在一个字符常量/字符串文字量中引入.
------------------------------
58) 占位符是一种另外的类型,它在翻译阶段4期间内部地使用(见6.10.3.3[P154第2点]),而不出现在源文件中.
① 约束指:这里所列的任何规则如果被破坏,编译器应该给出一条错误信息,而不仅仅是警告
② 注释总是被看作空格,原因见5.1.1.2第3点,P10
③ 空格与空白符后面会多次出现,这里要二者注意的区分
P50-----------------------------------------------------------------------------------------------------------------
4 如果输入流已被解析成若干个预处理记号,直到遇见一个给定的字符,那么下一个预处理记号,应是能够组成一个预处理记号的最长的字符序列.①对这条规则也有一处例外:一个头文件名预处理记号,仅仅在一条#include预处理指令中,才会被识别,并且在这条指令中,一个即可能是头文件名﹑也可能是字符串文字量的字符序列,会被识别为前者.
5 例1 对于程序片断1Ex,它被解析成1个预处理数字记号(不是一个合法的浮点常量,也是不是一个合法的整型常量),尽管将它解析为1和Ex这对预处理记号也许会生成一个合法的表达式(例如,当Ex是定义为+1的宏时).类似地,程序片断1E1被解析成1个预处理数字(一个合法的浮点常量记号),无论E是不是个宏名.
6 例2 对于程序片断x+++++y,它被解析为x ++ ++ + y1②,它违反了对增量操作符的约束③,尽管解析为x ++ + ++ y也许能产生一个合法的表达式.
向前参考:字符常量(6.4.4.4),注释(6.4.9),表达式(6.5),浮点常量(6.4.4.2),头文件名(6.4.7),宏替换(6.10.3),后缀增量和减量操作符(6.5.2.4),前缀增量和减量操作符(6.5.3.1),预处理指令(6.10),预处理数字(6.4.8),字符串文字量(6.4.5).
------------------------------
① 这段话意思是,词法分析器每次总是将最长匹配的记号返回,例如对于关系运算符==,词法分析器不会将其当作2个赋值号返回,类似情况见本页例2
② 第1次识别出标识符x,第2次和第3次识别出最长匹配的符号++,第4次和第5次识别出符号+和y
③ 因为前面的x++执行结果是个表达式,对表达式再++显然错误
P66-----------------------------------------------------------------------------------------------------------------
6.4.9 注释
1 除了在一个字符常量、字符串文字量、或另一个注释中,字符序列/*引入一段注释.扫描注释中的内容时,仅仅识别多字节字符,以及终结这段注释的*/字符序列.69)
2 除了在一个字符常量、字符串文字量、或另一个注释中,字符序列//引入一段注释.注释中可以包括所有多字节字符,但不包括下一个换行符①.扫描注释中的内容时,仅仅识别多字节字符,以及终结这段注释的换行符.
3 例
"a//b"; //4个字符的字符串文字量
#include "//e" //未定义的行为
// */ //这是注释,不是语法错误
f = g/**//h; //等价于f = g / h;
///
i(); //二行注释的一部分②
//
/ j(); //二行注释的一部分②
#define glue(x,y) x##y
glue(/,/) k(); //语法错误,不是注释③
/*//*/ l(); //等价于l();
m = n//**/o
+ p; //等价于m = m + p;
------------------------------
69) 因此,/* ... */ 注释不能嵌套.
① 这一点容易被忽略,假如换行符本身也作为//注释的一部分,那么下面的预处理指令序列:
#include <stdio.h>//一段单行注释
#include <string.h>
在翻译阶段3(见5.1.1.2第3点,P10)之后,注释将被一个空格所替换,上面的2条指令将变成一行:
#include <stdio.h> #include <string.h>
这显然造成了错误
② 见5.1.1.2第2点,P9
③ 因为注释先于预处理指令被处理(见5.1.1.2第3﹑4点,P10),当该行被展开成//k();时,注释已处理完毕,此时再出现//当然错误.因此,试图用宏开始或结束一段注释是行不通的,类似的例子还有:
#define BSC //
#define BMC /*
#define EMC */
BSC my single-line comment //错误
BMC my multi-line comment EMC //错误
P145-----------------------------------------------------------------------------------------------------------------
6.10 预处理指令
语法
1 [该部分所列的语法产生式,可帮助读者从整体上把握预处理文件的结构,以及预处理指令的格式]
[预处理文件可以为空,或为一组]
pre-processing-file:
group_opt[后缀_opt是optional的缩写,表示该项是可选的]
[组定义为1至若干个分组]
group:
group-part
group group-part
[分组可以是条件编译块,或其它预处理指令行,或正文行,或#号加上非预处理指令行]
group-part:
if-section
control-line
text-line
# non-directive
[条件编译块由if...elif...else...endif组成,其中elif组、else组是可选的]
if-section:
if-group elif-groups_opt else-group_opt endif-line
[if组可以是if﹑ifdef或ifndef指令行,再加上其它语句组成.其中,“其它语句”或者为空,或者又是一个group]
if-group:
# if constant-expression new-line group_opt
# ifdef identifier new-line group_opt
# ifndef identifier new-line group_opt
[elif组可以有1至多个elif分组,即elif分组可以连续出现多个]
elif-groups:
elif-group
elif-groups elif-group
[elif分组由#号+elif指示符+常量表达式+换行符+其它语句组成,“其它语句”或者为空,或者又是一个group]
elif-group:
# elif constant-expression new-line group_opt
[else组由#号+else指示符+换行符+其它语句组成,“其它语句”或者为空,或者又是一个group]
else-group:
# else new-line group_opt
[endif组由#号+endif指示符+换行符组成]
endif-line:
# endif new-line
P146-----------------------------------------------------------------------------------------------------------------
[其它预处理指令行可以是include、define、undef、line、error、pragma或空指令行(每种指令的详细介绍见后面)]
control-line:
# include pp-tokens new-line
# define identifier replacement-list new-line
# define identifier lparen identifier–list_opt ) replacement-list new-line
# define identifier lparen ... ) replacement-list new-line
# define identifier lparen identifier–list_opt , ... ) replacement-list new-line
# undef identifier new-line
# line pp-tokens new-line
# error pp-tokens_opt new-line
# pragma pp-tokens_opt new-line
# new-line
[正文行可以只是一个换行符,或者是一段预处理记号序列加上换行符]
text-line:
pp-tokens_opt new-line
[非预处理指令行由一段预处理记号序列,加上换行符组成]
non-directive:
pp-tokens new-line
lparen:
一个左圆括号字符,前面不能紧跟空白符[见6.10.3注解1,P152]
[替换列表可以为空,或者是一段预处理记号序列]
replacement-list:
pp-tokens_opt
[预处理记号序列由1至若干个预处理记号(见6.4第3点,P49)组成]
pp-tokens:
preprocessing-token
pp-tokens preprocessing-token
new-line:
the new-line character ①
描述
2 一条预处理指令包含一串预处理记号的序列,在翻译阶段4刚开始的时候②,它以一个#预处理记号开头,#预处理记号可以是源文件的第一个字符(随意跟在不含换行符的空白符之后也行),或者跟在至少含一个换行符的空白符之后③,一条预处理指令以下一个换行符结束.139)一个换行符结束一条预处理指令,[?]即使这个换行符出现在类似一次函数宏的调用中.
------------------------------
139) 因此,预处理指令通常被称为“行”.这些“行”没有其它语义重要性,同样地,除了在预处理过程中的某些情形下,所有空白符都具有相同的意义(例如,见6.10.3.2的#字符串产生操作符).
① 依上述产生式的规定,一个pre-processing-file的每行都要以换行符结束,因此,当某个源文件最后一行不是以换行符结尾时,便不匹配pre-processing-file,这时预处理器会假想地在其末尾附加1至2个换行符.见5.1.1.2第2点,P10
②强调这一前提至少有以下几种原因:
1.在翻译阶段2之初,预处理指令中可能含有用作断行的换行符,到翻译阶段4时,它们都已被删除(见5.1.1.2第2点,P9).
2.到了翻译阶段4的时候,每处注释都已经被一个空格所替换,这使得预处理指令内部可以夹有注释(见5.1.1.2第3点,P10).
3.在翻译阶段4进行期间(而不是刚开始时),由宏展开而产生的#号不作为预处理指示符(见P147第7点).
③ 即:#预处理记号必须是某行的第一个非空白符
P147-----------------------------------------------------------------------------------------------------------------
3 一条正文行①不应以#预处理记号开头.一条非预处理指令②不应以语法中任何预处理指令名开头.
4 当处在一个被跳过的group中时,预处理语法较为宽松,以允许任何预处理记号的序列在预处理指令名与下一个换行符之间出现.
约束
5 在一条预处理指令中(从引入#预处理记号到终结的换行符之前),可以出现在预处理记号之间的空白符,是空格与水平制表符(在翻译阶段3,注释已被空格所替换,其它空白符也可能被空格所替换).
语义
6 实现能够处理并有条件地跳过源文件的一些部分[条件编译],包含其它源文件,以及宏替换,这些能力称为预处理,因为在概念上,它们在对作为结果的“编译单位”翻译之前发生.
7 除非另有规定,在一条预处理指令内部的预处理记号不是宏展开的对象.
例如在下面语句中:
#define EMPTY
EMPTY # include <file.h>
第二行的预处理记号序列就不是一条预处理指令,因为在翻译阶段4刚开始的时候,它还尚未以#开头,尽管在宏EMPTY被替换之后,它将成为那样。
6.10.1条件包含
约束
1 控制条件包含的表达式应该是一个整型常量表达式,此外,它不能包含类型转换③,标识符(包括那些词法上等同于关键字的标识符)以页底所描述的方式被解释;140)并且它可以包含如下形式的一元操作符表达式
defined 标识符
或
defined ( 标识符 )
如果这个标识符当前作为宏名被定义(更确切地说,如果它是
------------------------------
140) 由于常量表达式是在翻译阶段4期间被计算的,这时对于每个标识符,它要么是[已经定义的]宏名,要么不是宏名----简单来说,这时还没有关键字,枚举常量等等.
① 预处理指令以外的其它代码行,语法见text-line的产生式,P146
② 语法见non-directive的产生式,P146
③ 例如此种写法错误:#if (int)3.14
P148-----------------------------------------------------------------------------------------------------------------
预定义的宏名①,或者它被#define预处理指令定义过且尚未用#undef指令解除),那么这个一元操作符表达式的值为1,否则值为0.
语义
2 如下形式的预处理指令
# if 常量表达式 换行符 group_opt ②
# elif 常量表达式 换行符 group_opt
检查常量表达式的求值是否非0.
3 在常量表达式求值之前, 要成为常量表达式的预处理记号列表中的宏名(除了被defined一元操作符修饰的那些宏名),像在普通正文中那样被替换. 如果替换的结果仅仅是记号defined,或者在宏替换之前,对一元操作符defined的使用不匹配其2种指定形式之一,那么行为是未定义的.在所有由宏展开而产生的替换和defined一元操作符被执行之后,所有剩下的标识符被替换成数值0,然后每个预处理记号被转换成一个记号.所有结果记号组成了常量表达式,再根据6.6节③的规则对常量表达式进行求值,此外,所有有符号整型与 <stdint.h>头文件中定义的intmax_t类型具有相同的表现; 所有无符号整型与 <stdint.h>头文件中定义的uintmax_t类型具有相同的表现.求值过程包括对字符常量的解释,可能包括将转义字符序列转换成执行字符集成员.当一个相同的字符常量出现在一个编译后续阶段的表达式中(而不是一条#if或#elif指令中)时,这些字符常量的数值是否相称于预处理阶段获得的值,则由实现定义141)④.同样,某个字符常量是否可以具有负值,也由实现定义.
4 如下形式的预处理指令
# ifdef 标识符 换行符 group_opt
# ifndef 标识符 换行符 group_opt
检查指定的标识符当前是否定义为一个宏名.它们的情况分别等同于指令#if defined 标识符和#if !defined 标识符.
------------------------------
141) 因此,下列#if指令与if语句中的常量表达式,在这两种上下文中,不保证求出相同的值.
#if ‘z’ - ‘a’ == 25
if (‘z’ - ‘a’ == 25)
① 见6.10.8,P160
② 后缀_opt是optional的缩写,表示该项是可选的
③ C标准关于常量表达式的章节
④ 实现定义的行为(implementation-defined behavior):由编译器设计者决定采取何种行动,并写入使用手册.因此,在不同编译器下行为很可能是不同的.例如当一个整型数向右移位时,要不要扩展符号位.程序的运行依赖这种行为,会产生不可移植的代码.详查见索引
P149-----------------------------------------------------------------------------------------------------------------
5 每条指令的条件被适当地检查.如果它求值为假(0),它所控制的group部分便被跳过:仅在经过能够确定指令的名字的时候,那些指令才被处理,以保证明了嵌套情形的级别①;当group部分有其它预处理记号时,其它预处理指令记号都被忽略.只有第一个求值为真(非0)的条件指令所控制的group被处理.如果没有条件之值为真(非0)的条件指令,且后面有一条#else指令,那么由#else控制的group被处理;如果没有#else指令,所有#endif之前的group都被跳过.142)
向前参考:宏替换(6.10.3),源文件包含(6.10.2[本页]),最大整数类型(7.18.1.5).
6.10.2 源文件包含
约束
1 一条#include指令应能够识别一个能被实现处理的头文件或源文件.
语义
2 一条如下形式的预处理指令
# include <h-char-sequence> 换行符 ②
按照由实现定义的一系列位置搜索一个头文件,这个头文件应能由 <和>分隔符之间指定的字符序列唯一确定,并导致用这个头文件的全部内容替换这条#include指令.搜索位置如何指定③,或者头文件如何确定④,都由实现定义.
3 一条如下形式的预处理指令
# include "q-char-sequence" 换行符 ⑤
导致用这个源文件的全部内容替换这条#include指令,这个源文件应能由两个"分隔符之间指定的字符序列所确定.命名的源文件以实现定义的方式被搜索.如果搜索不被支持,或者搜索失败,则该指令被重新处理,就像它读入了
# include <h-char-sequence> 换行符
这条假想的指令与原始指令具有相同的包含序列(包括>字符,即便要的话)⑥.
------------------------------
142) 按语法所指,在终结的换行符之前﹑一个预处理记号之后,不应跟有#else或#endif指示符.然而,注释⑦可以出现在源文件的任何地方,包括在一条预处理指令之中.
① 比如与#if匹配的#endif指令便不能被跳过
② h-char-sequence为一段非空字符序列,每个字符是源字符集中除换行符和>以外的任何字符
③ 比如按照什么次序搜索哪些标准目录
④ 有些实现的头文件可能并不与存储器内的物理文件一一对应
⑤ q-char-sequence为一段非空字符序列,每个字符是源字符集中除换行符和"以外的任何字符
⑥ 这意味着用双引号包含标准头文件通常也行,例如:#include "stdio.h"
⑦ 注释的详细规定见6.4.9,P66
P150-----------------------------------------------------------------------------------------------------------------
4 一条如下形式的预处理指令
# include 预处理记号序列 换行符
(不匹配上述两种形式之一)也是允许的. 指令中include后面的预处理记号序列像普通正文那样被处理.(每个当前定义为宏名的标识符,被其替换列表中的预处理记号所替换.)所有替换完成之后,结果应匹配上述两种形式之一.143) <与>记号对之间的预处理记号序列,或一对"字符之间的预处理记号序列,如何组成一个单一的头文件名,其方式是由实现定义的.
5 实现应该为包含一个或多个字母/数字(如5.2.1定义的那样)﹑后跟一个句号(.)和一个单独字母的序列提供单一的映射.实现也可以忽略大小写的区分,并且限定对句号前面有意义的8个字符的映射.
6 一条#include预处理指令也可以出现在这样一个源文件中,这个源文件由于另外一个文件中的#include指令而被读入[嵌套包含],直到一个由实现定义的嵌套界限(见5.2.4.1)①.
7 例1 最常见的对#include预处理指令的使用像下面这样:
#include <stdio.h>
#include "myprog.h"
8 例2 这里阐明了经宏替换的#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
向前参考:宏替换(6.10.3).
------------------------------
143) 注意,邻近的字符串文字量不会连接成一个(见5.1.1.2的翻译阶段);因此,一个导致两个字符串文字量的宏展开会产生一条非法的指令.②
① 标准虽未规定源文件之间能否递归包含(例如在a.h中执行了#include"b.h",而在b.h中又执行#include"a.h"),但标准规定,嵌套包含的层数是有限制的,因此在编译时,递归包含会因嵌套层数过多越界而报错
② 邻近字符串的拼接操作是在预处理之后进行的,如下指令无法正确包含myfile.h:
#include "myfile" ".h"
P151-----------------------------------------------------------------------------------------------------------------
6.10.3 宏替换
约束
1 对于两个替换列表(replacement list),当且仅当它们含有的预处理记号的数目、字符顺序、拼写、空白符分隔都相同时,才被认为是相同的.替换列表中所有的空白符分隔被同等考虑.
2 一个当前定义为对象宏(object-like)的标识符,不能被另一条#define预处理指令重定义,除非第二次仍定义为对象宏,且二者的替换列表相同.同样地, 一个当前定义为函数宏(function-like)的标识符,不能被另一条#define预处理指令重定义,除非第二次仍定义为函数宏,且与前者具有相同数目与拼写的参数,且二者的替换列表相同①.
3 在定义对象宏时,标识符与替换列表之间可以有空白符.
4 如果定义函数宏时,参数列表不是以一个省略号结束,那么调用这个宏时,实参(包括那些不含预处理记号的实参)的数目应等于定义宏时形参的数目.另外,实参的数目比定义宏时(未使用省略号...)形参数目多也可以.应存在一个右括号)预处理记号用作调用的终结符.
5 标识符符__VA_ARGS__只应出现在一个函数宏的替换列表之中,该函数宏的参数列表中使用了省略号[可变参数的宏].
6 函数宏内,一个用作形参的标识符,在它的作用范围②内应被唯一地声明[不能重名].
语义
7 紧跟在关键字define之后的标识符称为宏名.宏名只有一个名字空间③.对于二种形式的宏,替换列表预处理记号之前或之后的若干空白符不作为替换列表的一部分④.
8 如果一个#预处理记号后面跟着一个标识符,从字面上出现在一条预处理指令可以开始的位置,那么该标识符不是宏替换的对象⑤.
9 一条如下形式的预处理指令
# define 标识符 替换列表 换行符
------------------------------
① 见P157例6
② 形参的作用范围从标识符列表中定义点开始,延伸到终结这条预处理指令的换行符为止,见P152第10点
③ 见6.10.3.5第1点,P155
④ 即,宏名后面及换行符前面的空白符都被忽略
⑤ 这意味着下面用法是错误的
#define INCL_STD include <stdio.h>
#INCL_STD //宏INCL_STD不会执行期望的替换
P152-----------------------------------------------------------------------------------------------------------------
定义了一个对象宏,它导致该宏名随后的每个实例144)都被指定的替换列表中的预处理记号所替换.这些预处理记号组成了这条指令的余留部分.
10 一条如下形式的预处理指令
# define 标识符 左圆括号 标识符列表_opt ) 替换列表 换行符 ①
# define 标识符 左圆括号 ... ) 替换列表 换行符
# define 标识符 左圆括号 标识符列表 , ... ) 替换列表 换行符
定义了一个带有参数的函数宏, 它在句法上类似于一个函数.各个参数由可选的标识符列表所指定,它们的作用范围从标识符列表中定义点开始,延伸到终结这条预处理指令的换行符为止.对于随后函数宏的每个实例,宏名之后应跟着一个左圆括号②,这个左圆括号应是宏名之后的下一个预处理记号③,它开始了一段预处理记号的序列,这些序列④都被宏定义中的替换列表所代替(即一次宏调用).被替换的预处理记号的序列以相匹配的右圆括号终结,期间跳过其它互相匹配的左﹑右圆括号对.在组成一条函数宏调用的预处理记号序列中,换行符被当作普通的空白符⑤.
11 最外层相匹配的圆括号所限定的预处理记号序列,形成了函数宏的实参列表.列表中每个参数之间以逗号分隔,但内层相匹配的圆括号之间的逗号并不作为当前宏的实参分隔符.如果在实参列表中出现了一些预处理记号序列,这些序列又可能被当作是预处理指令,则行为是未定义的.
12 如果宏定义的标识符列表中有个省略号,那么尾部的若干个实参,包括该宏任何分隔实参的逗号,都被合并以组成一个称为可变实参(variable arguments)的单一项目.实参如此结合,使得在合并之后,实参的数目比宏定义时形参的数目多出1个(不含省略号).
------------------------------
144) 因为在宏替换时,所有字符常量和字符串文字量都是预处理记号,而不是可能包含编译后续阶段类似标识符的序列(见5.1.1.2[P10]),在编译后续阶段,类似标识符的序列决不会当作宏名或宏参数被扫描⑥.
① 注意:左圆括号它与前面的标识符之间不能含有任何空白符,否则便会误当成对象宏,因为在上页对象宏的定义格式(# define 标识符 替换列表 换行符)中,标识符与替换列表之间是用空白符来区分的.违反上面规定会造成难以察觉的编译错误,例如:
#define MAX (a,b) a>b?a:b //MAX与(a,b)间不小心加了空格
int i=MAX(1,2);
在一编译器下会出现如下费解的提示:
error C2065: 'a' : undeclared identifier
error C2065: 'b' : undeclared identifier
error C2146: syntax error : missing ';' before identifier 'a'
error C3861: 'b': identifier not found
这是因为最后一句被展开成了int i=(a,b) a>b?a:b(1,2);
② 否则不认为该宏名构成一次宏调用,亦不会替换,例如:
int t=3;
#define t(a) a
printf("%d/n", t(5) );//t是宏名,t(5)被替换为5,输出结果是5
printf("%d/n", t );//t不是宏名,不作任何替换,输出变量t的值3
③ 因空白符不属于预处理记号,所以宏名与左圆括号间可以有空白符,这与定义函数宏时不同
④ 以及宏名和圆括号
⑤ 例如:
#define conncat(x,y) x y
puts(conncat("a"
,"b"));//实参"a"后面有换行符,但宏展开的结果是"ab",而不是"a"/n"b"
⑥ 因为那时所有的宏替换都已完成
P153-----------------------------------------------------------------------------------------------------------------
6.10.3.1 实参的替换
1 一次函数宏调用的所有实参被识别后,参数替换发生了.一个替换列表中的形参,除非前面跟有一个#或##预处理记号,或者后面跟有一个##预处理记号①,则在对应实参中所包含的宏都展开之后,这个形参被对应实参的展开结果所替换.在进行替换前,每个实参中的预处理记号,就像组成预处理文件的其余部分那样,都是已被完全替换的宏②;没有其它预处理记号是有用的.
2 出现在替换列表中的一个__VA_ARGS__标识符应像一个形参那样对待,而且若干个可变的实参应该形成用以替换它的预处理记号.
6.10.3.2 #操作符
约束
1 在函数宏的替换列表中③,每个#预处理记号之后都可以立即跟一个形参,这个形参应该作为替换列表中#的下一个预处理记号.
语义
2 如果在替换列表中,一个形参立即跟在一个#预处理记号之后, 那么这二者都被替换成一个单独的窄字符串文字量(character string literal)④记号,它所包含的预处理记号,与对应实参的拼写序列⑤完全相同.在实参预处理记号中的每段[而不是每个]空白符,都变成这个字符串中的一个空格符.实参中第一个预处理记号之前的空白符,以及最后一个预处理记号之后的空白符都会被删除⑥.实参内部其它预处理记号的原始拼写都会保留在这个字符串当中,除了下面用于处理字符串文字量和字符常量拼写的特殊操作:如果实参内部含有字符常量或字符串文字量,那么这些字符常量或字符串文字量中的每个"(包括作为字符串界符的一对")和/字符之前会插入一个/字符⑦.例外情况是,在开始一个大字符集⑧的/字符之前,是否插入一个/字符由实现定义.如果最终的替换结果不是一个合法的窄字符串,则行为是未定义的.空实参对应的结果是空串"".对#与##操作符的求值顺序是未确定的⑨.
------------------------------
① 见6.10.3.2第2点(本页)的特别规定,以及6.10.3.3第2点(P154)的特别规定
② 注意这里的替换顺序:先展开实参中所有宏,再用展开结果替换对应的形参
③ 如果#操作符出现在对象宏的替换列表中,则仅作为一个普通字符,不具有下述含义
④ 字符串文字量(string literal)有2种:
1.窄字符串文字量(character string literal),指括在一对双引号中的0至多个字符(不含这对双引号)
2.宽字符串文字量(wide string literal),与上面一样,但带有前缀L
在不至混淆时,本文档将它们都简译为字符串文字量或者字符串.
标准这里特别采用了character string literal,指明#操作符不会生成宽字符串
⑤ 注意而不是实参扩展后的拼写序列,即使对应的实参也是个宏.
例1:
#define format(x) #x "=%d/n",x
#define N 100
printf( format(N) );//输出N=100
对于format替换列表中形参x,它第1次出现时作为#的操作数,故直接替换成N;第2次出现时不是#的操作数,故先展开对应的实参N,再用展开结果100替换这次出现.最后展开成printf( "N" "=%d/n",100 );经字符串连接后成为printf( "N=%d/n",100 );
例2:
#define str(x) #x
#define str2(x) str(x)
#define myfun(a) a
puts( str( myfun(34) ) );/*不展开宏myfun(34),直接用该序列生成字符串,得到结果"myfun(34)"*/
puts( str2( myfun(34) ) );/*先展开宏调用myfun(34)(见6.10.3.1第1点,P153),再用展开结果34替换对应的形参x,得到str(34),继续展开得到最后结果"34"*/
⑥ 例如:
#define str(s) #s
puts(str(
└┘└─┘a
└┘└─┘a
└┘└─┘a
└┘└─┘));
假如└┘表示一个空格符, └─┘表示一个制表符,则输出结果为a└┘a└┘a,而不是原实参└┘└─┘a└┘└─┘a└┘└─┘a└┘└─┘
⑦ 例如:
#define str(s) #s
printf("%s", str(a/nb "a/nb" // '//' /n '/n'));
上一句经宏替换后,变成:
printf("%s", "a/nb /"a//nb/" // '////' /n '//n'");
从而得到如下输出:
a(换行符)
b "a/nb" / '//' (换行符)
'/n'
如果没有上述规定,那么上一句经宏替换后,就会因双引号的嵌套而出现词法错误了:
printf("%s", "a/nb "a/nb" // '//' /n '/n'");
⑧ 见P10注解7
⑨ 未确定的行为(unspecified behavior):在某些正确情况下的做法,标准并未明确规定应该怎样做.例如对函数参数的求值顺序.程序的运行结果依赖这种行为,会产生不可移植的代码.详查见索引
P154-----------------------------------------------------------------------------------------------------------------
6.10.3.3 ##操作符
约束
1 在宏定义的任何一种形式中,##不应出现在替换列表的开头或结尾位置①.
语义
2 如果在函数宏的替换列表中,一个形参立即跟在##预处理记号之前或之后,那么这个形参将被对应实参的预处理记号序列②所替换.然而,如果一个实参中不含预处理记号③,那么对应的形参将被一个占位符(placemarker)预处理记号所代替.145)
3 对于对象宏和函数宏两者的调用,替换列表中每个##预处理记号(而不是来自实参的##)被删除,并且它前面的预处理记号与后面的预处理记号相连接④.之后,它们的替换列表被重复检查,以让更多的宏名能被替换.占位符预处理记号被特殊处理:两个占位符连接的结果是一个占位符;一个占位符与一个非占位符连接的结果是那个非占位符.如果连接的结果不是一个合法的预处理记号,那么行为是未定义的.作为连接结果的记号还可用于更进一步的宏替换.对##操作符的求值顺序是未确定的.
4 例 对于下面的程序片段
#define hash_hash # ## #
#define mkstr(a) # a
#define in_between(a) mkstr(a)
#define join(c, d) in_between(c hash_hash d)
char p[] = join(x, y); // 等同于char p[] = "x ## y";
展开过程变化的阶段:
join(x, y)⑤
in_between(x hash_hash y)
in_between(x ## y)
mkstr(x ## y)
"x ## y"
换句话说,宏hash_hash的展开产生了一个新的记号,它含有两个明显相邻的符号##,但这个新的记号并不是##操作符.
------------------------------
145) 占位符预处理记号未出现在语法中,因为它们是临时实体,仅在翻译阶段4出现.
① 这意味着不能直接将##定义成一个宏,例如下面写法错误:#define MACRO ##,正确写法是:#define MACRO # ## #,见本页第4点的例子
② 而不是实参扩展后的结果,即使对应的实参也是个宏,例如
#define connect(x) i ## x
#define connect2(x) connect(x)
#define s(a) a
#define is(a) a
int i2=2;
printf("%d/n", connect(s(1)) );/*connect的形参x是##的操作数,故不展开它对应的实参s,直接连接记号i和实参序列s(1),得到is(1),继续替换得到最后结果1*/
printf("%d/n", connect2(s(2)) );/*connect2的形参x不是##的操作数,故先展开它对应的实参s,再用展开结果2替换之,得到connect(2),继续替换得到最后结果i2*/
③ 比如实参为空
④ 注意预处理记号是不含空白符的,因此##操作符与它的2个操作数(即前后2个形参)间的空白符被忽略,就像普通表达式中那样
⑤ 对于宏调用join(x, y)的展开顺序:
先用实参x,y替换join的形参,join的替换列表变为in_between(x hash_hash y).然后重复扫描替换列表,发现in_between(x hash_hash y)仍是宏,于是先扩展实参中的宏hash_hash,得到in_between(x ## y),接着用展开结果x ## y替换in_between对应的形参a,得到mkstr(x ## y).进一步展开得到最终结果"x ## y"
P155-----------------------------------------------------------------------------------------------------------------
6.10.3.4 重复扫描和进一步替换
1 在替换列表中所有形参都被替换﹑#和##处理都已发生之后,便删除所有的占位符.然后,结果预处理记号序列,连同源文件的所有后继预处理记号一起,被重复扫描,以使更多的宏名被替换①.
2 如果正在替换的宏的名字在扫描替换列表(尚未扫描到源文件的其它预处理记号部分时)过程中又被发现的话,该宏名不再被替换②.进一步说,如果任何嵌套的替换过程内遇到了正在替换的宏的名字,那么该宏名不再被替换③.这些不替换的宏名记号,对再次扫描﹑进一步的替换过程亦不再可用,即使稍后它们将在上下文中被重复检查,而在这些上下文中,它们可能会另作替换④.
3 对于最终完全替换的结果序列,即使它像是一条预处理指令,也不会再步当作预处理指令去处理⑤.但对于结果序列中所有_Pragma一元操作符表达式,会按照下面6.10.9[P161]所指定的方式被处理.
[小结:下面的宏替换算法虽不够严密,却有助于理清思路]⑥
6.10.3.5 宏定义的作用范围
1 宏定义的作用范围独立于语句块结构⑦,它持续直到遇到一条对应的#undef指令,如果一条也没有遇到,则一直持续到预处理翻译单元的结束.在翻译阶段4结束之后,这些宏定义便不再具有意义.
2 一条如下形式的预处理指令
# undef 标识符 换行符
使指定的标识符不再是一个被定义的宏名.如果指定的标识符并不是当前已定义的宏名,那么这条指令被忽略.
3 例1 对这种工具最简单的应用是定义一个"显而易见的常量",就像这样
#define TABSIZE 100
int table[TABSIZE];
4 例2 下面定义了一个函数宏,它的取值是所给实参中的最大者.它的优点是,可以运行在任何类型一致或相容的二个实参上,并且生成内联的代码﹑没有函数调用的开销.它的缺陷是,会对某个实参或其它实参求值2次(包含副作用),并且如果多次调用的话,会生成比函数调用更多的代码.也不能对它取地址,因为它没有地址.
#define max(a, b) ((a) > (b) ? (a) : (b))
圆括号保证实参及结果表达式能被括号适当地限定⑧.
------------------------------
① 例如
#define s(a) a
#define t(b) b
printf("%d/n", s(t)(3) );/*先将s(t)替换成t,然后t连同后继预处理记号(3)一起重复扫描,这时发现t(3)又是宏,将其替换成最后结果3*/
② 例如:
#define puts(s) printf("%s is ",#s);puts(s));//利用这一特性,用自定义宏改写库函数puts
char translator[]={-70,-6,-47,-27,0};
puts(translator);
由于扫描puts的替换列表时,又遇到了正在替换的宏名puts,故不再替换,展开结果为printf("%s is",translator);puts(translator)
③ 例如:
int M=3;
#define M N
#define N M*M
printf("%d/n",M);/*在替换宏M的过程中又发现了宏N,那么对宏N替换的过程,相对于外层M的替换过程来说,便是嵌套的替换过程.由于此过程中又遇到了正在替换的宏名M,故每次都不再替换,最终展开结果是M*M,输出结果为9*/
注意:要区分“嵌套的替换过程”与“宏的嵌套调用”.
嵌套的替换过程指在替换列表中又出现了(内嵌的)宏,因此需要(对内嵌宏)进行深层的替换,由浅入深,层层入内.
宏的嵌套调用指在函数宏的实参中又出现了宏(函数宏或对象宏),因此需要先展实参中的宏(内层宏),再展开外层的(函数)宏,由内而外,层层爆开,就像函数调用时实参的求值那样.
例如对下面用法:
#define DOUBLE(a) a*2
printf("%d/n", DOUBLE(DOUBLE(2)) );//输出8
不能这样理解: 当扫描到第1个DOUBLE(外层的)时,便进入宏DOUBLE替换的过程.
当扫描到第2个DOUBLE(内层的)时,由于当前正在替换(外层的)DOUBLE,便不再替换内层的DOUBLE了.因为通常的语法分析过程不是这样的,而是:
(1)当扫描到第1个DOUBLE(时,继续移进
(2)当移进了DOUBLE(DOUBLE(2)时,识别出DOUBLE(2)为一次宏调用,归约并执行相应语义动作,通常是展开DOUBLE(2),得到2*2,并替换掉原DOUBLE(2).此时移进内容变为DOUBLE(2*2
(3)继续移进右括号),得到DOUBLE(2*2),识别出DOUBLE(2*2)为一次宏调用,归约并执行相应语义动作,展开DOUBLE(2*2),得到最终结果2*2*2.
④ P156例3有相关例子
⑤ 这意味着不能指望用宏来代替一条预处理指令,例如下面用法都是错误的.错误原因还可另见P146第2点,P147第7点:
#define INCL_STD #include <stdio.h>//试图用宏代替一条#include指令
INCL_STD//错误
#define ENDIF #endif//试图用宏代替一条#endif指令
#if 1
ENDIF//错误
#define DEF_PI #define PI 3.1415926//试图用宏代替一条#define指令
DEF_PI//错误
⑥ void expand(x)//宏x替换算法
{
if(x是函数宏),则对x的替换列表中,每次形参p的出现都执行下面的步骤
{
if(p对应的实参不是对象宏 && p对应的实参不构成一次函数宏的调用 //即:p对应的实参不是宏
|| p此次作为##的操作数而出现)
{
将p替换成对应实参的拼写序列
}
else if(p此次作为#的操作数而出现)
{
将p连带它的#操作符一起替换成p对应实参的拼写序列
}
else//p对应的实参a是宏
{
expand(a);//递归调用expand展开宏a
将p替换成a展开后的结果序列
}
}
if(x的替换列表中含有##)
{
将##连同它前后的空白符一起删除; //记号连接
}
用x现在的替换列表,代替源文件中x的这次调用; //宏替换
将刚才对x替换的结果,连同源文件的下文一起重复扫描,这时如果遇到了一个宏名y
if(y是出现在下文中)
{
expand(y);//递归调用expand展开宏y
}
else//y是出现在替换列表中
{
if(y!=x //y不是当前正在替换的宏名
&& y未被标记为不可用) //且y不是父层替换过程正在替换的宏名
{
expand(y);//递归调用expand展开宏y
}
else
{
将y标记为不可用;//使y在子层替换过程中也不再替换
}
}
}