章节目录

预处理器简介

本节阅读量:

编译项目时,你可能以为编译器会按照编写的代码文件原样编译每个文件。但事实并非如此。

相反,在编译之前,每个代码(.cpp)文件都要经过预处理阶段。在这个阶段,预处理器会对代码文件进行一些更改。预处理器不会修改原始代码文件——所做的更改都是临时地发生在内存中或使用临时文件。

预处理器所做的大多数工作都很琐碎。例如,去除注释,确保每个代码文件以换行结束。然而,预处理器确实有一个非常重要的作用:它会处理#include指令。

预处理器完成对代码文件的处理后,处理结果称为翻译单元。翻译单元是编译器随后编译的基本单元。


预处理指令

预处理器运行时,会扫描代码文件(从上到下),查找预处理指令。预处理指令是以#符号开头、以换行符(不是分号)结尾的指令。这些指令告诉预处理器执行某些文本操作任务。注意,预处理器不理解C++语法——它有自己的语法(在某些情况下类似于C++语法,在其他情况下则差异较大)。

本课中,将学习一些常见的预处理指令。


include

我们已经了解了#include指令的作用(一个常见的例子是#include <iostream>)。当#include一个文件时,预处理器将#include指令替换为所包含文件的内容。然后对包含的内容进行预处理(这可能导致递归地预处理其他#include文件),接着处理文件的其余部分。

如下程序:

1
2
3
4
5
6
7
#include <iostream>

int main()
{
    std::cout << "Hello, world!\n";
    return 0;
}

预处理器运行此程序时,预处理器将用名为“iostream”的文件内容替换#include,然后处理引入的内容和文件其余部分。

预处理器对代码文件和所有#include内容的处理结果称为翻译单元。翻译单元是编译器实际编译的单元。

由于#include专用于处理头文件,下一课(讨论头文件时)中会更详细地讨论#include。


宏定义

#define指令用于创建宏。C++中,宏是一条规则,定义如何将输入文本转换为替换输出文本。

宏有两种基本类型:类对象宏和类函数宏。

类函数宏的行为类似于函数,并有类似的用途。使用它们通常是不安全的,它能做的事情都可以用普通函数完成。

类对象宏可以用两种方法定义:

1
2
#define 标识符
#define 标识符 替换文本

第一个定义没有替换文本,第二个有。这些是预处理指令(不是语句),请注意,两种形式都没有以分号结尾。

宏的标识符与普通标识符使用相同的命名规则:可以使用字母、数字和下划线,不能以数字开头,也不能以下划线开头。按照惯例,宏名称通常全部使用大写字母,用下划线分隔。


具有替换文本的类对象宏

当预处理器遇到该指令时,后续出现的每个该标识符都将被替换为substitution_text。标识符按惯例使用全大写字母,用下划线表示空格。

考虑以下程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>

#define MY_NAME "Fly"

int main()
{
    std::cout << "My name is: " << MY_NAME << '\n';

    return 0;
}

预处理器将上述转换为以下内容:

1
2
3
4
5
6
7
8
// iostream 中的内容将会被替换到这里

int main()
{
    std::cout << "My name is: " << "Fly" << '\n';

    return 0;
}

运行时打印输出。“My name is: Fly”。

具有替换文本的类对象宏在C中用于为文本分配名称。但在C++中提供了更好的替代方案。具有替换文本的类对象宏现在只能在旧代码中看到。

建议避免使用这种类型的宏,因为有更好的实现方式。后续章节会讨论——常量变量和符号常量。


无替换文本的类对象宏

类对象宏也可以在没有替换文本的情况下定义。

例如:

1
#define USE_YEN

这种形式的宏的工作方式可能与你想的一样:标识符的任何后续出现都将被删除,且不替换为任何内容!

这可能看起来没什么用,对于文本替换来说确实如此。但这并不是此指令的主要使用场景。

与具有替换文本的类对象宏不同,这种形式的宏通常被认可使用。


条件编译

条件编译预处理器指令允许指定在某些条件下编译或不编译某些代码。有许多条件编译指令,这里只介绍目前最常用的三个:#ifdef、#ifndef和#endif。

#ifdef预处理器指令允许预处理器检查某个标识符是否已被#define定义过。如果是,则编译#ifdef和匹配的#endif之间的代码。如果不是,则忽略这些代码。

考虑以下程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>

#define PRINT_JOE

int main()
{
#ifdef PRINT_JOE
    std::cout << "Joe\n"; // PRINT_JOE被定义,这一行会被编译
#endif

#ifdef PRINT_BOB
    std::cout << "Bob\n"; // PRINT_BOB未被定义,这一行不会被编译
#endif

    return 0;
}

由于PRINT_JOE已定义,因此将编译行std::cout«“JOE\n”。由于未定义PRINT_BOB,将忽略行std::cout « “BOB\n”。

#ifndef与ifdef相反,允许检查标识符是否未定义。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>

int main()
{
#ifndef PRINT_BOB
    std::cout << "Bob\n";
#endif

    return 0;
}

该程序打印“Bob”,因为PRINT_BOB未定义。

除了#ifdef PRINT_BOB 和#ifndef PRINT_BOB,你还会看到#if defined(PRINT_BOB)和#if !defined(PRINT_BOB)这两种写法。它们的作用相同,只是使用了更具C++风格的语法。


#if 0

条件编译的另一个常见用法是使用#if 0来排除不需要编译的代码块(效果和使用注释块一样):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>

int main()
{
    std::cout << "Joe\n";

#if 0 // 从这里开始的不编译
    std::cout << "Bob\n";
    std::cout << "Steve\n";
#endif // 到这里结束

    return 0;
}

上面的代码只打印“Joe”,因为#if 0预处理指令将“Bob”和“Steve”排除在编译之外。

这提供了一种方便的方法来”注释掉”包含多行注释的代码(由于多行注释不可嵌套,无法使用另一个多行注释来注释掉已有的多行注释):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>

int main()
{
    std::cout << "Joe\n";

#if 0 // 从这里开始的不编译
    std::cout << "Bob\n";
    /* 一些
     * 多行
     * 注释
     */
    std::cout << "Steve\n";
#endif // 到这里结束

    return 0;
}

要临时重新启用被#if 0包裹的代码,可以将#if 0改成#if 1。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>

int main()
{
    std::cout << "Joe\n";

#if 1 // 永远为true, 所以下面的代码会被编译
    std::cout << "Bob\n";
    /* 一些
     * 多行
     * 注释
     */
    std::cout << "Steve\n";
#endif

    return 0;
}

类对象宏不影响其他预处理器指令

现在您可能会想:

1
2
3
4
#define PRINT_JOE

#ifdef PRINT_JOE
// ...

既然将PRINT_JOE定义为空,为什么预处理器没有用空值来替换#ifdef PRINT_JOE中的PRINT_JOE呢?

宏只会导致普通代码中的文本替换,而会忽略其他预处理器指令。因此,#ifdef PRINT_JOE中的PRINT_JOE不会被替换。

例如:

1
2
3
4
5
#define FOO 9 // 宏定义

#ifdef FOO // 这里的预处理指令不会受影响
    std::cout << FOO << '\n'; // FOO 被替换为 9,因为这里是普通代码 
#endif

预处理器的最终输出不包含任何预处理指令——它们会在编译之前被解析掉,因为编译器不知道如何处理这些指令。


#define的作用范围

#define在编译之前被解析,从文件中从上到下、逐个文件进行处理。

考虑以下程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>

void foo()
{
#define MY_NAME "Fly"
}

int main()
{
	std::cout << "My name is: " << MY_NAME << '\n';

	return 0;
}

尽管#define MY_NAME “Fly"写在函数foo内部,但预处理器不理解C++的概念(如函数)。因此,该程序的行为与#define MY_NAME “Fly"放在函数foo之前或之后是一样的。为了可读性,通常应将#define放在函数外部。

一旦预处理器处理完成,该文件中所有通过#define定义的标识符将被丢弃。这意味着指令仅从定义点到定义它们的文件末尾有效。在一个代码文件中定义的指令不会影响同一项目中的其他代码文件。

考虑以下示例:

function.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>

void doSomething()
{
#ifdef PRINT
    std::cout << "Printing!\n";
#endif
#ifndef PRINT
    std::cout << "Not printing!\n";
#endif
}

main.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void doSomething(); // 前向声明 doSomething()

#define PRINT

int main()
{
    doSomething();

    return 0;
}

上述程序将打印:

1
Not printing!

PRINT是在main.cpp中定义的,对function.cpp的代码没有任何影响(PRINT只是从定义点到main.cpp末尾定义有效)。


2.8 命名冲突和名称空间简介

上一节

2.10 头文件

下一节