📄 第5章 编译预处理.txt
字号:
C语言编程常见问题解答
发表日期:2003年9月28日 已经有2865位读者读过此文
第5章 编译预处理
本章集中讨论与预处理程序有关的问题。在编译程序对程序进行通常的编译之前,要先运行预处理程序。可能你以前没有见过这个程序,因为它通常在幕后运行,程序员是看不见它的,然而,这个程序非常有用。
预处理程序将根据源代码中的预处理指令来修改你的程序。预处理指令(如#define)为预处理程序提供特定的指令,告诉它应该如何修改你的源代码。预处理程序读入所有包含的文件和待编译的源代码,经过处理生成源代码的预处理版本。在该版本中,宏和常量标识符已用相应的代码和值代替。如果源代码中包含条件预处理指令(如#if),预处理程序将先判断条件,然后相应地修改源代码。
预处理程序有许多非常有用的功能,例如宏定义,条件编译,在源代码中插入预定义的环境变量,打开或关闭某个编译选项,等等。对专业程序员来说,深入了解预处理程序的各种特征,是创建快速和高效的程序的关键之一。
在阅读本章时,请记住本章采用的一些技术(以及所提到的一些常见陷阱),以便更好地利用预处理程序的各种功能。
5.1 什么是宏(macro)?怎样使用宏?
宏是一种预处理指令,它提供了一种机制,可以用来替换源代码中的字符串,宏是用“#define"语句定义的,下面是一个宏定义的例子:
#define VERSION—STAMP "1.02"
上例中所定义的这种形式的宏通常被称为标识符。在上例中,标识符VERSION_STAMP即代表字符串"1.02"——在编译预处理时,源代码中的每个VERSION_STAMP标识符都将被字符串“1.02”替换掉。
以下是另一个宏定义的例子:
#define CUBE(x)((x),(x)*(x))
上例中定义了一个名为CUBE的宏,它有一个参数x。CUBE宏有自己的宏体,即((x)*(x)*(x))——在编译预处理时,源代码中的每个CUBE(x)宏都将被((x)*(x)*(x))替换掉。
使用宏有以下几点好处;
(1)在输入源代码时,可省去许多键入操作。
(2)因为宏只需定义一次,但可以多次使用,所以使用宏能增强程序的易读性和可靠性。
(3)使用宏不需要额外的开销,因为宏所代表的代码只在宏出现的地方展开,因此不会引起程序中的跳转。
(4)宏的参数对类型不敏感,因此你不必考虑将何种数据类型传递给宏。
需要注意的是,在宏名和括起参数的括号之间绝对不能有空格。此外,为了避免在翻译宏时产生歧义,宏体也应该用括号括起来。例如,象下例中这样定义CUBE宏是不正确的:
denne CUBE(x) x * x * x
对传递给宏的参数也要小心,例如,一种常见的错误就是将自增变量传递给宏,请看下例:
#include <stdio. h>
#include CUBE(x) (x * x * x)
void main (void);
void main (void)
{
int x, y;
x = 5;
y = CUBE( + +x);
printfC'y is %d\n" . y);
}
在上例中,y究竟等于多少呢?实际上,y既不等于125(5的立方),也不等于336(6* 7*8),而是等于512。因为变量x被作为参数传递给宏时进行了自增运算,所以上例中的CUBE宏实际上是按以下形式展开的:
y = ((++x) * (++x) * (++x));
这样,每次引用x时,x都要自增,所以你得到的结果与你预期的结果相差很远,在上例中,由于x被引用了3次,而且又使用了自增运算符,因此,在展开宏的代码时,x实际上为8,你将得到8的立方,而不5的立方。
上述错误是比较常见的,作者曾亲眼见过有多年C语言编程经验的人犯这种错误。因为在程序中检查这种错误是非常费劲的,所以你要给予充分的注意。你最好试一下上面的例子,亲眼看一下那个令人惊讶的结果值(512)。
宏也可使用一些特殊的运算符,例如字符串化运算符“#”和。连接运算符“##”。“#”运算符能将宏的参数转换为带双引号的字符串,请看下例:
define DEBUG_VALUE(v) printf(#v"is equal to %d.\n",v)
你可以在程序中用DEBUG_VALUE宏检查变量的值,请看下例:
int x=20;
DEBUG_VALUE(x);
上述语句将在屏幕上打印"x is equal to 20"。这个例子说明,宏所使用的“#”运算符是一种非常方便的调试工具。
“##”运算符的作用是将两个独立的字符串连接成一个字符串,详见5.16。
请参见:
5.10 使用宏更好,还是使用函数更好?
5.16 连接运算符“##”有什么作用?
5.17 怎样建立对类型不敏感的宏?
5.18 什么是标准预定义宏?
5.31 怎样取消一个已定义的宏?
5.2 预处理程序(preprocessor)有什么作用?
C语言预处理程序的作用是根据源代码中的预处理指令修改你的源代码。预处理指令是一种命令语句(如#define),它指示预处理程序如何修改源代码。在对程序进行通常的编译处理之前,编译程序会自动运行预处理程序,对程序进行编译预处理,这部分工作对程序员来说是不可见的。
预处理程序读入所有包含的文件以及待编译的源代码,然后生成源代码的预处理版本。在预处理版本中,宏和常量标识符已全部被相应的代码和值替换掉了。如果源代码中包含条件预处理指令(如#if),那么预处理程序将先判断条件,再相应地修改源代码。
下面的例子中使用了多种预处理指令:
# include <stdio. h>
# define TRUE 1
# define FALSE (!TRUE)
# define GREATER (a, b) ((a) > (b) ? (TRUE) : (FALSE))
# define PIG-LATIN FALSE
void main (void);
void main (void)
{
int x, y;
# if PIG_LATIN
printf("Easeplay enternay ethay aluevay orfay xnay:") ;
scanf("%d", &x) ;
printf("Easeplay enternay ethay aluevay orfay ynay:");
scanf("%d", &y);
#else
printf(" Please enter the value for x: ");
scanf("%d", &x);
printf("Please enter the value for y: ");
scanf("%d", &y);
# endif
if (GREATER(x, y) = = TRUE)
{
# if PIG- LATIN
printf("xnay islay eatergray anthay ynay!\n");
#else
printf {" x is greater than y! \n" ) ;
# endif
}
else
{
# if PIG_LATIN
printf ("xnay islay otnay eatergray anthay ynay!\n");
#else
printf ("x is not greater than y!\n");
# endif
}
}
上例通过预处理指令定义了3个标识符常量(即TRUE,FALSE和PIG_LATIN)和一个宏(即GREATER(a,b)),并使用了一组条件编译指令。当预处理程序处理上例中的源代码时,它首先读入stdio.h头文件,并解释其中的预处理指令,然后把所有标识符常量和宏用相应的值和代码替换掉,最后判断PIG_LATIN是否为TRUE,并由此决定是使用拉丁文还是
使用英文。
如果PIG_LATIN为FALSE,则上例的预处理版本将如下所示:
/ * Here is where all the include files
would be expanded. * /
void main (void)
{
int x, y;
printf("Please enter the value for X: ");
scanf("%d", &x);
printf("Please enter the value for y: ");
scanf("%d", &y),
if (((x) > (y) ? (1) : (!1)) == 1)
{
printf("x is greater than y!\n");
}
else
{
printf{"x is not greater than y!\n");
}
}
多数编译程序都提供了一个命令行选项,或者有一个独立的预处理程序,可以让你只启动预处理程序并将源代码的预处理版本保存到一个文件中。你可以用这种方法查看源代码的预处理版本,这对调试与宏或其它预处理指令有关的错误是比较有用的。
请参见:
5.3 怎样避免多次包含同一个头文件?
5.4 可以用#include指令包含类型名不是".h"的文件吗?
5.12 #include <file>和#include"file"有什么不同?
5.22 预处理指令#pragma有什么作用?
5.23 #line有什么作用?
5.3 怎样避免多次包含同一个头文件?
通过#ifndef和#define指令,你可以避免多次包含同一个头文件。在创建一个头文件时,你可以用#define指令为它定义一个唯一的标识符名称。你可以通过#ifndef指令检查这个标识符名称是否已被定义,如果已被定义,则说明该头文件已经被包含了,就不要再次包含该头文件;反之,则定义这个标识符名称,以避免以后再次包含该头文件。下述头文件就使用了这种技术:
# ifndef _FILENAME_H
#define _FILENAME_H
#define VER_NUM " 1. 00. 00"
#define REL_DATE "08/01/94"
#if _WINDOWS_
# define OS_VER "WINDOWS"
#else
#define OS_VER "DOS"
# endif
# endif
当预处理程序处理上述头文件时,它首先检查标识符名称_FILENAME_H是否已被定义——如果没有被定义,预处理程序就对此后的语句进行预处理,直到最后一个#endif语句;反之,预处理程序就不再对此后的语句进行预处理。
请参见:
5.4 可以用#include指令包含类型名不是".h"的文件吗?
5,12 #include <file>和#include“file”有什么不同?
5.14 包含文件可以嵌套吗?
5.15 包含文件最多可以嵌套几层?
5. 4 可以用#include指令包含类型名不是".h"的文件吗?
预处理程序将包含用#include指令指定的任意一个文件。例如,如果程序中有下面这样一条语句,那么预处理程序就会包含macros.inc文件。
#include <macros.inc>
不过,最好不要用#include指令包含类型名不是".h"的文件,因为这样不容易区分哪些文件是用于编译预处理的。例如,修改或调试你的程序的人可能不知道查看macros.inc文件中的宏定义,而在类型名为".h"的文件中,他却找不到在macros.inc文件中定义的宏。如果将macros.inc文件改名为macros.h,就可以避免发生这种问题。
请参见:
5.3怎样避免多次包含同一个头文件?
5.12#include<file>和#include“file”有什么不同?
5,14包含文件可以嵌套吗?
5.15包含文件最多可以嵌套几层?
5.5 用#define指令说明常量有什么好处?
如果用#define指令说明常量,常量只需说明一次,就可多次在程序中使用,而且维护程序时只需修改#define语句,不必一一修改常量的所有实例。例如,如果在程序中要多次使用PI(约3.14159),就可以象下面这样说明一个常量:
#define PI 3.14159
如果想提高PI的精度,只需修改在#define语句中定义的PI值,就不必在程序中到处修改了。通常,最好将#define语句放在一个头文件中,这样多个模块就可以使用同一个常量了。
用#define指令说明常量的另一个好处是占用的内存最少,因为以这种方式定义的常量将直接进入源代码,不需要再在内存中分配变量空间。
但是,这种方法也有缺点,即大多数调试程序无法检查用#define说明的常量。
用#define指令说明的常量可以用#under指令取消。这意味着,如果原来定义的标识符(如NULL)不符合你的要求,你可以先取消原来的定义,然后重新按自己的要求定义一个标识符,详见5.31。
请参见:
5.6用enum关键字说明常量有什么好处?
5.7与用#define指令说明常量相比,用enum关键字说明常量有什么好处?
5.31怎样取消一个已定义的宏?
5.6 用enum关键字说明常量有什么好处?
用enum关键字说明常量(即说明枚举常量)有三点好处:
(1)用enum关键字说明的常量由编译程序自动生成,程序员不需要用手工对常量一一赋值。
(2)用enum关键字说明常量使程序更清晰易读,因为在定义enum常量的同时也定义了一个枚举类型标识符。
(3)在调试程序时通常可以检查枚举常量,这一点是非常有用的,尤其在不得不手工检查头文件中的常量值时。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -