13.8 预处理命令的其他话题
13.8.1 再谈宏
1.符号常量、枚举常量与const变量
通过“#define”预处理命令可以定义符号常量,这种常量由于在预处理过程中只进行简单的文字替换,所以显得十分直接和坦率。通常与问题有关的常数都可以用这种方法处理。比如,要编写一个计算24点的程序,那么这个24显然是描述问题的一个常数,就可以用符号常量的方式描述。
尽量使用抽象的性质来描述这种常量,比具体的名词来描述显得更专业些。
与
相比无疑后者更好写。因为后者具有更好的适应性,可以轻易地改为计算5点、6点的程序(对于有些问题,可以通过这个办法把问题规模减小再进行测试)。前者做这样的修改则显得生硬牵强,让人感到别扭。
枚举常量适合描述一组相关的散列常量,在for语句或switch语句中使用枚举常量可能非常漂亮自然。比如把星期几描述为枚举类型就非常自然。
使用枚举常量的另一个好处是,在程序调试时容易跟踪,符号常量由于在编译之前就已经被替换掉了,所以没办法跟踪。
但是枚举常量只是枚举变量的一个值域而已,归根到底还是要使用变量。在C语言中一个枚举类型的变量可以取这个值域之外的值。
还有一种有些像常量的变量——const变量。必须要说的是,const变量并非常量而是变量,是一种不可以显式改变(比如通过“=”、“++”、“——”等改变)的变量,把它理解为“只读变量”是比较靠谱的。
由于是变量,所以可以求得其指针。通过这个指针去修改相应的变量,在C标准中是一种未定义行为,也就是说修改这个变量还是有可能的。
const变量不像枚举常量那样可以写出漂亮的switch语句,因为变量不可以作为case的标号。
通常在两种情况下使用const变量:需要求指向这个变量的指针,指向符号常量的指针是没有办法求得的;在确信某个参数在函数中不应该被改变的前提下,把形参定义为const变量。这有两个好处:编译器会发现你对这个形参的误修改;编译器可以对代码进行合理的优化。
所以,尽管三者有一些相似的地方,然而它们最恰当的使用场合并不一样,这需要在实践中努力体会。
2.参数数目可变的宏(C99)
C99容许定义参数数目不定的宏。这些不确定的形参同样用“…”表示;由于这些形参没有名字,所以在对应的替换表中,这些参数出现的位置用预定义的宏“VA_ARGS”表示。例如:
那么在代码中出现的宏调用:
在宏展开之后就变成了:
有一点需要注意,就是C99要求“…”必须是最右边的,也就是最后一个参数。
3.预定义的宏
C标准规定编译器必须预先某些宏,这些宏如下所示。
DATE:替代为编译的日期。
FILE:替代为源文件的名称。
LINE:目前源代码的行数,从文件头开始算起。
TIME:编译的时间。
STDC:整数常量1,表示编译器遵循C标准。
STDC_VERSION:如果支持C99则为199901L
这些宏的名称都以两个下划线开头也以两个下划线结束,在自己定义宏时应注意避免取这样的名字。这些预定义的宏不可以通过“#undef”命令取消。
C99标准中还增加了另外一个宏,如下所示。
STDC_HOSTED:如果目前的实现版本是宿主环境,则为1;否则为0。
所谓“宿主环境”一般指程序是在某种操作系统下运行,这样的C程序都必须有且只有一个main()函数,这是程序执行的起点。非宿主环境是指程序不依赖操作系统而独立运行,这种情况下程序不一定要有main()函数,至于程序从那哪个函数开始执行也视具体的编译器而定。
4.分层展开的问题
可以用宏来定义另一个宏,比如:
也可以用宏调用作为另一个宏调用的参数,比如:
程序代码13-37
其中的“COS(PF(3.))”将被展开为—>(cos(PF(3.)))—>(cos(((3.)(3.))))。但是若希望得到“"(cos(((3.)(3.))))"”这样的字符串字面量却需要费一点周折。如果只是定义
那么,S(COS(PF(3.)))宏展开后得到的只是"COS(PF(3.))"。这时应该定义另外一个宏先将“COS(PF(3.))”展开,然后再把展开之后的内容转变成字符串字面量。
程序代码13-38
这时,S(COS(PF(3.)))被展开为—>S((cos(PF(3.))))—>S((cos(((3.)(3.)))))—>"(cos(((3.)(3.))))。这样,程序的输出如图13-7所示。
图13-7 分层展开的问题
13.8.2 其他编译预处理命令
1.内置的编译命令#pragram、_Pragma
#Pragma预处理命令的作用是给编译器提供一些额外的信息,基本上相当于IDE开发环境中的“编译选项”菜单的功能,比如结构体成员的对齐方式等。例如
表示的是结构体成员应该对齐到偶数地址。这条命令的一般使用形式如下。
各个编译器可以规定自己的“额外信息”的提供方式,这些提供方式显然不具备很好的可移植性。如果编译器在代码中碰到不认识的#Pragma指示则会忽略这条预处理命令。
C99增加了3个新的#pragma的使用方式,其中之一如下所示。
其中“on或off或default”,可以取值为“ON”、“OFF”或“DEFAULT”,分别表示浮点表达式的被处理的方式。
C99新增加的另外两条“# Pragma”命令如下所示。
前者关于浮点环境,后者关于复数计算。这里不打算给出更详细的说明。在这里想说的一件事情是,GNU的GCC编译器无视这个预处理命令,一旦遇到“#Pragma”预处理命令,GCC预处理器就会自动运行一个小游戏程序或者干脆停止编译。从这里不难看出在C语言界中许多人对“#pragma”这条预处理命令的态度和看法。
“_Pragma”是一个预处理运算符,其作用和“#Pragma”相似。不同之处在于“#Pragma”是一条预处理命令,它必须单独占据一行;而“_Pragma”则不受这个限制,并且它还可以很容易地通过宏展开实现“#pragma”命令的“参数化”。例如
的作用和
是一样的。
2.#error
这条命令的作用是停止预处理并输出一个错误信息,其一般格式如下所示。
这条命令通常结合条件编译预处理命令一起使用,用于检查代码中是否存在着不应该继续预处理然后编译下去的情况。例如
当预处理器发现编译器不符合C标准时将停止继续预处理,并输出“编译器不符合C标准。”这条信息。
3.#line
编译时产生的警告信息、错误信息以及程序调试时的信息通常会给出对应代码的位置(所在源文件、行号及所在函数的名字)。#line预处理命令的作用是重新指定代码的行号和文件的别名,其一般形式的语法如下所示。
在该条预处理命令之后,如果输出行号的信息,将不再是默认的自然行号,而是以“#line”命令指定的行号开始记数。如果同时指定了文件名,那么在输出文件名字的时候将使用新的别名而不是真正的文件名。下面的代码演示了这条命令的作用。
程序代码13-39
其中的“FILE”、“LINE”为标准所规定的预定义的宏,分别代表代码的文件名及行号。
这段程序的输出如图13-8所示。
图13-8 #line的作用