from http://www.learncpp.com/cpp-tutorial/110-a-first-look-at-the-preprocessor/
预处理器是一个独立的程序,在编译器编译你的程序之前运行。这样做的目的是处理指令。这些指令是以#符号开始的,以新行结束而不是以分号结束的特殊的指令。存在几种不同的指令,我们在下面将会谈到。预处理器并不聪明,它不能理解C++语法;但是它在编译器读取之前巧妙的处理了文本。
包含
你已经看到了#include指令的使用。#include 告诉预处理器把包含的文件中的内容插入到当前指令所在的位置。当你有内容需要在很多不同的地方被调用的时候是很有用的(正如前置声明)。
#include 命令有两种方式:
#include <filename> 告诉编译器去操作系统定义的特殊的地方去寻找头文件,这些头文件主要是为了运行库的。
#include "filename" 告诉编译器去包含源代码的文件夹中去寻找相应的头文件。如果没有找到,它会自动转换成以上面尖括号的方式来处理。
宏定义
宏定义具有这样的形式:
#define identifier replacement
当预处理器无论在什么时候遇到了这样的指令,任何出现'identifier'的地方都将被替换成'replacement'。标识符通常为大写字母,使用下划线代替空格。
举个例子:
1: #define MY_NAME "Alex"
2:
3: cout << "Hello, " << MY_NAME << endl;
预处理器将会把上面的内容转换成下面的内容:
1: #define MY_NAME "Alex"
2:
3: cout << "Hello, " << "Alex" << endl;
当运行的时候会输出 Hello, Alex。
#define 以这种方式使用,有两个重要的特征。首先,允许你给一些东西命名为描述性的名字,如数字。
举个例子:
1: int nYen = nDollars * 122;
像122这样的数字在程序中被称为魔法数字。一个魔法数字是hard-coded数字,它在代码中没有任何意义——122表示什么呢?是转换率还是其他什么呢?它是不明确的。在一些复杂的程序里,通常很难判断一个hard-coded数字具体代表什么。
下面小段代码是清晰的:
1: #define YEN_PER_DOLLAR 122
2: int nYen = nDollars * YEN_PER_DOLLAR;
其次,#defined 数字可以使得程序更加容易被修改。假设将转换率从122变成123,我们的程序需要进行相应的调整。考虑下面的代码:
1: int nYen1 = nDollars1 * 122;
2: int nYen2 = nDollars2 * 122;
3: int nYen3 = nDollars3 * 122;
4: int nYen4 = nDollars4 * 122;
5: SetWidthTo(122);
为了改变成新的转换率,我们必须将前面四个语句中的数字改变。但是第五个语句呢?这里的122是不是和其他的122具有相同意义呢?如果是,它应该被改变。如果不是,则不需改变,或者我们也许在其他地方中断。
现在考虑使用了#defined的代码,如下:
1: #define YEN_PER_DOLLAR 122
2: #define COLUMNS_PER_PAGE 122
3:
4: int nYen1 = nDollars1 * YEN_PER_DOLLAR;
5: int nYen2 = nDollars2 * YEN_PER_DOLLAR;
6: int nYen3 = nDollars3 * YEN_PER_DOLLAR;
7: int nYen4 = nDollars4 * YEN_PER_DOLLAR;
8: SetWidthTo(COLUMNS_PER_PAGE);
此时改变转换率只要改变一个数字,如下:
1: #define YEN_PER_DOLLAR 123
2: #define COLUMNS_PER_PAGE 122
3:
4: int nYen1 = nDollars1 * YEN_PER_DOLLAR;
5: int nYen2 = nDollars2 * YEN_PER_DOLLAR;
6: int nYen3 = nDollars3 * YEN_PER_DOLLAR;
7: int nYen4 = nDollars4 * YEN_PER_DOLLAR;
8: SetWidthTo(COLUMNS_PER_PAGE);
现在我们正确改变了转换率,并且不用担心一不小心将每页的行数改变。
虽然#define的值对于魔法数字有很好的处理,但是它们本身具有一些问题,我们在随后的章节中遇到变量和范围界定问题时将会详细的讨论。
你可以undefine先前定义的变量使用#undef预处理指令。
看一下下面的小代码:
1: #define MY_NAME "Alex"
2: cout << "My name is " << MY_NAME << endl;
3: #undef MY_NAME
4: cout << "My name is " << MY_NAME << endl;
程序的最后一行产生了编译错误,因为MY_NAME已经被undefined了。
条件编译
条件编译预处理指令允许你提前特别处理在哪些情况下会进行编译或是不会进行编译。在这节中我们讨论到的条件编译指令有#ifdef,#ifndef,和#endif。
#ifdef预处理指令允许预处理器确认一个值是不是在前面被定义过,如果被定义过,#ifdef和#endif之间的代码被编译,如果没有被定义过,两者之间的代码被忽略。
考虑一下下面代码:
1: #define PRINT_JOE
2:
3: #ifdef PRINT_JOE
4: cout << "Joe" << endl;
5: #endif
6:
7: #ifdef PRINT_BOB
8: cout << "Bob" << endl;
9: #endif
因为PRINT_JOE已经被定义,cout << "Joe" << endl; 将被编译。因为BOB没有被定义,因此 cout << “Bob” << endl; 不会被编译。
#ifndef与#ifdef相反,通过它你能够检测哪些名字没有被定义过。
1: #ifndef PRINT_BOB
2: cout << "Bob" << endl;
3: #endif
程序输出 Bob ,因为PRINT_BOB没有被定义过。
头文件guards
因为头文件可以被其他头文件包含,因此很容易产生多次包含的情况。如下:
add.h
1: #include "mymath.h"
2: int add(int x, int y);
subtract.h
1: #include "mymath.h"
2: int subtract(int x, int y);
main.cpp
1: #include "add.h"
2: #include "subtract.h"
我们包含了add.h,它带来了mymath.h和函数add的原型。当我们包含subtract.h时,它再次将mymath.h包含了进来,另外还有subtract函数的原型。因此,mymath.h中的所有内容都被包含了两次,这会产生编译器的抱怨。
为了组织这类事情的发生,我们使用头文件guards,它是一些编译指令的集合,如下:
1: #ifndef SOME_UNIQUE_NAME_HERE
2: #define SOME_UNIQUE_NAME_HERE
3:
4: // your declarations here
5:
6: #endif
当头文件被包含的时候,首先做的事情是检查SOME_UNIQUE_NAME_HERE在之前是不是被定义了。如果这是第一次我们包含这个头文件,SOME_UNIQUE_NAME_HERE还没有被定义。因此它定义了SOME_UNIQUE_NAME_HERE包含了文件的所有内容。如果不是第一次包含,SOME_UNIQUE_NAME_HERE已经被定义了,因此整个头文件会被忽略。
你的所有的头文件都应该含有头文件guards。SOME_UNIQUE_NAME_HERE可以使任何你想要的名字,但是一般情况下头文件的名字加上_H的扩展。举个例子,add.h的头文件guard:
1: #ifndef ADD_H
2: #define ADD_H
3:
4: // your declarations here
5:
6: #endif
即使是标准库也使用头文件guards。如果看看Visual Studio 2005 Express的iostream头文件中,你会看到:
1: #ifndef _IOSTREAM_
2: #define _IOSTREAM_
3:
4: // content here
5:
6: #endif