预处理器简介
本节阅读量:编译项目时,希望编译器按照编写代码文件编译每个文件。事实并非如此。
相反,在编译之前,每个代码(.cpp)文件要经过预处理阶段。在这个阶段,预处理器对代码文件进行更改。预处理器不会修改原始代码文件,所做的更改都是临时发生在内存中或使用临时文件。
预处理器所做的大多数工作都很乏味。例如,去除注释,将每个代码文件以换行结束。然而,预处理器确实有一个非常重要的作用:它会处理#include指令。
预处理器完成对代码文件的处理,处理结果称为翻译单元。翻译单元是编译器随后编译的基本单元。
旁白
在历史上,预处理器是一个独立于编译器的程序,但在现代编译器中,预处理器可直接构建到编译器本身中。
预处理指令
预处理器运行时,扫描代码文件(从上到下),查找预处理指令。预处理指令是以#符号开头、以换行符(不是分号)结尾的指令。这些指令告诉预处理器执行某些文本操作任务。注意,预处理器不理解C++语法——相反,它有自己的语法(在某些情况下类似于C++语法,在其他情况下则不太类似)。
本课中,将学习一些常见的预处理指令。
作为旁白…
using namespace(在命名冲突和名称空间简介一节)不是预处理指令(不由预处理器处理)。
include
了解了#include指令作用(一个常见的例子是#include <iostream>)。当#include文件时,预处理器将#include指令替换为所包含文件的内容。然后对包含内容进行预处理(可能导致递归地预处理其他的#include文件),然后处理文件的其余部分。
如下程序:
|
|
预处理器运行此程序时,预处理器将用名为“iostream”的文件内容替换#include,然后处理引入的内容和文件其余部分。
预处理器对代码文件和所有#包含内容的处理结果称为翻译单元。翻译单元是编译器编译的单元。
由于#include专用于处理头文件,下一课(讨论头文件时)中会更详细地讨论#include。
关键点
翻译单元既包含来自代码文件的代码,也包含所有#included文件的处理代码。
宏定义
#define指令用于创建宏。C++中,宏是一条规则,定义如何将输入文本转换为替换输出文本。
宏有两种基本类型:类对象宏和类函数宏。
类函数宏的行为类似于函数,并有类似的用途。使用它们通常是不安全的,它所做的事情都可由正常功能完成。
类对象宏可以用两种方法定义:
|
|
第一个定义没有替换文本,第二个有。这些是预处理指令(不是语句),请注意,两种形式都没有以分号结尾。
宏的标识符与普通标识符使用相同的命名规则:可以使用字母、数字和下划线,不能以数字开头,且不以下划线开头。按照惯例,宏名称通常都是大写的,由下划线分隔。
具有替换文本的类对象宏
当预处理器遇到该指令时,后续标识符的每次出现都将替换为substitution_text。标识符传统上是用所有大写字母键入的,使用下划线表示空格。
考虑以下程序:
|
|
预处理器将上述转换为以下内容:
|
|
运行时打印输出。“My name is: Fly”。
具有替换文本的类对象宏(在C中)用作将名称分配给文本。但C++中提供了更好的方法。具有替换文本的类对象宏只能在旧代码中看到。
建议避免这种类型的宏,因为有更好的方法实现。后续章节会讨论——常量变量和符号常量。
无替换文本的类对象宏
类对象宏也可以在没有替换文本的情况下定义。
例如:
|
|
这种形式的宏的工作方式可能与期望一样:标识符任何进一步出现都将被删除,并且不替换任何内容!
这可能看起来无用,并且对于进行文本替换也是无用的。但这并不是此指令的使用场景。
与具有替换文本的类对象宏不同,这种形式的宏通常被认可使用。
条件编译
条件编译预处理器指令允许在指定条件下编译或不编译。有许多条件编译指令,只介绍目前使用最多的三个:#ifdef、#ifndef和#endif。
#ifdef预处理器指令允许预处理器检查以前是否#定义了标识符。如果是,则编译#ifdef和匹配的#endif之间的代码。如果不是,则忽略代码。
考虑以下程序:
|
|
由于PRINT_JOE已定义,因此将编译行std::cout«“JOE\n”。由于未定义PRINT_BOB,将忽略行std::cout « “BOB\n”。
#ifndef与ifdef相反,允许检查标识符是否未定义。
|
|
该程序打印“Bob”,因为PRINT_BOB未定义。
代替#ifdef PRINT_BOB 和#ifndef PRINT_BOB,还将看到#if defined(PRINT_BOB)和#if !defined(PRINT_BOB)。作用相同,只使用了更具C++风格的语法。
#if 0
条件编译另一常见用法是使用#if 0排除不需编译的代码块(和使用注释块一样):
|
|
上面的代码只打印“Joe”,因为#if 0预处理指令将“Bob”和“Steve”排除在编译之外。
这提供了一种方便的方法来“注释掉”包含多行注释的代码(由于多行注释不可嵌套,因此不能使用另一个多行注释来注释):
|
|
要临时重新启用包装在#if 0中的代码,可以将#if 0改成 #if 1。
|
|
类对象宏不影响其他预处理器指令
现在您可能会想:
|
|
既然将PRINT_JOE定义为空,为什么预处理器没有用空来替换#ifdef PRINT_JOE中的PRINT_JOE?
宏仅导致普通代码的文本替换。忽略其他预处理器命令。因此,#ifdef PRINT_JOE中的PRINT_JOE将被保留。
例如:
|
|
预处理器的最终输出不包含预处理指令——会在编译之前解析,因为编译器不知道如何处理。
#define的作用范围
#define 在编译之前解析,从文件中,从上到下,逐个文件进行处理。
考虑以下程序:
|
|
尽管#define MY_NAME “Fly” 是在函数foo中定义的,但预处理器不理解C++概念(如函数)。因此,该程序的行为与#define MY_NAME “Fly” 在函数foo之前或之后定义的程序相同。为了可读性,通常需要在函数外部设置 #define。
一旦预处理器处理完成,该文件中定义的所有#define定义的标识符将被丢弃。意味着指令仅从定义点到定义它们的文件末尾有效。在一个代码文件中定义的指令不会影响同一项目中的其他代码文件。
考虑以下示例:
function.cpp:
|
|
main.cpp:
|
|
上述程序将打印:
|
|
PRINT是在main.cpp中定义的,对function.cpp的代码没有任何影响(PRINT只是从定义点到main.cpp末尾定义有效)。
