2-10 预处理器介绍

当你编译项目时,可能会以为编译器会完全按照你编写的代码来编译每个文件。但实际情况并非如此。

相反,在编译之前,每个代码文件(.cpp)都会经历预处理preprocessing阶段。在此阶段,名为预处理器preprocessor的程序会对代码文件文本进行多项修改。预处理器不会实际修改原始代码文件——所有变更均在内存中临时完成,或通过临时文件实现。

顺带一提……
历史上,预处理器曾是独立于编译器的程序,但在现代编译器中,预处理器可能直接内置于编译器本身。

预处理器所做的大部分工作都相当乏味。例如,它会删除注释,并确保每个代码文件以换行符结尾。然而,预处理器确实承担着一个极其重要的角色:它负责处理#include指令(我们稍后将详细讨论)。

当预处理器完成对代码文件的处理后,其结果被称为翻译单元translation unit。这个翻译单元随后将由编译器进行编译。

相关内容
预处理、编译和链接的整个过程称为翻译translation

若您感兴趣,以下是翻译阶段的列表。截至本文撰写之时,预处理涵盖第1至第4阶段,编译涵盖第5至第7阶段。


预处理器指令

预处理器运行时,会从上至下扫描代码文件,寻找预处理器指令。预处理器指令(通常简称为指令)是以#符号开头、以换行符(而非分号)结尾的指令。这些指令指示预处理器执行特定的文本处理任务。需注意:预处理器不理解C++语法——指令拥有独立的语法体系(部分语法与C++相似,部分则差异显著)。

核心要点
预处理器的最终输出不含指令本身——仅处理后的指令结果会被传递给编译器。

补充说明…
使用指令(详见第2.9课——命名冲突与命名空间介绍)并非预处理指令(因此不会被预处理器处理)。因此虽然“指令”一词通常指预处理指令,但并非总是如此。


#include

你已经见过#include指令的实际应用(通常用于#include )。当你包含一个文件时,预处理器会将#include指令替换为被包含文件的内容。被包含的内容随后会被预处理(这可能导致额外的#include指令被递归预处理),然后文件的其余部分才会进行预处理。

请看以下程序:

#include <iostream>

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

当预处理器运行此程序时,它会将 #include 替换为名为“iostream”的文件内容,随后对包含的内容及文件其余部分进行预处理。

由于 #include 几乎专用于包含头文件,我们将在下一课(讨论头文件时)更详细地探讨 #include。

关键要点
每个翻译单元通常由单个代码文件(.cpp)及其包含的所有头文件构成(因头文件可递归包含其他头文件,故采用递归方式处理)。


宏定义

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

宏主要分为两类:对象型宏object-like macros函数型宏function-like macros

函数型宏的行为类似函数,用途也相似。其使用通常被视为不安全,且几乎所有能通过函数实现的功能都能由普通函数完成。

对象型宏可通过以下两种方式定义:

#define IDENTIFIER
#define IDENTIFIER substitution_text

顶部的定义没有替换文本,而底部的定义有。由于这些是预处理器指令(而非语句),请注意两种形式均不以分号结尾。

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

最佳实践
宏名称应全部使用大写字母书写,单词之间用下划线underscores分隔。


带替换文本的对象式宏

当预处理器遇到此指令时,会在宏标识符与替换文本之间建立关联。此后所有宏标识符的出现位置(在其他预处理命令中使用的情况除外)都将被替换为替换文本。

请看以下程序:

#include <iostream>

#define MY_NAME "Alex"

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

    return 0;
}

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

// The contents of iostream are inserted here

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

    return 0;
}

运行时,会输出以下内容:

image

在C语言中,曾使用带替换文本的对象类宏作为为字面量命名的方法。如今这种做法已不再必要,因为C++提供了更优方案(参见7.10节——跨多个文件共享全局常量(使用内联变量))。带替换文本的对象类宏现多见于遗留代码中,我们建议尽可能避免使用它们。

最佳实践
除非没有可行的替代方案,否则应避免使用包含替换文本的宏。


不带替换文本的对象样式宏

对象样式宏也可以不带替换文本进行定义。

例如:

#define USE_YEN

此类宏的行为符合预期:后续出现的同名标识符大多会被移除并替换为空!

这看似毫无用处,确实无法用于文本替换。但此指令形式通常并非为此目的而设计,我们稍后将探讨其具体用途。

与带替换文本的对象类宏不同,此类宏通常被视为可接受的使用方式。


条件编译

条件编译conditional compilation预处理指令允许您指定在何种条件下某段代码将被编译或不予编译。虽然存在多种不同的条件编译指令,但本文仅重点介绍最常用的几种:#ifdef、#ifndef 和 #endif。

#ifdef 预处理指令使预处理器能够检查某个标识符是否已被 #define 预先定义。若已定义,则 #ifdef 与匹配的 #endif 之间的代码将被编译;若未定义,则该代码段将被忽略。

请看以下程序示例:

#include <iostream>

#define PRINT_JOE

int main()
{
#ifdef PRINT_JOE
    std::cout << "Joe\n"; // will be compiled since PRINT_JOE is defined
#endif

#ifdef PRINT_BOB
    std::cout << "Bob\n"; // will be excluded since PRINT_BOB is not defined
#endif

    return 0;
}

image

由于PRINT_JOE已被#define定义,因此编译器会处理std::cout << “Joe\n”这一行代码。由于PRINT_BOB尚未被#define定义,编译器将忽略std::cout << “Bob\n”这一行代码。

ifndef与#ifdef相反,它允许检查某个标识符是否尚未被#define定义。

#include <iostream>

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

    return 0;
}

该程序会输出“Bob”,因为PRINT_BOB从未被#define定义。

除了#ifdef PRINT_BOB和#ifndef PRINT_BOB,你还会看到#if defined(PRINT_BOB)和#if !defined(PRINT_BOB)。它们实现相同功能,但采用更接近C++风格的语法。

本功能的实际应用可参见第0.13课——我的编译器采用何种语言标准?。


#if 0

条件编译的另一种常见用法是使用 #if 0 排除代码块的编译(如同将其置于注释块内):

#include <iostream>

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

#if 0 // Don't compile anything starting here
    std::cout << "Bob\n";
    std::cout << "Steve\n";
#endif // until this point

    return 0;
}

image

上述代码仅输出“Joe”,因为“Bob”和“Steve”被#if 0预处理指令排除在编译之外。

这为包含多行注释的代码提供了便捷的“注释”方式(由于多行注释不可嵌套,无法使用另一段多行注释将其注释掉):

#include <iostream>

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

#if 0 // Don't compile anything starting here
    std::cout << "Bob\n";
    /* Some
     * multi-line
     * comment here
     */
    std::cout << "Steve\n";
#endif // until this point

    return 0;
}

要暂时重新启用被包裹在 #if 0 中的代码,可以将 #if 0 改为 #if 1:

#include <iostream>

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

#if 1 // always true, so the following code will be compiled
    std::cout << "Bob\n";
    /* Some
     * multi-line
     * comment here
     */
    std::cout << "Steve\n";
#endif

    return 0;
}

image


在其他预处理器命令中的宏替换

现在你可能会疑惑,给定以下代码:

#define PRINT_JOE

int main()
{
#ifdef PRINT_JOE
    std::cout << "Joe\n"; // will be compiled since PRINT_JOE is defined
#endif

    return 0;
}

既然我们已将PRINT_JOE定义为空值,为何预处理器未将#ifdef PRINT_JOE中的PRINT_JOE替换为空值,从而排除该输出语句的编译?

通常情况下,当宏标识符出现在其他预处理命令内部时,不会发生宏替换。

顺带一提……
这条规则至少有一个例外:大多数形式的#if和#elif会在预处理命令内部进行宏替换。

再举一个例子:

#define FOO 9 // Here's a macro substitution

#ifdef FOO // This FOO does not get replaced with 9 because it’s part of another preprocessor directive
    std::cout << FOO << '\n'; // This FOO gets replaced with 9 because it's part of the normal code
#endif

#define 宏的作用域

指令在编译前按文件顺序自上而下解析。

考虑以下程序:

#include <iostream>

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

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

	return 0;
}

尽管看起来 #define MY_NAME “Alex” 是定义在函数 foo 内部,但预处理器并不理解函数这类 C++ 概念。因此,该程序的行为与在函数 foo 之前或之后立即定义 #define MY_NAME “Alex” 的程序完全相同。为避免混淆,通常应在函数外部定义标识符。

由于 #include 指令会用包含文件的内容替换原指令,因此包含操作可将包含文件中的指令复制到当前文件中。这些指令随后将按顺序处理。

例如,以下代码的行为也与前例完全一致:

Alex.h:

#define MY_NAME "Alex"

main.cpp:

#include "Alex.h" // copies #define MY_NAME from Alex.h here
#include <iostream>

int main()
{
	std::cout << "My name is: " << MY_NAME << '\n'; // preprocessor replaces MY_NAME with "Alex"

	return 0;
}

预处理器完成后,该文件中定义的所有标识符都会被丢弃。这意味着指令仅在其定义点到定义文件末尾之间有效。在一个文件中定义的指令不会影响其他文件(除非它们被#include到另一个文件中)。例如:

function.cpp:

#include <iostream>

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

main.cpp:

void doSomething(); // forward declaration for function doSomething()

#define PRINT

int main()
{
    doSomething();

    return 0;
}

上述程序将输出:

image

尽管PRINT在main.cpp中被定义,但这不会影响function.cpp中的任何代码(PRINT仅在定义点到main.cpp结尾之间被#define)。这将在我们后续课程讨论头文件保护机制时产生影响。

posted @ 2026-02-10 02:56  游翔  阅读(0)  评论(0)    收藏  举报