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

C/C++预处理 ISO/ANSI C标准译文与注解

2015-06-30 16:29 295 查看
转载:/article/8672730.html

<<ISO/ANSI C标准译文与注解 C/C++预处理部分>>

内容简介:本文档完整翻译了C标准(99版)中预处理和相关章节的内容,并在许多必要之处附加了注解和程序示例,以帮助读者理解标准原文,同时制作了详细的中英文索引备查。

译者:胡彦

出处:http://blog.csdn.net/huyansoft

如果转载,请保留译者和出处信息,谢谢!

本文同时制作了HTML格式的文档,可在http://download.csdn.net/source/476473下载。HTML文档的好处在于,其中制作了大量的链接,阅读时可以随时点击链接方便地跳转、前进和后退。

原先发布的CHM格式的文档(地址是http://download.csdn.net/source/468852),由于下载后无法打开,请不要再下载了。

郑重声明:

本文档之英文原版来自互联网,仅供个人学习﹑私下交流之用,版权仍归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在子层替换过程中也不再替换

}

}

}

⑦ 见P9的翻译阶段.例如:

void fun2();

void fun1()

{

#define MACRO1 "MACRO1"//此宏的作用范围从此行开始往下

}

void main()

{

puts(MACRO1);//正确,尽管看起来定义MACRO1的fun1()还未调用过

fun2();

puts(MACRO2);//错误,MACRO2未定义,尽管看起来fun2()已经调用过了

}

void fun2()

{

#define MACRO2 "MACRO2"//此宏的作用范围从此行开始往下

}

⑧ 以避免实参之间﹑结果表达式与上下文之间产生不期望的结合

P156-----------------------------------------------------------------------------------------------------------------

5 例3 举例说明重定义与重检查规则,下面的指令序列①

#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

#define p() int

#define q(x) x

#define r(x,y) x ## y

#define str(x) # x

f(y+1) + f(f(z)) % t(t(g)(0) + t) (1); ②③④

g(x+(3,4)-w) | h 5) & m

(f)^m(m); ⑤

p() i[q()] = { q(1), r(2,3), r(4,), r(,5), r(,) }; ⑥

char c[2][6] = { str(hello), str() };

经宏替换后,将产生如下结果

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);

int i[] = { 1, 23, 4, 5, };

char c[2][6] = { "hello", "" };

6 例4 举例说明字符串的生成与记号连接的规则,下面的指令序列

#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 // 来自前面#include的例子[见P150例2]

#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') // 此注释将被删除

== 0) str(: @/n), s); ⑦

#include xstr(INCFILE(2).h) ⑧

glue(HIGH, LOW); ⑨

xglue(HIGH, LOW) ⑩

经宏替换后,将产生如下结果

------------------------------

① 理解本例十分必要.要正确编译这段代码,还需在这段代码之前,添加类似下面的变量与函数定义,函数体中的语句可以随意:

int y=1;

int z[1]={1};

int f(int a)

{

return a==0 ? 1 : a;

}

int t(int a)

{

return a;

}

int m(int a,int b)

{

return b;

}

② 对f(y+1) 展开过程:

用实参y+1替换对应的形参,得到f(x*(y+1)).重复扫描替换结果,发现f虽是宏,但却是当前正在处理的宏,故不再递归替换(见6.10.3.4第2点,P155),继续扫描发现x是宏,将其用当前的替换列表2(而不是3)替换,结果为f(2*(y+1)).

由宏x的替换可见,当在扫描宏定义的替换列表时,不会将其中的宏名立刻替换(尽管那样做效率高),而是由某次宏调用触发了所有的替换过程.所以,宏x的定义不必在f定义之前出现.例如,将上面程序中#define x 3一行删除,运行结果仍然相同.类似的情况适用于函数宏:

#define m1(x) m2(x)//宏m1调用宏m2,尽管这时m2还未定义

#define m2(x) x

printf("%d/n",m1(5));//输出5.此时m2已作为宏定义过了

为简便起见,下面注解中对宏x的展开过程不再复述.

③ 对f(f(z))的展开过程:

1.替换f(f(z))的实参f(z)

11.替换f(z)的实参z

111.将z替换成z[0],重复扫描z[0],发现z是当前正在处理的宏,故不再递归替换,并将z标记为不可用(见6.10.3.4第2点,P155).

112.得到z的替换结果z[0].

12.用实参z展开的结果z[0]替换对应的形参,得到f(2*(z[0])).重复扫描替换结果,不再递归替换其中的宏f,宏z已在111中标记为不可用,也不再替换.

13.得到f(z)的替换结果f(2*(z[0])).

2.用实参f(z)展开的结果f(2*(z[0]))替换对应的形参,得到f(2*(f(2*(z[0])))).重复扫描替换结果,不再递归替换其中的宏f,宏z已在111中标记为不可用,也不再替换.

3.于是得到最终结果f(2*(f(2*(z[0])))).

④ 对t(t(g)(0)+t)(1)的展开过程:

1.展开t(t(g)(0)+t)宏调用部分

11.展开t(t(g)(0)+t)的实参t(g)(0)+t

111.展开t(g)宏调用部分

1111.替换t(g)的实参g,得到f.重复扫描f,由于f未带括号,故不认为是宏调用,不予展开(见P152注解1).

1112.用实参g展开的结果f替换对应的形参,得到f.重复扫描f,由于f未带括号,故不认为是宏调用,不予展开.

1113.得到t(g)的替换结果f.

112.将t(g)展开的结果f,连同后续上下文(0)一起重复扫描(见6.10.3.4第1点,P155),发现f(0)是宏,于是展开宏f(0)

1121.用f(0)的实参0替换对应的形参,得到f(2*(0)).重复扫描替换结果,发现f虽是宏,却是当前正在处理的宏,故不再递归替换f(2*(0)),并将f标记为不可用(见6.10.3.4第2点,P155).

1122.得到f(0)的替换结果f(2*(0)).

113.将t(g)展开的结果f(2*(0)),连同后续上下文+ t一起重复扫描,由于f已在1121中标记为不可用,t未带括号,故对f(2*(0))和t都不再替换.

114.得到t(g)(0)+t的替换结果f(2*(0))+t.

12.用实参t(g)(0)+t展开的结果f(2*(0))+t替换对应的形参,得到f(2*(0))+t.重复扫描替换结果,由于f已在1121中标记为不可用,t未带括号,故对f(2*(0))和t都不再替换.

13.得到t(t(g)(0)+t)的替换结果f(2*(0))+t.

2.将t(t(g)(0) + t)展开的结果,连同后续上下文(1)一起重复扫描,即重复扫描f(2*(0)) + t(1),由于f已在1121中标记为不可用,故对f(2*(0))不再替换.t是当前正在处理的宏,故对t(1)不再递归替换.

3.于是得到最终结果f(2*(0))+t(1).

⑤ 对m(m)的展开过程:

1.试图展开m(m)的实参m.由于m未带括号,故不认为是宏调用,不予展开(见P152注解1).

2.用实参m替换对应的形参,得到m(w).重复扫描,发现m虽是宏,却是当前正在处理的宏,故不再递归替换m(见6.10.3.4第2点,P155).继续扫描发现w是宏,展开w得到0,1.

3.于是得到最终结果m(0,1)

⑥ 由这里可以看出,调用函数宏时,可以省略部分乃至全部实参,可以省略用于分隔实参的逗号.但不能连宏名后的括号也省略了

⑦ 注意:str(: @/n)内的/之前不会插入/字符,见6.10.3.2第2点,P153

⑧ 对于调用xstr(INCFILE(2).h)的展开过程:

1.展开xstr(INCFILE(2).h)的实参INCFILE(2).h

1.1.实参INCFILE(2).h中INCFILE是宏,故需先展开INCFILE(2).用INCFILE(2)的实参2替换对应的形参,得到vers2

1.2.将INCFILE(2)的展开结果vers2,连同后续上下文.h一起重复扫描(见6.10.3.4第1点,P155),未发现需要替换的宏

1.3.得到INCFILE(2).h的展开结果vers2.h

2.用实参INCFILE(2).h展开的结果vers2.h替换对应的形参,得到str(vers2.h).重复扫描,发现str是宏,于是直接用str的实参序列vers2.h生成字符串(见6.10.3.2第2点,P153),得到"vers2.h"

3.于是得到最终结果"vers2.h"

⑨ 对于调用glue(HIGH, LOW)的展开过程:

实参LOW是宏,但由于它对应的形参是##的操作数,故不予展开.而是直接连接实参序列HIGH和LOW(见6.10.3.3第2点,P154),得到HIGHLOW.重复扫描替换结果(见6.10.3.4第1点,P155),发现HIGHLOW仍是宏,进一步展开得到最后结果"hello".

⑩ 对于调用xglue(HIGH, LOW)的展开过程:

1.展开xglue(HIGH, LOW)中的实参LOW,得到LOW ", world".重复扫描替换结果,发现LOW虽是宏,却是当前正在处理的宏,故不再递归替换(见6.10.3.4第2点,P155).于是得到LOW的替换结果LOW ", world".

2.用实参HIGH,和实参LOW的展开结果替换对应的形参,得到glue(HIGH, LOW ", world").重复扫描,发现glue是宏,于是再继续展开glue(HIGH, LOW ", world").

3.glue的实参LOW ", world"中含有宏,但由于它对应的形参是##的操作数,故不予展开.而是直接连接2个实参序列HIGH和LOW ", world",得到HIGHLOW ", world".重复扫描替换结果,发现HIGHLOW仍是宏,进一步展开得到最终结果"hello" ", world".(这2个邻近的字符串将在后续编译阶段被连接成一个,见P10第6点)

P157-----------------------------------------------------------------------------------------------------------------

printf("x" "1" "= %d, x" "2" "= %s", x1, x2);

fputs(

"strncmp(/"abc//0d/", /"abc/", '//4') == 0" ": @/n",

s);

#include "vers2.h" (在宏替换之后﹑访问文件之前①)

"hello";

"hello" ", world"

或者,在字符串连接之后,

printf("x1= %d, x2= %s", x1, x2);

fputs(

"strncmp(/"abc//0d/", /"abc/", '//4') == 0: @/n",

s);

#include "vers2.h" (在宏替换之后﹑访问文件之前①)

"hello";

"hello, world"

在宏定义中,#和##记号左右两边的空格是可选的.

7 例5 举例说明“占位符”预处理记号的相关规则,下面的指令序列

#define t(x,y,z) x ## y ## z

int j[] = { t(1,2,3), t(,4,5), t(6,,7), t(8,9,),

t(10,,), t(,11,), t(,,12), t(,,) };

经宏替换后,将产生如下结果

int j[] = { 123, 45, 67, 89,

10, 11, 12, };

8 例6 演示宏的重定义规则,下面的指令序列是合法的

#define OBJ_LIKE (1-1)

#define OBJ_LIKE /* 空白 */ (1-1) /* 其它字符 */

#define FUNC_LIKE(a) ( a )

#define FUNC_LIKE( a ) ( /* 空白注释 */ /

a /* 此行的其它东西

*/ )

但下面的重定义是非法的:[假如已有了上面4条指令,则再出现下面指令是非法的]

#define OBJ_LIKE (0) // 记号序列不同[前面的替换列表是(1-1)]

#define OBJ_LIKE (1 - 1) // 空白符不同[减号前后多出了空白符]

#define FUNC_LIKE(b) ( a ) // 使用的形参不同[前面的形参是a]

#define FUNC_LIKE(b) ( b ) // 参数拼写不同[前面的替换列表是( a )]

9 例7 最后,展示带有可变参数列表的宏:

#define debug(...) fprintf(stderr, __VA_ARGS__)

#define showlist(...) puts(#__VA_ARGS__)

#define report(test, ...) ((test)?puts(#test):/

printf(__VA_ARGS__))

debug("Flag");

debug("X = %d/n", x);

showlist(The first, second, and third items.);

report(x>y, "x is %d but y is %d", x, y);

------------------------------

① 包含了该文件后,这条#include指令便被删除了

P158-----------------------------------------------------------------------------------------------------------------

经宏替换后,将产生如下结果

fprintf(stderr, "Flag" );

fprintf(stderr, "X = %d/n", x );

puts("The first, second, and third items." );

((x>y)?puts("x>y"):

printf("x is %d but y is %d", x, y))

6.10.4 Line指令

约束

1 一条#line指令中给出的字符串文字量,应该是一个窄字符串文字量①.

语义

2 当将源文件正处理成当前记号时,当前代码行的行号②,比已读入的或在翻译阶段1期间引入的换行符数目大1.

3 一条如下形式的预处理指令

# line 数字序列 换行符

使实现做出如下行为:好像源文件下一行的行号以指定的数字开始,这个数字由“数字序列”指定③.“数字序列”被解释为十进制整数,它不能是0,也不能大于2147483647.

4 一条如下形式的预处理指令

# line 数字序列 "s-char-sequence_opt" 换行符 ④

类似地设置假定的行号,并且将源文件名假定地改为字符串所指定的内容.

5 一条如下形式的预处理指令

# line 预处理记号序列 换行符

(即不匹配上述两种形式之一)是允许的.在这条指令中,line之后的预处理记号序列像普通正文那样被处理(每个当前定义为宏名的标识符,被其替换列表中的预处理记号所替换).在所有替换完成之后,结果应匹配上述两种形式之一,然后它以适当方式被处理⑤.

------------------------------

① 而不能是宽字符串文字量,见P153注解4.例如下面写法错误

#line 10 L"myprogram.c"

② 这里的行号指物理上的代码行,而不是经翻译阶段2处理之后逻辑上的代码行,见P9第2点

③ 即,如果这里给出的数字是n,这条指令使下面的行依次被编号为n,n+1,n+2...等等

④ s-char-sequence_opt是括在双引号内的0至若干个字符的序列(不带前缀L),每个字符是源字符集中除换行符﹑双引号"﹑反斜线/以外的任何字符.

⑤ 类似的情况见前面的#include指令,P150第4点

P159-----------------------------------------------------------------------------------------------------------------

6.10.5 Error指令

语义

1 一条如下形式的预处理指令

# error 预处理记号序列_opt 换行符

使得实现产生一条诊断信息,其中包含指定的“预处理记号序列”①.

6.10.6 Pragma指令

语义

1 一条如下形式的预处理指令

# pragma 预处理记号序列_opt 换行符

如果指令中,pragma之后未紧跟着预处理记号STDC(在任何宏替换发生之前)146),那么这条指令的行为依实现定义.可能会导致翻译失败,也可能会使编译器或目标程序的行为不一致.任何实现不能识别的pragma指令都被忽略②.

2 如果指令中,pragma之后紧跟着预处理记号STDC(在任何宏替换发生之前),那么对这条指令不会执行宏替换,并且这条指令应该是下列形式之一147),这些形式的含义在别处描述:

#pragma STDC FP_CONTRACT on-off-switch

#pragma STDC FENV_ACCESS on-off-switch

#pragma STDC CX_LIMITED_RANGE on-off-switch

on-off-switch:下列之一

ON OFF DEFAULT

向前参考:the FP_CONTRACT pragma(7.12.2),the FENV_ACCESS pragma(7.6.1),the CX_LIMITED_RANGE pragma(7.3.4).

146) 实现对pragma指令不需要执行宏替换,但这么做也是允许的,除了在标准pragma指令(STDC紧跟在pragma之后的)中.如果在一条非标准pragma指令中执行了宏替换后,其结果与标准pragma指令形式相同,那么行为仍然由实现定义;也允许实现把它当作一条标准pragma指令去行为.

147) 看“语言未来方向”(6.11.8).

------------------------------

① 预处理记号序列可以为空,这意味着写成“# error 换行符”也可以

② 这意味着如果#pragma指令中出现了拼写错误,或者包含了无法识别的命令,编译器必须忽略它们,而不允许报错.

P160-----------------------------------------------------------------------------------------------------------------

6.10.7 空指令

语义

1 一条如下形式的预处理指令

# 换行符

不产生任何结果.

6.10.8 预定义的宏名

1 实现应定义下列宏名148):

__DATE__ 对预处理翻译单元翻译的日期:一个形式为"Mmm dd yyyy"的窄字符串文字量,其中月份名的形式与那些由asctime函数产生的结果相同,如果日值小于10,那么dd的第一个字符以空格填充.如果翻译的日期不可用,应提供一个由实现定义的合法日期值.

__FILE__ 当前源文件③假定的文件名(一个窄字符串文字量).149)

__LINE__ 当前代码行在当前源文件内②假定的行号(一个整型常量).149)

__STDC__ 整数1,以指示当前是一个符合标准的实现.

__STDC_HOSTED__ 如果操作系统存在,则值为整型常数1,否则为整型常数0.

__STDC_VERSION__ 整型常数199901L.150)

__TIME__ 对预处理翻译单元翻译的时间:一个形式为"hh:mm:ss"的窄字符串文字量,与asctime函数产生的结果相同.如果翻译的时间不可用,应提供一个由实现定义的合法时间值.

2 实现可以有条件地定义下列宏名:

__STDC_IEC_559__ 整型常数1,以指示与附录F(IEC60559浮点运算)中的规范一致.

------------------------------

148) 看“语言未来方向”(6.11.9).

149)假定的源文件名和行号能用#line指令改变.

150)该宏在ISO/IEC 9899:1990中未指定,在ISO/IEC 9899/AMD1:1995中指定为199409L.目的是保持它为一个long int整型常数,并且它的值随着这份国际标准的修订不断增加.

① 而不是当前的预处理翻译单元.例如:在Translation_unit.c中使用#include指令包含了头文件header.h,那么一进入header.h,__FILE__便立刻指向header.h,而不是原先的值

② 而不是当前的预处理翻译单元.例如:在Translation_unit.c中使用#include指令包含了头文件header.h,那么一进入header.h,__LINE__便立刻改为1,而不再按原先的行号递增

P161-----------------------------------------------------------------------------------------------------------------

__STDC_IEC_559_COMPLEX__ 整型常数1,以指示对附录G(IEC60559兼容的复数运算)中规范的支持.

__STDC_ISO_10646__ 一个形式为yyyymmL的整型常数(例如199712L),以指示ISO/IEC10646规范的年月形式的值,并从指定的年月开始,随着所有改正和技术上的勘误而更新.

3 预定义宏的值(除__FILE__和__LINE__外),在贯穿当前编译单位的处理期间是不变的①.

4 上述预定义的宏名,以及defined标识符,不能用#define指令重定义,不能被#undef指令解除定义.

任何其它的预定义的宏名都应以一个下划线开头,后跟一个大写字母或跟第二个下划线.

5 实现不应定义__cplusplus宏,也不应该在任何标准头文件中定义它②.

向前参考:asctime函数(7.23.3.1),标准头文件(7.1.2).

6.10.9 Pragma操作符

1 一个如下形式的一元操作符表达式:

_Pragma (字符串文字量)

被如下处理:对这个字符串文字量,删除前缀L(如果有的话)和头尾的一对双引号,每个转义字符/"被替换成一个双引号,每个转义字符//被替换成一个反斜线字符.这些结果字符序列再经翻译阶段3处理,解析成若干预处理记号,之后这些预处理记号就像在一条pragma指令中那样被执行.最后删除原始的一元操作符表达式中4个预处理记号.

2 例 一条如下形式的预处理指令:

#pragma listing on "../listing.dir"

也可表示成这样:

_Pragma ( "listing on /"..//listing.dir/"" )

后一种形式会以相同方式被处理,无论它是像上面那样字面地出现,还是由类似下面的宏替换产生:

#define LISTING(x) PRAGMA(listing on #x) ③

#define PRAGMA(x) _Pragma(#x)

LISTING ( ../listing.dir )

------------------------------

① __DATE__和__TIME__的值可能会因预处理翻译单元的不同而异

② 因为这是C实现.同样,C++实现也不定义__STDC__宏

③ 调用了当前尚未定义的PRAGMA宏,这样做是可以的,见P156注解2

-----------------------------------------------------------------------------------------------------------------

术语辨析

注意下面每组术语的联系与区别:

source files(源文件) P9第5.1.1.1节第1点, P145第1点

preprocessing files(预处理文件)P9第5.1.1.1节第1点,P145第1点

preprocessing translation unit(预处理翻译单元) P9第5.1.1.1节第1点

translation unit(编译单位) P9第5.1.1.1节第1点

white space(空格) P49第3点

white-space characters(空白符) P49第3点

string literal(字符串文字量) P153注解4

character string literal(窄字符串文字量) P153注解4

wide string literal(宽字符串文字量) P153注解4

token(记号) P49第1--3点

preprocessing token(预处理记号) P49第1--3点,P10注解4

断行符/ P9第2点,P10注解3

反斜线符/ P9第2点,P10注解3,P153第6.10.3.2节第2点

#预处理记号 P146第2点

#操作符 P153第6.10.3.2节

define指令 P151第9点,P152第10点

defined操作符 P147--P148第6.10.1节第1点,P161第4点

object-like macro(对象宏) P151--P152第9点

function-like macro(函数宏) P152第10点

parameters(形参) P152第10点

argument(实参) P152第11点

嵌套的替换过程 P155注解3,P155第6.10.3.4节第2点

宏的嵌套调用 P155注解3

pragma指令 P159第6.10.6节

_Pragma操作符 P161第6.10.9节

空行 P146的text-line产生式

null directive(空指令) P160第6.10.7节

unspecified behavior(未确定的行为) P153注解9,P153第6.10.3.2节第2点,P154第3点

undefined behavior(未定义的行为) P10注解8,P10第4点,P49第3点,P66第3点,P148第3点,P152第11点,P153第6.10.3.2节第2点,P154第3点

implementation-defined behavior(实现定义的行为) P148注解4,P9第5.1.1.2节第1点,P10第3点,P10第5点,P148第3点,P149第2点,P149第3点,P150第4点,P150第6点,P153第6.10.3.2节第2点,P159第6.10.6节第1点,P159注解146,P160第6.10.8节第1点

-----------------------------------------------------------------------------------------------------------------

索引

#操作符 P153第6.10.3.2节

#预处理记号 P146第2点

##操作符 P154第6.10.3.3节

( P152第10点和注解1,P146关于lparen的产生式

*/ P66第6.4.9节,P10注解⑥

, P152第11点,P156注解6

... P151第4--5点,P152第12点

/* P66第6.4.9节,P10注解⑥

// P66第6.4.9节,P10注解⑥

/ P9第2点,P10注解3,P153第6.10.3.2节第2点

_Pragma操作符 P161第6.10.9节

__cplusplus宏 P161第5点

__DATE__ P160第6.10.8节

__FILE__ P160第6.10.8节

__LINE__ P160第6.10.8节

__STDC__ P160第6.10.8节

__STDC_HOSTED__ P160第6.10.8节

__STDC_IEC_559__ P160第6.10.8节

__STDC_IEC_559_COMPLEX__ P161第6.10.8节

__STDC_ISO_10646__ P161第6.10.8节

__STDC_VERSION__ P160第6.10.8节

__TIME__ P160第6.10.8节

__VA_ARGS__ P151第5点,P153第6.10.3.1节第2点,P157例7

A

argument P152第11点

argument list P152第11点

argument substitution P153第6.10.3.1节,P153第6.10.3.2节第2点,P154第2点,P155注解6

B

backslash P9第2点,P10注解3,P153第6.10.3.2节第2点

编译单位 P9第5.1.1.1节第1点

标准pragma指令 P159注解146

C

character string literal P153注解4

comma P152第11点,P156注解6

comments P66第6.4.9节,P10注解⑥

conditional inclusion P147--P149第6.10.1节

constant expression P147--P149第6.10.1节

constraint P49注解1

__cplusplus宏 P161第5点

参数替换 P153第6.10.3.1节,P153第6.10.3.2节第2点,P154第2点,P155注解6

常量表达式 P147--P149第6.10.1节

程序结构 P9第5.1.1.1节

重定义宏 P151第2点,P156例3,P156注解2,P157例6,P161第4点

重复扫描和进一步替换 P155第6.10.3.4节,P156例3

词法元素 P49--P50第6.4节

D

__DATE__ P160第6.10.8节

defined操作符 P147--P148第6.10.1节第1点,P161第4点

define指令 P151第9点,P152第10点

diagnostic message P159第6.10.5节

递归包含 P150注解1

递归的宏定义 P155第6.10.3.4节第2点和注解2

大字符集 P10注解7

单行注释 P66第6.4.9节,P10注解⑥

逗号 P152第11点,P156注解6

断行符(/) P9第2点,P10注解3

对象宏 P151--P152第9点

多行注释 P66第6.4.9节,P10注解⑥

多字节字符 P9第5.1.1.2节第1点

E

elif指令 P148第2点,P145第1点

ellipsis P151第4--5点,P152第12点

else指令 P149第5点,P145第1点

endif指令 P149第5点,P145第1点

error指令 P159第6.10.5节

F

__FILE__ P160第6.10.8节

function-like macro P152第10点

翻译阶段 P9第5.1.1.2节

反斜线字符(/) P9第2点,P10注解3,P153第6.10.3.2节第2点

G

group P145第1点

关键字 P10注解9

H

header name P149--P150第6.10.2节,P50第4点

函数宏 P152第10点

行号 P158第6.10.4节第2点和注解1

宏的重定义 P151第2点,P156例3,P156注解2,P157例6,P161第4点

宏的递归定义 P155第6.10.3.4节第2点和注解2

宏的嵌套调用 P155注解3

宏的形参 P152第10点

宏调用 P152第10点和注解2

宏定义的作用范围 P155第6.10.3.5节

宏名 P151第7点

宏名的名字空间 P155第6.10.3.5节

宏替换 P151--P158第6.10.3节

宏替换算法 P155注解6

换行符 P146第2点和注解1,P10第5.1.1.2节第2点

I

if指令 P148第2点,P145第1点

ifdef指令 P148第3点, P145第1点

ifndef指令 P148第3点, P145第1点

implementation P9注解6

implementation-defined behavior P148注解4,P9第5.1.1.2节第1点,P10第3点,P10第5点,P148第3点,P149第2点,P149第3点,P150第4点,P150第6点,P153第6.10.3.2节第2点,P159第6.10.6节第1点,P159注解146,P160第6.10.8节第1点

J

记号 P49第1--3点

记号连接 P154第3点,P156例4

K

keywords P10注解9

可变参数 P152第12点

可变实参 P152第12点

空白符 P49第3点

空格 P49第3点

空指令 P160第6.10.7节

宽字符串文字量 P153注解4

L

lexical elements P49--P50第6.4节

__LINE__ P160第6.10.8节

line指令 P158第6.10.4节

line number P158第6.10.4节第2点和注解1

lparen P152第10点和注解1,P146关于lparen的产生式

M

macro replacement P151--P158第6.10.3节

multibyte characters P9第5.1.1.2节第1点

N

new-line P146第2点和注解1,P10第5.1.1.2节第2点

non-directive P145

null directive P160第6.10.7节

O

object-like macro P151--P152第9点

opt后缀 P148注解2

P

parameters P152第10点

placemarker P154第2--3点,P157例5

pp-tokens P145

pragma directive P159第6.10.6节

_Pragma操作符 P161第6.10.9节

pragma指令 P159第6.10.6节

predefined macro names P160--P161第6.10.8节

preprocessing directives P145--P161第6.10节

preprocessing files P9第5.1.1.1节第1点,P145第1点

preprocessing token P49第1--3点,P10注解4

preprocessing translation unit P9第5.1.1.1节第1点

program structure P9第5.1.1.1节

Q

嵌套包含 P150第6点

嵌套的替换过程 P155注解3,P155第6.10.3.4节第2点

R

replacement-list P146,P151第1点

rescanning and further replacement P155第6.10.3.4节,P156例3

S

Scope of macro definitions P155第6.10.3.5节

single-line comment P66第6.4.9节,P10注解⑥

source files P9第5.1.1.1节第1点, P145第1点

source inclusion P149--P150第6.10.2节

space P49第3点

standard pragma P159注解146

__STDC__ P160第6.10.8节

__STDC_HOSTED__ P160第6.10.8节

__STDC_IEC_559__ P160第6.10.8节

__STDC_IEC_559_COMPLEX__ P161第6.10.8节

__STDC_ISO_10646__ P161第6.10.8节

__STDC_VERSION__ P160第6.10.8节

STDC P159第6.10.6节

string literal P153注解4

三联符 P9注解7

省略号 P151第4--5点,P152第10点,P152第12点

实参 P152第11点

实参的替换 P153第6.10.3.1节,P153第6.10.3.2节第2点,P154第2点,P155注解6

实参列表 P152第11点

实现 P9注解6

实现定义的行为 P148注解4,P9第5.1.1.2节第1点,P10第3点,P10第5点,P148第3点,P149第2点,P149第3点,P150第4点,P150第6点,P153第6.10.3.2节第2点,P159第6.10.6节第1点,P159注解146,P160第6.10.8节第1点

T

text-line P146第1点

__TIME__ P160第6.10.8节

token P49第1--3点

token connection P154第3点,P157例5

translation phases P9第5.1.1.2节

translation unit P9第5.1.1.1节第1点

trigraph P9注解7

替换列表 P146,P151第1点

替换列表相同 P151第1点

条件包含 P147--P149第6.10.1节

条件编译 P147--P149第6.10.1节

头文件 P149--P150第6.10.2节, P50第4点

U

undefined behavior P10注解8,P10第4点,P49第3点,P66第3点,P148第3点,P152第11点,P153第6.10.3.2节第2点,P154第3点

unspecified behavior P153注解9,P153第6.10.3.2节第2点,P154第3点

undef指令 P155第6.10.3.5节,P146

universal character name P10注解7

V

__VA_ARGS__ P151第5点,P153第6.10.3.1节第2点,P157例7

variable arguments P152第12点

W

white space P49第3点

white-space characters P49第3点

wide string literal P153注解4

未定义的行为 P10注解8,P10第4点,P49第3点,P66第3点,P148第3点,P152第11点,P153第6.10.3.2节第2点,P154第3点

未确定的行为 P153注解9,P153第6.10.3.2节第2点,P154第3点

文件包含 P149--P150第6.10.2节

X

相同的替换列表 P151第1点

形参的作用范围 P152第10点

形参 P152第10点

Y

源文件 P9第5.1.1.1节第1点,P145第1点

源文件包含 P149--P150第6.10.2节

语法分析 P155注解3

预处理翻译单元 P9第5.1.1.1节第1点

预处理记号 P49第1--3点,P10注解4

预处理记号序列 P146

预处理文件 P9第5.1.1.1节第1点,P145第1点

预处理指令 P145--P161第6.10节

预处理指令语法 P145--P146第6.10节

预定义的宏名 P160--P161第6.10.8节

约束 P49注解1

Z

窄字符串文字量 P153注解4

占位符 P154第2--3点,P157例5

诊断信息 P159第6.10.5节

正文行 P146第1点

注释 P66第6.4.9节,P10注解⑥

转义字符的处理 P10第5点

字符串 P153注解4

字符串产生操作符 P153第6.10.3.2节,P156例4

字符串文字量 P153注解4

字符串文字量的连接 P10第6点,P150注解143和注解2

-----------------------------------------------------------------------------------------------------------------

参考文献

The C++ Programming Language:该书附录C中可以找到编码字符集相关术语的解释.另外,附录A所列的C++词法规则和预处理语法也与C标准基本一致.

C专家编程:该书第1章概要介绍了C标准

C语言程序设计-现代方法:该书第14章对C预处理指令讲解地十分详细

C语言大全(第4版):该书将C89版与C99版内容分开介绍,使人一目了然C99版新增内容

译注者简介

胡彦,C++程序员,爱好OOP,Compiler.Blog地址http://blog.csdn.net/huyansoft

[全文完]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: