C语言预处理的原理:明示常量:#define

#define预处理器指令和其他预处理器指令一样,以#号作为一行的开始。ANSI和后来的标准都允许#号前面有空格或制表符,而且还允许在#和指令的其余部分之间有空格。但是旧版本的C要求指令从一行最左边开始,而且#和指令其余部分之间不能有空格。指令可以出现在源文件的任何地方,其定义从指令出现的地方到该文件末尾有效。我们大量使用#define指令来定义明示常量(manifest constant)(也叫作符号常量),但是该指令还有许多其他用途。程序preproc.c演示了#define指令的一些用法和属性。

/* preproc.c -- simple preprocessor examples */
#include 
#define TWO 2        /* you can use comments if you like   */
#define OW "Consistency is the last refuge of the unimagina
tive. - Oscar Wilde" /* a backslash continues a definition */
                     /* to the next line                   */
#define FOUR  TWO*TWO
#define PX printf("X is %d.

int main(void)
{
    int x = TWO;
​
    PX;
    x = FOUR;
    printf(FMT, x);
    printf("%sn", OW);
    printf("TWO: OWn");
​
    return 0;
}

预处理器指令从#开始运行,到后面的第1个换行符为止。也就是说,指令的长度仅限于一行。然而,前面提到过,在预处理开始前,编译器会把多行物理行处理为一行逻辑行。每行#define(逻辑行)都由3部分组成。第1部分是#define指令本身。第2部分是选定的缩写,也称为宏。有些宏代表值(如本例),这些宏被称为类对象宏(object-likemacro)。C语言还有类函数宏(function-like macro),稍后讨论。宏的名称中不允许有空格,而且必须遵循C变量的命名规则:只能使用字符、数字和下划线(_)字符,而且首字符不能是数字。第3部分(指令行的其余部分)称为替换列表或替换体(见图16.1)。一旦预处理器在程序中找到宏的示实例后,就会用替换体代替该宏(也有例外,稍后解释)。从宏变成最终替换文本的过程称为宏展开(macro expansion)。注意,可以在#define行使用标准C注释。如前所述,每条注释都会被一个空格代替。


C语言预处理的原理:明示常量:#define

Parts of an object-like macro definition

运行该程序示例后,输出如下:

X is 2.
X is 4.
Consistency is the last refuge of the unimaginative. - Oscar Wilde
TWO: OW

下面分析具体的过程。下面的语句:

int x = TWO;

变成:

int x = 2;

2代替了TWO。而语句:

PX;

变成:

printf("X is %d.n", x);

这里同样进行了替换。这是一个新用法,到目前为止我们只是用宏来表示明示常量。从该例中可以看出,宏可以表示任何字符串,甚至可以表示整个C表达式。但是要注意,虽然PX是一个字符串常量,它只打印一个名为x的变量。下一行也是一个新用法。读者可能认为FOUR被替换成4,但是实际的过程是:

x = FOUR;

变成:

x = TWO*TWO;

即是:

x = 2*2;

宏展开到此处为止。由于编译器在编译期对所有的常量表达式(只包含常量的表达式)求值,所以预处理器不会进行实际的乘法运算,这一过程在编译时进行。预处理器不做计算,不对表达式求值,它只进行替换。 注意,宏定义还可以包含其他宏(一些编译器不支持这种嵌套功能)。 程序中的下一行:

printf (FMT, x);

变成了:

printf("X is %d.n",x);

相应的字符串替换了FMT。如果要多次使用某个冗长的字符串,这种方法比较方便。另外,也可以用下面的方法:

const char * fmt = "X is %d.n";

然后可以把fmt作为printf()的格式字符串。下一行中,用相应的字符串替换OW。双引号使替换的字符串成为字符串常量。编译器把该字符串存储在以空字符结尾的数组中。因此,下面的指令定义了一个字符常量:

#define HAL 'Z'

而下面的指令则定义了一个字符串(Z):

#define HAP "Z"

在程序示例16.1中,我们在一行的结尾加一个反斜杠字符使该行扩展至下一行:

#define OW "Consistency is the last refuge of the unimagina
tive. - Oscar Wilde"

注意,第2行要与第1行左对齐。如果这样做:

#define OW "Consistency is the last refuge of the unimagina
    tive. - Oscar Wilde"

那么输出的内容是:

Consistency is the last refuge of the unimagina    tive. - Oscar Wilde

第2行开始到tive之间的空格也算是字符串的一部分。一般而言,预处理器发现程序中的宏后,会用宏等价的替换文本进行替换。如果替换的字符串中还包含宏,则继续替换这些宏。唯一例外的是双引号中的宏。因此,下面的语句:

printf("TWO: OW");

打印的是TWO: OW,而不是打印:

2: Consistency is the last refuge of the unimaginative. - Oscar Wilde

要打印这行,应该这样写:

printf("%d: %sn", TWO, OW);

这行代码中,宏不在双引号内。

那么,何时使用字符常量?对于绝大部分数字常量,应该使用字符常量。如果在算式中用字符常量代替数字,常量名能更清楚地表达该数字的含义。如果是表示数组大小的数字,用符号常量后更容易改变数组的大小和循环次数。如果数字是系统代码(如,EOF),用符号常量表示的代码更容易移植(只需改变EOF的定义)。助记、易更改、可移植,这些都是符号常量很有价值的特性。 C语言现在也支持const关键字,提供了更灵活的方法。用const可以创建在程序运行过程中不能改变的变量,可具有文件作用域或块作用域。另一方面,宏常量可用于指定标准数组的大小和const变量的初始值。

#define LIMIT 20
const int LIM = 50;
static int data1[LIMIT];    // valid
static int data2[LIM];      // not required to be valid
const int LIM2 = 2 * LIMIT; // valid
const int LIM3 = 2 * LIM;   // not required to be valid

这里解释一下上面代码中的“无效”注释。在C中,非自动数组的大小应该是整型常量表达式,这意味着表示数组大小的必须是整型常量的组合(如5)、枚举常量和sizeof表达式,不包括const声明的值(这也是C++和C的区别之一,在C++中可以把const值作为常量表达式的一部分)。但是,有的实现可能接受其他形式的常量表达式。例如,GCC4.7.3不允许data2的声明,但是Clang 4.6允许。

1 记号

从技术角度来看,可以把宏的替换体看作是记号(token)型字符串,而不是字符型字符串。C预处理器记号是宏定义的替换体中单独的“词”。用空白把这些词分开。例如:

#define FOUR 2*2

该宏定义有一个记号:2*2序列。但是,下面的宏定义中:

#define SIX 2 * 3

有3个记号:2、*、3。替换体中有多个空格时,字符型字符串和记号型字符串的处理方式不同。考虑下面的定义:

#define EIGHT 4    *    8

如果预处理器把该替换体解释为字符型字符串,将用4 * 8替换EIGHT。即,额外的空格是替换体的一部分。如果预处理器把该替换体解释为记号型字符串,则用3个的记号4 *8(分别由单个空格分隔)来替换EIGHT。换而言之,解释为字符型字符串,把空格视为替换体的一部分;解释为记号型字符串,把空格视为替换体中各记号的分隔符。在实际应用中,一些C编译器把宏替换体视为字符串而不是记号。在比这个例子更复杂的情况下,两者的区别才有实际意义。 顺带一提,C编译器处理记号的方式比预处理器复杂。由于编译器理解C语言的规则,所以不要求代码中用空格来分隔记号。例如,C编译器可以把2*2直接视为3个记号,因为它可以识别2是常量,*是运算符。

2 重定义常量

假设先把LIMIT定义为20,稍后在该文件中又把它定义为25。这个过程称为重定义常量。不同的实现采用不同的重定义方案。除非新定义与旧定义相同,否则有些实现会将其视为错误。另外一些实现允许重定义,但会给出警告。ANSI标准采用第1种方案,只有新定义和旧定义完全相同才允许重定义。具有相同的定义意味着替换体中的记号必须相同,且顺序也相同。因此,下面两个定义相同:

#define SIX 2 * 3
#define SIX 2       *    3

这两条定义都有3个相同的记号,额外的空格不算替换体的一部分。而下面的定义则与上面两条宏定义不同:

#define SIX 2*3

这条宏定义中只有一个记号,因此与前两条定义不同。如果需要重定义宏,使用#undef指令(稍后讨论)。如果确实需要重定义常量,使用const关键字和作用域规则更容易些。

展开阅读全文

页面更新:2024-04-18

标签:常量   编译器   数组   表达式   记号   空格   字符串   变量   指令   字符   定义   原理   大小   过程   语言

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top