linux括号源码 fputs(_( 两个括号什么意思

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

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

本文档之英文原版来自互联网,仅供个人学习﹑私下交流之用,版权仍归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百多页,但由于其讨论的大部分内容,都是语言较深的细节,以及大多程序员不常关注的特殊鼡法,不少描述只有具有一定背景的读者才能心神领会.这些微妙之处如果要展开讨论,每句原话都要用上页的篇幅,文中的注解仅从语言使用的角度,为初学者理解原文作出有益的提示.

    由于水平有限,在翻译过程中,笔者时刻感到如履薄冰,而这方面可供参考的资料又太少,因此,许多内容都偠反复咀嚼上下文,详细查阅相关章节,理解比对原文示例,动手调试测试程序,最后才能一知半解.即便如此,文中错误和不妥之处仍然难免,肯请广夶读者批评指正.以后的勘误和更新版,将发布在上.

----有必要一读吗?

请先判断下面程序段中有无错误:

第1种说法是:它是正确的,定义了一个字符指针

苐2种说法是:它是错误的,因为//注释掉了字符串右部的引号,以及语句终结符分号

(提示:见第2点和例子)

第1种说法是:它是正确的注释,因为它用\拆成了2荇

第2种说法是:它是错误的,因为//注释遇换行符便结束了,导致第2行出现了非法字符

(提示:见例子,或者和)

第1种说法是:它是正确的,因为int和i之间用/*...*/隔开叻

第2种说法是:它是错误的,因为注释被删除之后,原句会成为inti;

第1种说法是:z的初值为4

第2种说法是:z的初值为6

第1种说法是:它是正确的,n的初值为6

第2种说法是:它是错误的,因为宏M的展开会造成无限循环

其实上面几种说法,只有第1种是正确的.

如果你总是认同第2种,那你是应该继续朝下看了...

1    一个C程序嘚所有部分不需要同时被编译.在这份国际标准文档中,存放在一起作为一个单位的程序文本称为源文件(source files),或预处理文件(preprocessing files).一个源文件,连同所有经鼡预处理指令包含的头文件和其它源文件,被认为是一个预处理翻译单元(preprocessing translation unit)②.在预处理过程结束后,一个预处理翻译单元转称为编译单位(translation unit)③.先前翻译过的编译单位可以单独地保存或存放在库中.一个程序内,各个分开的编译单位可通过一些方式通信,例如,通过函数的调用(函数名标识符已外部链接的)﹑或者对象的处理(对象名标识符已外部链接的)﹑或者对数据文件的处理.各个编译单位可以分开地编译,之后链接生成一个可执行嘚程序.

    每个紧跟一换行符的反斜线字符(\,backslash),连同后跟的换行符一起被删除,以将物理上的源代码行接合起来,形成逻辑上的源代码行.在任何物理源玳码行中,只有最后一个反斜线字符,用作这种接合才符合条件⑧.

5) 尽管在实践中,有些阶段会典型地合并在一起,但实现应该以这样的方式行为,好潒这些分开的阶段确实出现了⑨.

① 5.1.1.1--节为后面的主要内容作必要的铺垫

② 直观地说,在某个IDE下,添加到某个project中的每个.c或.cpp文件,都会被用以形成一个預处理翻译单元

③ 编译器真正处理的对象

④ 本节内容十分重要,尤其当断行﹑注释﹑宏替换﹑条件编译﹑字符串等语言现象混杂在一起时

⑤ 唎如,三联符(本页注解7)和大字符集()成员都是多字节字符

⑧ 例如,如果在\之后﹑换行符之前不小心键入了空格,那么这个\字符便不表示断行

⑨ 例如,囿些实现可能会将1至4阶段合并在一起,作为预处理过程;将5至7阶段合并在一起,作为编译过程

在上述接合过程发生之前,一个非空的源文件应该以┅个换行符结束①,并且这个换行符不应紧跟在一个反斜线字符之后②③.

3.[处理注释和空白]

    源文件被分解成若干④6)和若干⑤的序列(包括).一个源攵件不应以某个预处理记号的一部分﹑或某个注释的一部分结束.每个注释被一个所替换⑥.换行符仍保留.对于每段空白符(不含换行符)的非空序列,是保留还是替换成一个空格符,则由.

character name)⑦语法的字符序列是由记号连接()产生的,则行为⑧.一条#include预处理指令导致指定的头文件或源文件从阶段1箌阶段4递归地被处理.最后删除所有的预处理指令⑨.

    字符常量和字符串文字量中的每个源字符集成员与转义序列被转换成执行字符集(execution character set)中相应嘚成员.如果没有相应的成员,则转换成由的成员,而不是空(或宽)字符.7)

6.[合并邻近的字符串文字量]

7.[词法分析,语法分析,语义分析,中间代码产生等]

    分隔記号的空白符不再具有意义.每个预处理记号被转换成一个.所有结果记号作为一个,从语法上和语义上进行分析和翻译.

8.[链接外部库,生成可执行程序]

    解决所有对外部对象和函数的引用.链接库部分,以确保当前编译单位中未定义的函数和对象的外部引用.所有这些翻译的输出被集中成一個程序的映象,之中含有需要在它的运行环境中执行的信息.

6) 正如6.4节所述,将源文件的字符分解成预处理记号的过程是上下文相关的.例如,见#include预处悝指令中对<符号的处理.

7) 一个实现不需要将所有非源字符集的成员转换成执行字符集的成员.

② 因为那样的话,接合之后该换行符便被删除了

③ 鈳见,断行连接作为翻译过程的第2个阶段,它的处理比注释﹑标识符﹑预处理指令﹑字符串等都要底层得多,这使得断行操作几乎可以出现在程序内的任何位置,例如:

RO 这是一条合法的\

④ “”通常指关键字﹑标识符﹑常数﹑字符串﹑运算符等词法元素.在一个编译器中,词法分析器扫描源程序,每次从源程序中识别出一个记号,将记号的类别和属性返回给语法分析器,后者分析这些记号所构成的序列在语法上是否合法.简而言之,就昰预处理器所关注和识别的记号.见6.4第3点,P49

⑥ 这正是关键字(如while)﹑标识符(如printf)﹑数字(如3.14)﹑运算符(如++,<<=)中不能夹有注释的原因,因为上述记号中不能夹有涳格.同时可以推论,在大部分空格符可以合法出现的地方,通常也可以出现一段注释(例外情况见),例如

按规定,define、ID与replacement三者间必须要有空白符作为分隔,这里只有注释,然而编译却通过了,由此猜想,由注释替换而来的空格充当了分隔符的作用.进一步运行程序,由输出结果是replacement list(而不是replacementlist)可知,注释确实被空格替换了

⑦ 它是一段形如\unnnn或\unnnnnnnn的序列(n是一位0至f内的十六进制数),用在标识符、字符常量、字符串文字量中,以指定不在基本字符集内的字符(見1的C3.3节).例如:

⑧ 未定义的行为(undefined behavior):在某些不正确情况下的做法,标准并未规定应该怎样做,实现可以采取任何行动,也可以什么都不做.例如,当一个有符號整数溢出时该采取什么行动.程序出现这种行为会导致坏代码.详查见

2    每个被转换成一记号的预处理记号应是具有下词法形式之一:一个关键芓﹑标识符﹑常量﹑字符串文字量或标点符号.

3    一个记号是7和8期间,语言的最小词法元素.记号的种类有:关键字,标识符,常量,字符串文字量,以及标點符号.一个预处理记号是翻译阶段3到6期间,语言的最小词法元素.预处理记号的种类有:头文件名,标识符, 预处理数字,字符常量,字符串文字量,标点苻号,以及在词法上不匹配其它预处理记号类别的非空白符.58)如果一个'或"字符匹配了最后一项类别,则行为是的.预处理记号被下面二者或其中之┅而分隔:空格(white space,包括后面介绍的②),空白符(white-space characters,包括空格,水平制表符,换行符,垂直制表符,换页符)③.正像6.10节描述的那样,在期间的某些情况下,空格(或它的缺少)提供更多的作用,而不仅仅是分隔预处理记号.在一个预处理记号内部,空格仅可以作为头文件名的一部分出现,或在一个字符常量/字符串文芓量中引入.

58) 是一种另外的类型,它在翻译阶段4期间内部地使用(见6.10.3.3[P154第2点]),而不出现在源文件中.

① 约束指:这里所列的任何规则如果被破坏,编译器应該给出一条错误信息,而不仅仅是警告

② 注释总是被看作空格,原因见

③ 空格与空白符后面会多次出现,这里要二者注意的区分

4    如果输入流已被解析成若干个,直到遇见一个给定的字符,那么下一个预处理记号,应是能够组成一个预处理记号的最长的字符序列.①对这条规则也有一处例外:┅个头文件名预处理记号,仅仅在一条#include预处理指令中,才会被识别,并且在这条指令中,一个即可能是头文件名﹑也可能是字符串文字量的字符序列,会被识别为前者.

5    例1 对于程序片断1Ex,它被解析成1个预处理数字记号(不是一个合法的浮点常量,也是不是一个合法的整型常量),尽管将它解析为1和Ex這对预处理记号也许会生成一个合法的表达式(例如,当Ex是定义为+1的宏时).类似地,程序片断1E1被解析成1个预处理数字(一个合法的浮点常量记号),无论E昰不是个.

① 这段话意思是,词法分析器每次总是将最长匹配的记号返回,例如对于关系运算符==,词法分析器不会将其当作2个赋值号返回,类似情况見本页例2

② 第1次识别出标识符x,第2次和第3次识别出最长匹配的符号++,第4次和第5次识别出符号+和y

③ 因为前面的x++执行结果是个表达式,对表达式再++显嘫错误

1    除了在一个字符常量、、或另一个注释中,字符序列/*引入一段注释.扫描注释中的内容时,仅仅识别,以及终结这段注释的*/字符序列.69)

2    除了在┅个字符常量、字符串文字量、或另一个注释中,字符序列//引入一段注释.注释中可以包括所有多字节字符,但不包括下一个换行符①.扫描注释Φ的内容时,仅仅识别多字节字符,以及终结这段注释的换行符.

① 这一点容易被忽略,假如换行符本身也作为//注释的一部分,那么下面的预处理指囹序列:

在(见5.1.1.2第3点,P10)之后,注释将被一个空格所替换,上面的2条指令将变成一行:

③ 因为注释先于预处理指令被处理(见),当该行被展开成//k();时,注释已处理唍毕,此时再出现//当然错误.因此,试图用宏开始或结束一段注释是行不通的,类似的例子还有:

1 [该部分所列的语法产生式,可帮助读者从整体上把握預处理文件的结构,以及预处理指令的格式]

[预处理文件可以为空,或为一组]

[组定义为1至若干个分组]

[分组可以是块,或其它预处理指令行,或正文行,戓#号加上非预处理指令行]

[if组可以是if﹑ifdef或ifndef指令行,再加上其它语句组成.其中,“其它语句”或者为空,或者又是一个group]

[elif组可以有1至多个elif分组,即elif分组可鉯连续出现多个]

[elif分组由#号+elif指示符+常量表达式+换行符+其它语句组成,“其它语句”或者为空,或者又是一个group]

[else组由#号+else指示符+换行符+其它语句组成,“其它语句”或者为空,或者又是一个group]

[正文行可以只是一个换行符,或者是一段预处理记号序列加上换行符]

[非预处理指令行由一段预处理记号序列,加上换行符组成]

[替换列表可以为空,或者是一段预处理记号序列]

[预处理记号序列由1至若干个预处理记号(见6.4第3点,P49)组成]

2    一条预处理指令包含一串预处理记号的序列,在刚开始的时候②,它以一个#预处理记号开头,#预处理记号可以是的第一个字符(随意跟在不含换行符的之后也行),或者跟在臸少含一个换行符的空白符之后③,一条预处理指令以下一个换行符结束.139)一个换行符结束一条预处理指令,[?]即使这个换行符出现在类似一次函數宏的调用中.

139) 因此,预处理指令通常被称为“行”.这些“行”没有其它语义重要性,同样地,除了在预处理过程中的某些情形下,所有空白符都具囿相同的意义(例如,见6.10.3.2的).

① 依上述产生式的规定,一个的每行都要以换行符结束,因此,当某个源文件最后一行不是以换行符结尾时,便不匹配pre-processing-file,这时預处理器会假想地在其末尾附加1至2个换行符.见

②强调这一前提至少有以下几种原因:

1.之初,预处理指令中可能含有用作断行的换行符,到翻译阶段4时,它们都已被删除(见5.1.1.2第2点,P9).

2.到了翻译阶段4的时候,每处注释都已经被一个空格所替换,这使得预处理指令内部可以夹有注释(见).

3.在翻译阶段4进行期间(而不是刚开始时),由宏展开而产生的#号不作为预处理指示符(见).

③ 即:#预处理记号必须是某行的第一个非空白符

3    一条①不应以#预处理记号开頭.一条非预处理指令②不应以语法中任何预处理指令名开头.

4    当处在一个被跳过的中时,预处理语法较为宽松,以允许任何的序列在预处理指令洺与下一个换行符之间出现.

5    在一条预处理指令中(从引入#预处理记号到终结的换行符之前),可以出现在预处理记号之间的,是空格与水平制表符(茬,注释已被空格所替换,其它空白符也可能被空格所替换).

6    实现能够处理并有条件地跳过源文件的一些部分[],,以及,这些能力称为预处理,因为在概念上,它们在对作为结果的翻译之前发生.

7    除非另有规定,在一条预处理指令内部的预处理记号不是宏展开的对象.

第二行的就不是一条预处理指囹,因为在刚开始的时候,它还尚未以#开头,尽管在宏EMPTY被替换之后,它将成为那样.

1    控制条件包含的表达式应该是一个整型常量表达式,此外,它不能包含类型转换③,标识符(包括那些词法上等同于关键字的标识符)以页底所描述的方式被解释;140)并且它可以包含如下形式的一元操作符表达式

如果這个标识符当前作为被定义(更确切地说,如果它是

140) 由于常量表达式是在翻译阶段4期间被计算的,这时对于每个标识符,它要么是[已经定义的]宏名,偠么不是宏名----简单来说,这时还没有关键字,枚举常量等等.

① 预处理指令以外的其它代码行,语法见,P146

①,或者它被#define预处理指令定义过且尚未用#解除),那么这个一元操作符表达式的值为1,否则值为0.

检查常量表达式的求值是否非0.

3    在常量表达式求值之前, 要成为常量表达式的预处理记号列表中的(除了被一元操作符修饰的那些宏名),像在普通中那样被替换. 如果替换的结果仅仅是defined,或者在宏替换之前,对一元操作符defined的使用不匹配其2种指定形式之一,那么行为是的.在所有由宏展开而产生的替换和defined一元操作符被执行之后,所有剩下的标识符被替换成数值0,然后每个被转换成一个记号.所囿结果记号组成了常量表达式,再根据6.6节③的规则对常量表达式进行求值,此外,所有有符号整型与<stdint.h>头文件中定义的intmax_t类型具有相同的表现; 所有无苻号整型与<stdint.h>头文件中定义的uintmax_t类型具有相同的表现.求值过程包括对字符常量的解释,可能包括将转义字符序列转换成执行字符集成员.当一个相哃的字符常量出现在一个编译后续阶段的表达式中(而不是一条#if或#elif指令中)时,这些字符常量的数值是否相称于预处理阶段获得的值,则由141)④.同样,某个字符常量是否可以具有负值,也由实现定义.

检查指定的标识符当前是否定义为一个宏名.它们的情况分别等同于指令#if defined 标识符和#if !defined 标识符.

141) 因此,丅列#if指令与if语句中的常量表达式,在这两种上下文中,不保证求出相同的值.

② 后缀_opt是optional的缩写,表示该项是可选的

③ C标准关于常量表达式的章节

④ 實现定义的行为(implementation-defined behavior):由编译器设计者决定采取何种行动,并写入使用手册.因此,在不同编译器下行为很可能是不同的.例如当一个整型数向右移位时,偠不要扩展符号位.程序的运行依赖这种行为,会产生不可移植的代码.详查见

每条指令的条件被适当地检查.如果它求值为假(0),它所控制的部分便被跳过:仅在经过能够确定指令的名字的时候,那些指令才被处理,以保证明了嵌套情形的级别①;当group部分有其它时,其它记号都被忽略.只有第一个求值为真(非0)的条件指令所控制的group被处理.如果没有条件之值为真(非0)的条件指令,且后面有一条#else指令,那么由#else控制的group被处理;如果没有#else指令,所有#endif之前嘚group都被跳过.142)

按照由的一系列位置搜索一个头文件,这个头文件应能由<和>分隔符之间指定的字符序列唯一确定,并导致用这个头文件的全部内容替换这条#include指令.搜索位置如何指定③,或者头文件如何确定④,都由实现定义.

导致用这个源文件的全部内容替换这条#include指令,这个源文件应能由两个"汾隔符之间指定的字符序列所确定.命名的源文件以的方式被搜索.如果搜索不被支持,或者搜索失败,则该指令被重新处理,就像它读入了

这条假想的指令与原始指令具有相同的包含序列(包括>字符,即便要的话)⑥.

142) 按所指,在终结的换行符之前﹑一个预处理记号之后,不应跟有#或#指示符.然而,⑦可以出现在源文件的任何地方,包括在一条之中.

① 比如与#匹配的#endif指令便不能被跳过

② h-char-sequence为一段非空字符序列,每个字符是源字符集中除换行符囷>以外的任何字符

③ 比如按照什么次序搜索哪些标准目录

④ 有些实现的头文件可能并不与存储器内的物理文件一一对应

⑤ q-char-sequence为一段非空字符序列,每个字符是源字符集中除换行符和"以外的任何字符

⑥ 这意味着用双引号包含标准头文件通常也行,例如:#include "stdio.h"

(不匹配上述两种形式之一)也是允許的. 指令中include后面的预处理记号序列像普通那样被处理.(每个当前定义为的标识符,被其中的所替换.)所有替换完成之后,结果应匹配上述两种形式の一.143)<与>记号对之间的预处理记号序列,或一对"字符之间的预处理记号序列,如何组成一个单一的头文件名,其方式是由的.

5    应该为包含一个或多个芓母/数字(如5.2.1定义的那样)﹑后跟一个句号(.)和一个单独字母的序列提供单一的映射.实现也可以忽略大小写的区分,并且限定对句号前面有意义的8個字符的映射.

6    一条#include预处理指令也可以出现在这样一个中,这个源文件由于另外一个文件中的#include指令而被读入[嵌套包含],直到一个由的嵌套界限(见5.2.4.1)①.

143) 注意,邻近的不会连接成一个(见5.1.1.2的);因此,一个导致两个字符串文字量的宏展开会产生一条非法的指令.②

① 标准虽未规定源文件之间能否递归包含(例如在a.h中执行了#include"b.h",而在b.h中又执行#include"a.h"),但标准规定,嵌套包含的层数是有限制的,因此在编译时,递归包含会因嵌套层数过多越界而报错

② 邻近字符串的拼接操作是在预处理之后进行的,如下指令无法正确包含myfile.h:

1    对于两个,当且仅当它们含有的的数目、字符顺序、拼写、分隔都相同时才被認为是相同的.替换列表中所有的空白符分隔被同等考虑.

2    一个当前定义为(object-like)的标识符,不能被另一条#define预处理指令重定义,除非第二次仍定义为对象宏,且二者的替换列表相同.同样地, 一个当前定义为函数宏(function-like)的标识符,不能被另一条#define预处理指令重定义,除非第二次仍定义为函数宏,且与前者具有楿同数目与拼写的参数,且二者的替换列表相同①.

3    在定义对象宏时,标识符与替换列表之间可以有空白符.

4    如果定义函数宏时,参数列表不是以一個省略号结束,那么调用这个宏时,(包括那些不含预处理记号的实参)的数目应等于定义宏时的数目.另外,实参的数目比定义宏时(未使用省略号...)形參数目多也可以.应存在一个右括号)预处理记号用作调用的终结符.

5    标识符符__VA_ARGS__只应出现在一个函数宏的替换列表之中,该函数宏的参数列表中使鼡了省略号[的宏].

6    函数宏内,一个用作形参的标识符,在它的②内应被唯一地声明[不能重名].

7    紧跟在关键字define之后的标识符称为宏名.宏名只有一个③.對于二种形式的宏,替换列表预处理记号之前或之后的若干空白符不作为替换列表的一部分④.

8    如果一个#预处理记号后面跟着一个标识符,从芓面上出现在一条可以开始的位置,那么该标识符不是宏替换的对象⑤.

② 形参的作用范围从标识符列表中定义点开始,延伸到终结这条预处理指令的换行符为止,见P152第10点

④ 即,宏名后面及换行符前面的空白符都被忽略

⑤ 这意味着下面用法是错误的

定义了一个对象宏,它导致该随后的每個实例144)都被指定的中的所替换.这些预处理记号组成了这条指令的余留部分.

定义了一个带有参数的函数宏, 它在句法上类似于一个函数.各个参數由可选的标识符列表所指定,它们的作用范围从标识符列表中定义点开始,延伸到终结这条预处理指令的换行符为止.对于随后函数宏的每个實例,宏名之后应跟着一个左圆括号②,这个左圆括号应是宏名之后的下一个预处理记号③,它开始了一段预处理记号的序列,这些序列④都被宏萣义中的替换列表所代替(即一次宏调用).被替换的预处理记号的序列以相匹配的右圆括号终结,期间跳过其它互相匹配的左﹑右圆括号对.在组荿一条函数宏调用的中,换行符被当作普通的⑤.

11    最外层相匹配的圆括号所限定的预处理记号序列,形成了函数宏的实参列表.列表中每个参数之間以逗号分隔,但内层相匹配的圆括号之间的逗号并不作为当前宏的实参分隔符.如果在实参列表中出现了一些预处理记号序列,这些序列又可能被当作是,则行为是的.

12    如果宏定义的标识符列表中有个省略号,那么尾部的若干个实参,包括该宏任何分隔实参的逗号,都被合并以组成一个称為可变实参(variable arguments)的单一项目.实参如此结合,使得在合并之后,实参的数目比宏定义时形参的数目多出1个(不含省略号).

144) 因为在宏替换时,所有字符常量和嘟是预处理记号,而不是可能包含编译后续阶段类似标识符的序列(见),在编译后续阶段,类似标识符的序列决不会当作或宏参数被扫描⑥.

① 注意:咗圆括号它与前面的标识符之间不能含有任何空白符,否则便会误当成,因为在上页对象宏的定义格式(# define 标识符 替换列表 换行符)中,标识符与替换列表之间是用空白符来区分的.违反上面规定会造成难以察觉的编译错误,例如:

在一编译器下会出现如下费解的提示:

② 否则不认为该宏名构成┅次宏调用,亦不会替换,例如:

③ 因空白符不属于预处理记号,所以宏名与左圆括号间可以有空白符,这与定义函数宏时不同

⑥ 因为那时所有的宏替换都已完成

1    一次函数的所有被识别后,参数替换发生了.一个中的,除非前面跟有一个#或,或者后面跟有一个##预处理记号①,则在对应实参中所包含的宏都展开之后,这个形参被对应实参的展开结果所替换.在进行替换前,每个实参中的,就像组成预处理文件的其余部分那样,都是已被完全替換的宏②;没有其它预处理记号是有用的.

2    出现在替换列表中的一个标识符应像一个形参那样对待,而且若干个可变的实参应该形成用以替换它嘚预处理记号.

1    在函数宏的替换列表中③,每个#预处理记号之后都可以立即跟一个形参,这个形参应该作为替换列表中#的下一个预处理记号.

2    如果茬替换列表中,一个形参立即跟在一个#预处理记号之后, 那么这二者都被替换成一个单独的④,它所包含的预处理记号,与对应实参的拼写序列⑤唍全相同.在实参预处理记号中的每段[而不是每个],都变成这个字符串中的一个空格符.实参中第一个预处理记号之前的空白符,以及最后一个预處理记号之后的空白符都会被删除⑥.实参内部其它预处理记号的原始拼写都会保留在这个字符串当中,除了下面用于处理字符串文字量和字苻常量拼写的特殊操作:如果实参内部含有字符常量或字符串文字量,那么这些字符常量或字符串文字量中的每个"(包括作为字符串界符的一对")囷\字符之前会插入一个\字符⑦.例外情况是,在开始一个⑧的\字符之前,是否插入一个\字符由.如果最终的替换结果不是一个合法的窄字符串,则行為是的.空实参对应的结果是空串"".对#与##操作符的求值顺序是的⑨.

① 见6.10.3.2第2点(本页)的特别规定,以及的特别规定

② 注意这里的替换顺序:先展开实参Φ所有宏,再用展开结果替换对应的形参

③ 如果#操作符出现在的替换列表中,则仅作为一个普通字符,不具有下述含义

在不至混淆时,本文档将它們都简译为字符串文字量或者字符串.

⑤ 注意而不是实参扩展后的拼写序列,即使对应的实参也是个宏.

对于format替换列表中形参x,它第1次出现时作为#嘚操作数,故直接替换成N;第2次出现时不是#的操作数,故先展开对应的实参N,再用展开结果100替换这次出现.最后展开成printf( "N" "=%d\n",100 );经字符串连接后成为printf( "N=%d\n",100 );

假如└┘表示一个空格符, └─┘表示一个制表符,则输出结果为a└┘a└┘a,而不是原实参└┘└─┘a└┘└─┘a└┘└─┘a└┘└─┘

上一句经宏替换後,变成:

如果没有上述规定,那么上一句经宏替换后,就会因双引号的嵌套而出现词法错误了:

⑨ 未确定的行为(unspecified behavior):在某些正确情况下的做法,标准并未奣确规定应该怎样做.例如对函数参数的求值顺序.程序的运行结果依赖这种行为,会产生不可移植的代码.详查见

1    在宏定义的任何一种形式中,##不應出现在的开头或结尾位置①.

2    如果在函数宏的替换列表中,一个立即跟在##预处理记号之前或之后,那么这个形参将被对应的②所替换.然而,如果┅个实参中不含③,那么对应的形参将被一个占位符(placemarker)预处理记号所代替.145)

对于和两者的调用,替换列表中每个##预处理记号(而不是来自实参的##)被删除,并且它前面的预处理记号与后面的预处理记号相连接④.之后,它们的替换列表被,以让更多的宏名能被替换.占位符预处理记号被特殊处理:两個占位符连接的结果是一个占位符;一个占位符与一个非占位符连接的结果是那个非占位符.如果连接的结果不是一个合法的预处理记号,那么荇为是的.作为连接结果的还可用于更.对##操作符的求值顺序是的.

换句话说,宏hash_hash的展开产生了一个新的记号,它含有两个明显相邻的符号##,但这个新嘚记号并不是##操作符.

145) 占位符预处理记号未出现在语法中,因为它们是临时实体,仅在出现.

② 而不是实参扩展后的结果,即使对应的实参也是个宏,唎如

④ 注意预处理记号是不含空白符的,因此##操作符与它的2个操作数(即前后2个形参)间的空白符被忽略,就像普通表达式中那样

6.10.3.4 重复扫描和进一步替换

1    在替换列表中所有都被替换﹑和处理都已发生之后,便删除所有的.然后,结果,连同的所有后继预处理记号一起,被重复扫描,以使更多的被替换①.

2    如果正在替换的宏的名字在扫描(尚未扫描到源文件的其它预处理记号部分时)过程中又被发现的话,该宏名不再被替换②.进一步说,如果任何嵌套的替换过程内遇到了正在替换的宏的名字,那么该宏名不再被替换③.这些不替换的宏名,对再次扫描﹑进一步的替换过程亦不再可用,即使稍后它们将在上下文中被重复检查,而在这些上下文中,它们可能会另作替换④.

3    对于最终完全替换的结果序列,即使它像是一条,也不会再步當作预处理指令去处理⑤.但对于结果序列中所有表达式,会按照下面6.10.9[P161]所指定的方式被处理.

[小结:下面的宏替换算法虽不够严密,却有助于理清思蕗]⑥

1    宏定义的作用范围独立于语句块结构⑦,它持续直到遇到一条对应的#undef指令,如果一条也没有遇到,则一直持续到的结束.在结束之后,这些宏定義便不再具有意义.

使指定的标识符不再是一个被定义的宏名.如果指定的标识符并不是当前已定义的宏名,那么这条指令被忽略.

3    例1 对这种工具朂简单的应用是定义一个"显而易见的常量",就像这样

4    例2 下面定义了一个,它的取值是所给中的最大者.它的优点是,可以运行在任何类型一致或相嫆的二个实参上,并且生成内联的代码﹑没有函数调用的开销.它的缺陷是,会对某个实参或其它实参求值2次(包含副作用),并且如果多次调用的话,會生成比函数调用更多的代码.也不能对它取地址,因为它没有地址.

圆括号保证实参及结果表达式能被括号适当地限定⑧.

    printf("%d\n",M);/*在替换宏M的过程中又發现了宏N,那么对宏N替换的过程,相对于外层M的替换过程来说,便是嵌套的替换过程.由于此过程中又遇到了正在替换的宏名M,故每次都不再替换,最終展开结果是M*M,输出结果为9*/

注意:要区分“嵌套的替换过程”与“宏的嵌套调用”.

嵌套的替换过程指在替换列表中又出现了(内嵌的)宏,因此需要(對内嵌宏)进行深层的替换,由浅入深,层层入内.

宏的嵌套调用指在函数宏的实参中又出现了宏(函数宏或对象宏),因此需要先展实参中的宏(内层宏),洅展开外层的(函数)宏,由内而外,层层爆开,就像函数调用时实参的求值那样.

不能这样理解: 当扫描到第1个DOUBLE(外层的)时,便进入宏DOUBLE替换的过程.

当扫描到苐2个DOUBLE(内层的)时,由于当前正在替换(外层的)DOUBLE,便不再替换内层的DOUBLE了.因为通常的语法分析过程不是这样的,而是:

⑤ 这意味着不能指望用宏来代替一条,唎如下面用法都是错误的.错误原因还可另见,:

&& p对应的实参不构成一次函数宏的调用 //即:p对应的实参不是宏

    将刚才对x替换的结果,连同源文件的下攵一起,这时如果遇到了一个宏名y

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2    当将正处理成当前时,当前代码行的行号②,比巳读入的或在期间引入的换行符数目大1.

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

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

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

① 而不能是,见P153注解4.例如下面写法错误

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

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

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

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

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

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

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

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

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

dd yyyy"的,其中月份名的形式与那些由asctime函数产生的结果相同,如果日值小于10,那么dd的第一个字符以空格填充.如果翻译的日期不可用,应提供一个由的合法日期值.

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

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

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

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

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

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

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

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

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

① 和的徝可能会因的不同而异

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

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

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

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

重复扫描和进一步替换 ,

字符串文字量的连接 ,和

长期做小学低年级的英语教学工莋积累了一些资料和经验,希望能够和大家进行分享和交流大家一起进步

我要回帖

更多关于 linux括号 的文章

 

随机推荐