当您编译项目时,您可能希望编译器完全按照您编写的方式编译每个代码文件。事实并非如此。
相反,在编译之前,每个代码 (.cpp) 文件都会经历一个预处理阶段。在此阶段,称为预处理器的程序对代码文件的文本进行各种更改。预处理器实际上不会以任何方式修改原始代码文件——相反,预处理器所做的所有更改要么临时发生在内存中,要么使用临时文件。
从历史上看,预处理器是与编译器分开的程序,但在现代编译器中,预处理器可以直接构建到编译器本身中。
预处理器所做的大部分工作都是相当无趣的。例如,它删除注释,并确保每个代码文件以换行符结尾。然而,预处理器确实有一个非常重要的作用:它处理#include
指令(我们稍后将详细讨论)。
当预处理器完成对代码文件的处理时,结果称为翻译单元。该翻译单元随后由编译器编译。
预处理、编译、链接的整个过程称为翻译。
预处理器指令
当预处理器运行时,它会扫描代码文件(从上到下),查找预处理器指令。预处理器指令(通常简称为指令)是以#符号开头并以换行符(不是分号)结尾的指令。这些指令告诉预处理器执行某些文本操作任务。请注意,预处理器不理解 C++ 语法——相反,指令有自己的语法(在某些情况下类似于 C++ 语法,而在其他情况下则不太相似)。
在本课中,我们将了解一些最常见的预处理器指令。
#Include
您已经看到了#include指令的实际应用(通常是#include <iostream>)。当您#include文件时,预处理器会将 #include 指令替换为所包含文件的内容。然后对包含的内容进行预处理(这可能会导致递归地预处理额外的#include),然后对文件的其余部分进行预处理。
考虑以下程序:
#include <iostream>
int main()
{
std::cout << "Hello, world!\n";
return 0;
}
当预处理器在此程序上运行时,预处理器将替换#include <iostream>
名为“iostream”的文件的内容,然后预处理包含的内容和文件的其余部分。
一旦预处理器完成对代码文件以及所有 #included 内容的处理,结果称为翻译单元。翻译单元是发送到编译器进行编译的内容。
翻译单元包含代码文件中已处理的代码以及所有#included 文件中已处理的代码。
由于#include几乎专门用于包含头文件,因此我们将在下一课(当我们讨论头文件时)更详细地讨论#include 。
宏定义
#define指令可用于创建宏。在 C++ 中,宏是定义如何将输入文本转换为替换输出文本的规则。
宏有两种基本类型:类对象宏和类函数宏。
类函数宏的行为类似于函数,并且具有相似的用途。它们的使用通常被认为是不安全的,几乎它们能做的任何事情都可以通过普通函数完成。
类似对象的宏可以通过以下两种方式之一定义:
#定义标识符
#定义标识符替换文本
顶部定义没有替换文本,而底部定义有。因为这些是预处理器指令(而不是语句),所以请注意这两种形式都以分号结尾。
宏的标识符使用与普通标识符相同的命名规则:可以使用字母、数字和下划线,不能以数字开头,也不应该以下划线开头。按照惯例,宏名称通常全部大写,并用下划线分隔。
带有替换文本的类似对象的宏
当预处理器遇到此指令时,任何进一步出现的标识符都会被替换为replacement_text。传统上,标识符全部以大写字母键入,并使用下划线表示空格。
考虑以下程序:
#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;
}
运行时,打印输出My name is: Alex
。
使用带有替换文本的类似对象的宏(在 C 中)作为将名称分配给文字的方法。这不再是必要的,因为 C++ 中有更好的方法。具有替换文本的类对象宏现在通常只能在遗留代码中看到。
我们建议完全避免使用此类宏,因为有更好的方法可以完成此类操作。我们将在Const 变量和符号常量中详细讨论这一点。
没有替换文本的类似对象的宏
类似对象的宏也可以在没有替换文本的情况下定义。
例如:
#define USE_YEN
这种形式的宏的工作方式与您可能期望的一样:任何进一步出现的标识符都将被删除并替换为任何内容!
这可能看起来毫无用处,而且对于进行文本替换也毫无用处。然而,这并不是这种形式的指令的通常用途。我们稍后将讨论此表单的用途。
与带有替换文本的类对象宏不同,这种形式的宏通常被认为可以使用。
条件编译
条件编译预处理器指令允许您指定在什么条件下某些内容将或不会编译。有很多不同的条件编译指令,但我们只介绍迄今为止最常用的三个:#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;
}
因为 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 从未被#定义。
除了 #ifdef PRINT_BOB 和 #ifndef PRINT_BOB,您还将看到 #if Defined(PRINT_BOB) 和 #if !define(PRINT_BOB)。 它们的作用相同,但使用稍微更 C++ 风格的语法。
#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;
}
上面的代码只打印“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;
}
类似对象的宏不会影响其他预处理器指令
#define PRINT_JOE
#ifdef PRINT_JOE
// ...
既然我们将 PRINT_JOE 定义为空,那么预处理器为什么不将 #ifdef PRINT_JOE 中的 PRINT_JOE 替换为空呢?
宏只会导致普通代码的文本替换。 其他预处理器命令将被忽略。 因此,#ifdef PRINT_JOE 中的 PRINT_JOE 被保留。
例如:
#define FOO 9 // 这是一个宏替换
#ifdef FOO // 该 FOO 不会被替换,因为它是另一个预处理器指令的一部分
std::cout << FOO << '\n'; // 该 FOO 被替换为 9,因为它是正常代码的一部分
#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” 的程序相同。 为了可读性,您通常需要在函数外部#define 标识符。
一旦预处理器完成,该文件中所有定义的标识符都将被丢弃。 这意味着指令仅从定义点到定义它们的文件末尾有效。 一个代码文件中定义的指令不会影响同一项目中的其他代码文件。
考虑以下示例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(); // 函数 doSomething() 的前向声明
#define PRINT
int main()
{
doSomething();
return 0;
}
输出:
Not printing!
尽管 PRINT 是在 main.cpp 中定义的,但这不会对 function.cpp 中的任何代码产生任何影响(PRINT 仅是从定义点到 main.cpp 末尾的#define)。
原创文章,作者:jkhxw,如若转载,请注明出处:https://www.jkhxw.com/cpp-preprocessor/