C++的预处理
在编译一个程序源文件时,C++编译器首先进行预处理。预处理的作用是通过执行源文件中包含的预处理指令将源文件转换为一个等价文件,常见的预处理指令有:文件包含,条件编译和宏替换。
图1示例了对一个简单源文件进行预处理的过程,包括:
★去掉注释,用一个空白字符代替。
★执行文件包含(#include)和条件编译指令(#ifdef等)。
★对源文件进行宏替换。
因为预处理只是对文本内容进行简单操作,它不可能针对C++语言的语法进行错误检查,这也导致它只能进行极少的错误检查。 图1 编译预处理的过程
预处理指令
声明预处理指令的一般形式为:
# directive tokens
符号“#”必须是该行第一个非空白字符,但前面有空白符或退格符都可以,#与directive之间也可以有多个空白符,如下代码具有完全相同的效果:
#define size 100
#define size 100
# define size 100
一般一行声明一条预处理指令,但不排除用多行声明一条指令,字符“\”用在一行的末尾表示下行仍然接着该行,比如下面两个声明是完全对等的:
#define CheckError \
if (error) \
exit(1)
#define CheckError if (error) exit(1)
预处理指令声明中出现的注释以及一行单独一个#符号的情况在预编译处理过程中都会被忽略掉。
表1是所有预处理指令和意义
指令 意义
#define 定义宏
#undef 取消定义宏
#include 包含文件
#ifdef 其后的宏已定义时激活条件编译块
#ifndef 其后的宏未定义时激活条件编译块
#endif 中止条件编译块
#if 其后表达式非零时激活条件编译块
#else 对应#ifdef, #ifndef, 或 #if 指令
#elif #else 和 #if的结合
#line 改变当前行号或者文件名
#error 输出一条错误信息
#pragma 为编译程序提供非常规的控制流信息
宏定义
#define指令定义宏,宏定义可分为两类:简单宏定义,带参数宏定义。
简单宏定义有如下一般形式:
#define 名字 替换文本
它指示预处理器将源文件中所有出现名字记号的地方都替换为替换文本,替换文本可以是任何字符序列,甚至可以为空(此时相当于删除掉文件中所有对应的名字)。
简单宏定义常用于定义常量符号,如:
#define size 512
#define word long
#define bytes sizeof(word)
因为宏定义对预编译指令行也有效,所以一个前面已经被定义的宏能被后来的宏嵌套定义(如上面的bytes定义用到了word)。对于下面这句代码:
word n = size * bytes;
它的宏扩展就是:
long n = 512 * sizeof(long);
使用简单宏定义定义常量符号起源于C语言,但在C++中,定义常量可以用const关键字,并且还附加类型检查的功能,因此C++中已经尽量避免使用宏定义来定义常量了。
带参数宏定义的一般形式为:
#define 名字(参数) 替换文本
其中参数是一个或多个用逗号分割的标识符;在“名字”和“(”之间不允许有空格,否则整个宏定义将退化为一个置换文本为“(参数) 替换文本”的简单宏定义。下例表示定义一个求两数中较大者的带参数宏Max。
#define Max(x,y) ((x) > (y) ? (x) : (y))
带参数宏的调用有点类似于函数调用,实参数目必须匹配形参。首先,宏的替换文本部分置换掉调用的代码,接着,替换文本部分的形参又被置换为相应的实参,这个过程叫做宏扩展。见下例:
n = Max (n - 2, k +6);
的宏扩展为:
n = (n - 2) > (k + 6) ? (n - 2) : (k + 6);
注意,宏扩展时有可能发生不预期的运算符优先级的变化,这时如果定义宏时将替换文本里出现的每个形参都用括号括起来就不会出现问题(如上述宏MAX所示)。
仔细考察带参数宏与函数调用的异同可以发现,由于宏工作在文本一层,相同功能的宏和函数调用产生的语义有时是不完全相同的,比如:
Max(++i, j)
扩展为
((++i) > (j) ? (++i) : (j))
可见i最后自增了两次,但相同功能的函数能够保证只自增一次。
带参数宏定义在C++中的使用同样也在减少,因为:1,C++的内联函数提供了和带参数宏同样高的代码执行效率,同时没有后者那样的语义歧义;2,C++模板提供了和带参数宏同样高的灵活性,还能够执行语法分析和类型检查。
最后讨论一点内容是宏能够被重定义,在重定义前,必须使用#undef指令取消原来的宏定义,#undef如果取消的是一个原本不存在的宏定义则视为无效。如:
#undef size
#define size 128
#undef Max
引用操作符和拼接操作符
预处理提供了两个特殊操作符操作宏内的参数。引用操作符(#)是一元的,后跟一个形参作为运算对象,它的作用是将该运算对象替换为带引号的字符串。
如有一个调试打印宏检查指针是否为空,为空时输出警告信息:
#define CheckPtr(ptr) \
if ((ptr) == 0) cout << #ptr << " is zero!\n"
此时#操作符将表达式中的变量ptr当成字符串输出为警告信息的一部分。因此,如下的调用:
CheckPtr(tree->left);
扩展为:
if ((tree->left) == 0) cout << "tree->left" << " is zero!\n";
注意:如果按照下面这样定义宏
#define CheckPtr(ptr) \
if ((ptr) == 0) cout << "ptr is zero!\n"
是不会得到期望结果的,因为宏不能在字符串内部进行置换。
拼接操作符(##)是二元的,被用来连接宏中两个实际参数,比如,如下宏定义
#define internal(var) internal##var
如果执行
long internal(str);
则被扩展为:
long internalstr;
在一般编程时很少用到拼接操作符,但在编写编译器程序或源代码生成器时特别有用,因为它能轻易的构造出一组标识符。
----------------------------------------------
版权所有:朱科 欢迎光临我的网站:www.goodsoft.cn,各位转贴别删,劳动成果啊
----------------------------------------------
文件包含
#include指令实现将一个文件包含在另外一个中,如:
#include "constants.h"
被包含的文件一般要求和源文件在同一个目录下,否则必须指定一个完整或相对的路径。如:
#include "../file.h" // 从父目录中包含文件 (UNIX)
#include "/usr/local/file.h" // 完整路径 (UNIX)
#include "..\file.h" // 从父目录中包含文件(DOS)
#include "\usr\local\file.h" // 完整路径(DOS)
#include <iostream.h>
编译预处理中遇到<>符号包含的文件时,处理程序将搜索一至多个系统中的特定目录,如UNIX系统中的/usr/include/cpp目录。用户也可通过执行编译命令或改变系统环境变量来自定义这些特定目录。
文件包含可以嵌套,如,文件f包含了文件g,文件g又包含了文件h,则文件f也包含了文件h。
编译预处理并没有限定包含的文件类型,可以是.h,.cpp或.cc文件,但习惯上只包含头文件.h。
重复包含头文件也许会造成编译错误,视情况而定。如,如果头文件中只有一些宏定义和声明,则重复包含没有问题。但如果它包含了一些变量定义,则编译器视重复包含为错误。
条件编译
条件编译控制某段代码是否能够被编译,它常用于为了适应不同的软硬件环境而进行的代码裁剪。表2列出了所有条件编译指令的一般形式。 表2 条件编译指令的一般形式
形式 解释
#ifdef 名字
代码段
#endif 如果名字被定义,代码段参与编译,否则不参与
#ifndef名字
代码段
#endif 如果名字未被定义,代码段参与编译,否则不参与
#if 表达式
代码段
#endif 如果表达式非0,代码段参与编译,否则不参与
#ifdef名字
代码段1
#else
代码段2
#endif 如果名字被定义,代码段1参与编译,代码段2不参与;否则相反。
类似,#else也可与#if配合使用
#if 表达式1
代码段1
#elif 表达式2
代码段2
#else
代码段3
#endif 如果表达式1非0,代码段1参与编译,否则,如果表达式2非0,
代码段2参与编译,再否则,代码段3参与编译
下面是两个例子
// 测试版和最终发行版本使用不同的代码:
#ifdef BETA
DisplayBetaDialog();
#else
CheckRegistration();
#endif
// 确保Unit有至少4个字节宽:
#if sizeof(int) >= 4
typedef int Unit;
#elif sizeof(long) >= 4
typedef long Unit;
#else
typedef char Unit[4];
#endif
#if指令的一个用处是临时忽略部分代码,当程序员在测试某段可疑代码时常常用到这招,当然你也可以用/*…*/将代码注释掉,但因为注释是不可以嵌套的,如果代码中已有/*…*/注释这种方法就不奏效了。
下面这段代码表示永久删除中间的代码段:
#if 0
...要删除的代码段
#endif
另外预处理提供了一个defined运算符与#if和#elif配合,如:
#if defined BETA
等价于:
#ifdef BETA
它的妙处是可以写出组合逻辑表达式(#ifdef就不能),如:
#if defined ALPHA || defined BETA
条件编译的另一个用处是可以防止重复包含头文件。如,有一个file.h的头文件,为了避免重复包含,可以在将该文件的内容包含在下列形式的条件语句中:
#ifndef _file_h_
#define _file_h_
这里是头文件内容
#endif
当编译预处理首次包含头文件file.h时,将定义名字_file_h_,其后准备再次包含file.h时,会发现该名字已经定义,这样就直接跳转到#endif处,就不会包含该头文件了。
其他预编译指令
还有三种不常用的指令。#line指令可改变当前行号和文件名,一般形式为:
#line 行号 文件名
其中 “文件名”可选,举例如下:
#line 20 "file.h"
上句告诉编译器当前行号为20,文件名为file.h,直至遇到下一个#line指令。它在编写C++编译器程序的中有些用处,因为编译器对C++源码编译过程中会产生一些中间文件,通过这条指令,可以保证文件名是固定的,不会被这些中间文件代替,有利于进行分析。
#error用于在预处理时输出错误信息,一般形式为:
#error 错误信息
当预处理时遇到这个指令,它将输出错误信息并中止编译,因此最好保证确实无法进一步编译时才书写这个指令代码,如:
#ifndef UNIX
#error This software requires the UNIX OS.
#endif
#pragma指令执行一些非标准的预编译命令,不同厂商的编译器有不同的解释执行方法,如对于SUN C++编译器有:
// name和val必须在8个字节的倍数上对齐:
#pragma align 8 (name, val)
char name[9];
double val;
// 程序启动即运行MyFunction:
#pragma init (MyFunction)
预定义标识符
预处理器提供了一些非常有用的预定义标识符,表3列出了标准预定义标识符。 表3 标准预定义标识符
标识符 表示
_FILE_ 正在编译的文件的文件名
_LINE_ 正在编译的文件的当前行
_DATE_ 字符串表示的当前日期(如“25 Dec 2000”)
_TIME_ 字符串表示的当前时间(如“12:30:55”)
预定义标识符在程序中可以像普通常量一样使用。如:
#define Assert(p) \
if (!(p)) cout << __FILE__ << ": assertion on line " \
<< __LINE__ << " failed.\n"
定义了一个测试用的宏(断言)。假设有如下调用:
Assert(ptr != 0);
出现在prog.cpp的第50行,当条件成立(ptr==0),输出信息为:
prog.cpp: assertion on line 50 failed.
完成。hoho 例外引用一篇写得简约明了的文章: #include文件的一个不利之处在于一个头文件可能会被多次包含,为了说明这种错误,考虑下面的代码:
#include "x.h"
#include "x.h"
显然,这里文件x.h被包含了两次,没有人会故意编写这样的代码。但是下面的代码:
#include "a.h"
#include "b.h"
看上去没什么问题。如果a.h和b.h都包含了一个头文件x.h。那么x.h在此也同样被包含了两次,只不过它的形式不是那么明显而已。
多重包含在绝大多数情况下出现在大型程序中,它往往需要使用很多头文件,因此要发现重复包含并不容易。要解决这个问题,我们可以使用条件编译。如果所有的头文件都像下面这样编写:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H
...
#endif
那么多重包含的危险就被消除了。当头文件第一次被包含时,它被正常处理,符号_HEADERNAME_H被定义为1。如果头文件被再次包含,通过条件编译,它的内容被忽略。符号_HEADERNAME_H按照被包含头文件的文件名进行取名,以避免由于其他头文件使用相同的符号而引起的冲突。
但是,你必须记住预处理器仍将整个头文件读入,即使这个头文件所有内容将被忽略。由于这种处理将托慢编译速度,所以如果可能,应该避免出现多重包含。