预计阅读本页时间:-
与所有预处理指令一样,预处理指令#define用#符号作为行的开头。ANSI标准允许#符号前有空格或制表符,而且该标准还允许在#和指令的其余部分之间有空格。但是旧版本的C一般要求指令在最左边一列开始,并且#和指令的其余部分之间不能有空格。指令可出现在源文件的任何地方。指令定义的作用域从定义出现的位置开始直到文件的结尾。本书的程序大量使用这个指令来定义符号常量或明显常量,但是它的应用范围远不止此。程序清单16.1举例说明了一些#define指令的用法和属性。
预处理器指令从#开始,到其后第一个换行符为止。也就是说,指令的长度限于一行代码。但是正如前文提到的,在预处理开始前,系统会删除反斜线和换行符的组合。因此可以把指令扩展到几个物理行,由这些物理行组成单个逻辑行。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
程序清单16.1 preproc.c程序
每个#define行(即逻辑行)由三部分组成。第一部分为指令#define自身。第二部分为所选择的缩略语,这些缩略语称为宏(macro)。像本例中的这些宏用来代表值,它们被称为类对象宏(object-like macro,C还有类函数宏,后面将讨论它们)。宏的名字中不允许有空格,而且必须遵循C变量命名规则:只能使用字母、数字和下划线(_),第一个字符不能为数字。第三部分(#define行的其余部分)称为替换列表(replacement list)或主体(body)(请参见图 16.1)。预处理器在程序中发现了宏的实例后,总会用实体代替该宏(有一种例外,以后进行说明)。从宏变成最终的替换文本的过程称为宏展开(macro expansion)。注意,可以使用标准的C注释方法在#define行中进行注释。正如前面提到的,在预处理器处理之前,每个注释都会被一个空格所代替。
图16.1 类对象宏定义的组成
运行示例程序,观察其工作情况:
下面分析具体过程。下面的语句:
其中2代替了TWO。而语句:
变成了:
该句同样发生了替换。这是一个新用法,因为到目前为止,我们只是使用宏代替常量。这里您可以看到,宏可以表示任何字符串,甚至可表示整个C表达式。但要注意,PX是个常量字符串,它只打印名为×的变量。
下面一行也提出了一些新情况。您可能认为用4代替了FOUR,但实际过程是:
变成了:
然后又变成:
宏展开过程在上句结束。因为C编译器在编译时对所有常量表达式(只包含常量的表达式)求值,所以,实际相乘过程发生在编译阶段,而不是预处理器工作阶段。预处理器不进行计算,它只是按照指令进行文字替换操作。
注意,宏定义中可包含其他宏(有些编译器不支持这种嵌套功能)。
下面一行:
变成了:
其中,用相应字符串替换了FMT。如果需要多次使用某个冗长的控制字符串,这种方法就比较方便。另外,也可采用下面的方法:
接下来,就可以把fmt用作printf()的控制字符串。
下面一行中,用相应字符串代替0W。双引号标志使替代字符串成为字符串常量。编译器把该字符串存储在以空字符结束的数组里。因此下面的语句定义了一个字符常量:
而下面的语句:
则是定义了一个字符串(Z\0)。
示例程序中,我们在一行结尾加反斜线符号以使该行扩展至下一行:
注意第二行要左对齐。相反,假设这样做:
则输出将会是:
这行开头和tive之间的空格也作为字符串的一部分。
一般而言,预处理器发现程序中的宏后,会用它的等价替换文本代替宏。如果该字符串中还包括宏,则继续替换这些宏。例外情况是双引号中的宏。因此下面的语句:
将打印出TWO: OW,而不是打印出:
要打印最后一行,可用这行代码:
这里的宏位于双引号之外。
什么时候应该使用符号常量呢?对大多数数字常量应该使用符号常量。如果是用于计算式的常量,那么使用符号名会更加清楚。如果数字代表数组大小,那么使用符号名后更容易改变数组大小和循环界限。如果数字是系统代码(如EOF),那么使用符号表示会使程序更加易于移植(只须改变EOF的定义)。记忆值的能力、易更改性、可移植性,这些功能使得符号常量很有使用价值。
const关键字得到C的支持,这确实提供了一种创建常量的更灵活的方法。使用const您可以创建全局常量和局部常量、数字常量、数组常量和结构常量。另一方面,宏常量可以用来指定标准数组的大小并作为const值得初始化值。
16.2.1 语言符号
从技术方面看,系统把宏的主体当作语言符号(token)类型字符串,而不是字符型字符串。C预处理器中的语言符号是宏定义主体中的单独的“词(word)”。用空白字符把这些词分开。例如:
这个定义中有一个语言符号:即序列2*2。但是:
这个定义中有三个语言符号:2、*和3。
在处理主体中的多个空格时,字符型字符串和语言符号类型字符串采用不同方法。考虑下面的定义:
把主体解释为字符型字符串时,预处理器用4 * 8替换EIGHT。也就是说,额外的空格也当作替换文本的一部分。但是,当把主体解释为语言符号类型时,预处理器用由单个空格分隔的三个语言符号,即4 * 8来替换EIGHT。换句话说,用字符型字符串的观点看,空格也是主体的一部分;而用语言符号字符串的观点看,空格只是分隔主体中语言符号的符号。在实际应用中,有些C编译器把宏主体当作字符串而非语言符号。在比这个实例更复杂的情况下,字符与语言符号之间的差异才有实际意义。
顺便提一下,C编译器处理语言符号的方式比预处理器的处理方式更加复杂。编译器能理解C的规则,不需要用空格来分隔语言符号。例如,C编译器把2*2当作三个语言符号。原因是C编译器认为每个2都是一个常量,而*是一个运算符。
16.2.2 重定义常量
假设您把LIMIT定义为20,后来在该文件中又把LIMIT定义为25。这个过程被称为重定义常量(redefining a constant)。不同编译器采用不同的重定义策略。在新定义不同于旧定义时,有的编译器认为这是错误,而有些编译器可能提出警告,但允许重定义。ANSI标准采用第一种方式:只允许新定义与旧定义完全相同。
相同定义意味着主体具有相同顺序的语言符号。因此,下面两个定义相同:
两者都有三个相同的语言符号,而且额外的空格不是主体的一部分。下面的定义则被认为是不同的:
上式只有一个(而非三个)语言符号,因此与前面两个定义不相同。可以使用#undef指令重新定义宏。稍后将讨论#undef指令。
如果确实需要重定义常量,使用const关键字和作用域规则可能会更容易。