重要的事情说三遍:
预处理指令是程序代码中以哈希符号(#
)开头的行。这些行不是程序语句,而是预处理器的指令。预处理器在实际编译代码开始之前检查代码,并在生成任何代码之前解析所有这些指令。
这些预处理指令仅在单行代码中有效。当找到换行符时,预处理指令结束。预处理指令的末尾不需要分号(;
)。唯一能使预处理指令跨越多行的方法是在行末换行符前加上反斜杠(\
)。
宏定义(#define, #undef)
要定义预处理宏,可以使用 #define
。其语法是:
#define identifier replacement
当预处理器遇到该指令时,它会在代码的其余部分中将所有 identifier
替换为 replacement
。这个 replacement
可以是一个表达式、语句、代码块或任何内容。预处理器不理解真正的 C++ 语法,它只是简单地将所有 identifier
替换为 replacement
。
#define TABLE_SIZE 100
int table1[TABLE_SIZE];
int table2[TABLE_SIZE];
在预处理器替换了 TABLE_SIZE
后,代码等效于:
int table1[100];
int table2[100];
#define
还可以使用参数来定义函数宏:
#define getmax(a,b) a>b?a:b
这将替换所有带有两个参数的 getmax
为替换表达式,并且也会用参数名替换每个参数,正如你期望的那样,如果它是一个函数:
// 函数宏
#include <iostream>
using namespace std;
#define getmax(a,b) ((a)>(b)?(a):(b))
int main()
{
int x=5, y;
y = getmax(x,2);
cout << y << endl;
cout << getmax(7,x) << endl;
return 0;
}
定义的宏不受代码块结构的影响。宏一直存在直到用 #undef
预处理指令取消定义:
#define TABLE_SIZE 100
int table1[TABLE_SIZE];
#undef TABLE_SIZE
#define TABLE_SIZE 200
int table2[TABLE_SIZE];
这将生成与以下代码相同的代码:
int table1[100];
int table2[200];
函数宏定义在替换序列中接受两个特殊操作符(#
和 ##
):
操作符 #
后跟参数名,将被替换为包含传递参数的字符串字面量(如同用双引号括起来一样):
#define str(x) #x
cout << str(test);
这将被翻译为:
cout << "test";
操作符 ##
连接两个参数,不留空格:
#define glue(a,b) a ## b
glue(c,out) << "test";
这也将被翻译为:
cout << "test";
由于预处理器替换发生在任何 C++ 语法检查之前,宏定义可能成为一个棘手的特性。但是请小心:依赖于复杂宏的代码变得难以阅读,因为预期的语法在许多情况下不同于程序员在 C++ 中通常期望的表达式。
条件包含(#ifdef, #ifndef, #if, #endif, #else 和 #elif)
这些指令允许在满足某个条件时包含或丢弃程序的一部分代码。
#ifdef
允许程序的某部分代码仅在指定的宏已被定义时编译,无论其值是什么。例如:
#ifdef TABLE_SIZE
int table[TABLE_SIZE];
#endif
在这种情况下,代码行 int table[TABLE_SIZE];
只有在 TABLE_SIZE
之前已用 #define
定义时才会编译,而不管其值是什么。如果未定义,则该行将不会包含在程序编译中。
#ifndef
用于完全相反的情况:在 #ifndef
和 #endif
指令之间的代码仅在指定的标识符尚未被定义时编译。例如:
#ifndef TABLE_SIZE
#define TABLE_SIZE 100
#endif
int table[TABLE_SIZE];
在这种情况下,如果在到达这段代码时,宏 TABLE_SIZE
尚未被定义,则它将被定义为值 100。如果它已经存在,则将保持其以前的值,因为 #define
指令不会执行。
#if
、#else
和 #elif
(即“else if”)指令用于指定在满足某个条件时所围绕的代码部分才会编译。紧随 #if
或 #elif
之后的条件只能计算常量表达式,包括宏表达式。例如:
#if TABLE_SIZE>200
#undef TABLE_SIZE
#define TABLE_SIZE 200
#elif TABLE_SIZE<50
#undef TABLE_SIZE
#define TABLE_SIZE 50
#else
#undef TABLE_SIZE
#define TABLE_SIZE 100
#endif
int table[TABLE_SIZE];
注意整个 #if
、#elif
和 #else
链接的指令结构以 #endif
结束。
#ifdef
和 #ifndef
的行为也可以通过在任何 #if
或 #elif
指令中使用特殊操作符 defined
和 !defined
分别实现:
#if defined ARRAY_SIZE
#define TABLE_SIZE ARRAY_SIZE
#elif !defined BUFFER_SIZE
#define TABLE_SIZE 128
#else
#define TABLE_SIZE BUFFER_SIZE
#endif
行控制(#line)
当我们编译程序并在编译过程中发生某些错误时,编译器会显示一个错误消息,引用发生错误的文件名和行号,以便更容易找到生成错误的代码。
#line
指令允许我们控制代码文件中的行号以及希望在发生错误时显示的文件名。其格式为:
#line number "filename"
其中 number
是将分配给下一行代码的新行号。从这一点起,后续行的行号将逐行递增。
"filename"
是一个可选参数,允许重新定义将显示的文件名。例如:
#line 20 "assigning variable"
int a?;
这段代码将生成一个错误,该错误将显示为文件 "assigning variable"
,行 20 的错误。
错误指令(#error)
当找到此指令时,它会中止编译过程,生成一个可以指定为其参数的编译错误:
#ifndef __cplusplus
#error 需要 C++ 编译器!
#endif
这个例子在宏名 __cplusplus
未定义时中止编译过程(这个宏名在所有 C++ 编译器中默认定义)。
源文件包含(#include)
在本教程的其他部分中经常使用该指令。当预处理器找到 #include
指令时,它将其替换为指定的头文件或文件的全部内容。使用 #include
有两种方式:
#include <header>
#include "file"
在第一种情况下,在尖括号 <>
中指定一个头文件。这用于包含实现提供的头文件,例如构成标准库的头文件(iostream
、string
等)。这些头文件是实际文件还是以其他形式存在是实现定义的,但无论如何,它们应通过此指令正确包含。
在第二种 #include
语法中使用引号,并包含一个文件。文件的搜索方式是实现定义的,通常包括当前路径。如果未找到文件,编译器会将指令解释为头文件包含,就像将引号(""
)替换为尖括号(<>
)一样。
Pragma 指令(#pragma)
该指令用于向编译器指定各种选项。这些选项是特定于您使用的平台和编译器的。有关可以用 #pragma
定义的可能参数的更多信息,请查阅您的编译器手册或参考资料。
如果编译器不支持 #pragma
的特定参数,它会被忽略 - 不会生成语法错误。
预定义宏名
以下宏名始终定义(它们都以两个下划线字符 _
开头和结尾):
宏名 | 值 |
---|---|
__LINE__ | 表示正在编译的源代码文件中的当前行的整数值。 |
__FILE__ | 包含正在编译的源文件名的字符串字面量。 |
__DATE__ | 形如 “Mmm dd yyyy” 的字符串字面量,包含编译过程开始的日期。 |
__TIME__ | 形如 “hh:mm:ss” 的字符串字面量,包含编译过程开始的时间。 |
__cplusplus | 整数值。所有 C++ 编译器都将此常量定义为某个值。其值取决于编译器支持的标准版本: - 199711L : ISO C++ 1998/2003- 201103L : ISO C++ 2011不符合标准的编译器将此常量定义为最多五位长的某个值。请注意,许多编译器并非完全符合标准,因此其定义的值既不是上述值之一。 |
__STDC_HOSTED__ | 如果实现是托管实现(具有所有标准头文件),则为 1 否则为 0 。 |
以下宏是可选定义的,通常取决于某个功能是否可用:
宏名 | 值 |
---|---|
__STDC__ | 在 C 中:如果定义为 1 ,则实现符合 C 标准。在 C++ 中:实现定义。 |
__STDC_VERSION__ | 在 C 中: - 199401L : ISO C 1990,修正案 1- 199901L : ISO C 1999- 201112L : ISO C 2011在 C++ 中:实现定义。 |
__STDC_MB_MIGHT_NEQ_WC__ | 如果多字节编码可能在字符字面量中给字符赋予不同的值,则为 1 |
__STDC_ISO_10646__ | 形如 yyyymmL 的值,指定遵循 Unicode 标准的日期以及 wchar_t 字符的编码 |
__STDCPP_STRICT_POINTER_SAFETY__ | 如果实现具有严格指针安全性(参见 get_pointer_safety ),则为 1 |
__STDCPP_THREADS__ | 如果程序可以有多个线程,则为 1 |
特定实现可能定义其他常量。
例如:
// 标准宏名
#include <iostream>
using namespace std;
int main()
{
cout << "这是第 " << __LINE__ << " 行";
cout << " 文件 " << __FILE__ << "。\n";
cout << "其编译开始于 " << __DATE__;
cout << " 时间 " << __TIME__ << "。\n";
cout << "编译器给出的 __cplusplus 值为 " << __cplusplus;
return 0;
}