预处理指令
预处理指令又称预编译指令,是在 C++ 源文件编译前执行的指令。预处理指令可以定义或取消定义宏、条件编译、包含文件、引发错误、设置编译器等等。预处理指令永远以 #
开头,且以换行符为结尾。下面将分别介绍这些预处理指令。
#define
#undef
)
宏( 宏使用 #define
定义。它大致有三种用法。
定义可供替换的宏
#define 宏名 替换文本
此条指令将源代码中出现的所有 宏名
替换为 替换文本
。请注意这里的 宏名
是经过词法分析的,即当一个“单词”与 宏名
相符时进行替换。比如:
#define MAX_N 10000
相当于把 MAX_N
这个词在编译前替换为 10000
:
定义宏,但不进行替换
#define 宏名
此条指令仅仅定义了一个宏,但它不能用于替换。它主要可以用于条件编译(参见下文)。比如:
#define ONLINE_JUDGE
定义可供替换的类函数宏
你可以定义一个类似函数一样的宏。即:
#define 宏名(形参列表) 带形参的替换文本
你可以在 宏名
后面附上 形参列表
,这样你可以像函数那样使用这个宏。替换的时候,会用你的实参去替换替换文本的形参。替换举几个例子的话:
#define PRINT(sth) cout << sth << endl
PRINT("Hello"); // 替换为 cout << "Hello" << endl;
PRINT(42); // 替换为 cout << 42 << endl;
又如:
形参列表可以不限定长度,使用省略号即可:
#define 宏名(形参列表, ...) 带形参的替换文本 #define 宏名(...) 替换文本
你可以使用 __VA_ARGS__
这个标识符来指代省略号所省略的内容(可以为空)。比如:
#define INT_ARRAY(name, len, ...) int name[len]{ __VA_ARGS__ }
INT_ARRAY(bar, 10, 3, 4, 5, 6); // 替换为 int bar[10]{ 3, 4, 5, 6 };
注意:仿函数宏的实参只识别 (
)
,而不识别 <
>
。这意味着将模板实参作为宏实参可能会被错误地替换。
#define F(a, b) /* ... */
F(std::pair<int, int>, int); // 错误:参数个数不匹配
// a 替换为 std::pair<int
// b 替换为 int>
// 出现多余的实参 int
#
运算符
#宏形参
在宏替换中, #
运算符可以将这个形参的文本用双引号引起,使之成为一个字符串字面量。举例来说:
#define PRINT1(a) cout << a << endl
#define PRINT2(a) cout << #a << endl
PRINT1(number); // 替换为 cout << number << endl;
PRINT2(number); // 替换为 cout << "number" << endl;
PRINT2("Hello"); // 替换为 cout << "\"Hello\"" << endl;
##
运算符
左侧文本或形参##右侧文本或形参
##
可以将文本和形参紧密地、无空格地粘贴在一起。这一运算符可以用于:形成更长的变量名、组成更多位数的算术类型字面量、组合为复合赋值运算符等。但不能用于创建注释。例如:
__VA_OPT__
运算符
__VA_OPT__(文本)
当使用 __VA_ARGS__
时,允许在替换文本中出现 __VA_OPT__
运算符。该运算符的含义是,如果 __VA_ARGS__
为空字符串,则在最终替换结果中删去 文本
,否则在最终替换结果中保留 文本
。例如:
#define G(...) f(0 __VA_OPT__(,) __VA_ARGS__)
G(1, 2); // 替换为 f(0, 1, 2);
G(); // 替换为 f(0); ,注意没有多余的逗号
除了 __VA_OPT__
运算符,还有一种扩展语法:当替换文本中出现 ,##__VA_ARGS__
时,则在 __VA_ARGS__
为空字符串时删去开头的逗号。尽管大多数编译器都启用此扩展,但它并不标准。
一些编译器预定义的宏
宏名 | 含义 |
---|---|
__cplusplus | 代表所用的 C++ 标准版本,定义为值 199711L (C++98)、201103L (C++11)、201402L (C++14)、 201703L (C++17) 或 202002L (C++20)。 |
__FILE__ | 定义为当前源文件名。 |
__LINE__ | 定义为当前源文件行号 |
__DATE__ | 定义为编译日期,形式为 "Mmm dd yyyy" 的字符串字面量。 |
__TIME__ | 定义为时间,形式为 "hh:mm:ss" 的字符串字面量。 |
你可以随时通过 #undef
来取消某个宏定义。
#undef 宏名
#if
#elif
#else
#endif
#ifdef
#ifndef
)
条件编译( 条件编译是指编译器选择性地编译某一部分而不编译其它部分。
#if 常量表达式 代码1 #else 代码2 #endif
若常量表达式为非 0
值,则编译 代码1
但不编译 代码2
;反之若常量表达式为 0
值,则编译 代码2
但不编译 代码1
。当然, #else
即 代码2
是可以省略的。例如:
如果连续多个 #else #if
,可以使用 #elif
来简化。
defined
运算符
defined
运算符可以返回当前环境是否定义了某个宏。
defined 宏名 defined(宏名)
若宏存在,则值为 1
;若宏不存在,则值为 0
。比如:
另外, #ifdef
等价于 #if defined
, #ifndef
等价于 #if !defined
。它们的用法是:
#ifdef 宏名 #ifndef 宏名
#include
)
包含文件( #include
指令可以把某个源文件直接插入进指令所在的位置。当文件编译时,将不断递归地展开 #include
指令,直至不存在#include
指令。其语法如下:
#include <头文件名> #include "文件名"
其中,用尖括号( <>
)包括的文件名将优先在系统库文件中查找,而用引号引起的文件名将优先在当前源文件所在的路径中查找。 我们经常使用的 <iostream>
<cmath>
<iomanip>
等库,都是位于系统库路径的名为iostream
cmath
和 iomanip
的文件,其中声明(或定义)了常用的变量、函数或者其他对象:比如 cin
cout
sqrt
和 setw
等等。在一些文本编辑器中,你可以按下 Ctrl 键的同时点击 #include
指令,就可以查看这些头文件。
#error
)
引发错误( 你可以手动地引发一个编译错误,通过这样的语法:
#error 错误信息
但凡编译到此指令编译器将停止编译,并将错误信息输出。
#pragma
#line
)
设置编译器( #pragma 某些参数
你可以通过某些参数去修改编译器的行为(比如静态链接等)。具体的参数语法和形式各有不同,可以参照编译器的说明文档。
常见的通用语法有 #pragma once
和 #pragma pack
。
#pragama once
指定这个文件只能被 #include
一次,也就是说等价于
#pragma pack
指定接下来定义的结构体的对齐。虽然 alignas
修饰也可以指定对齐,但它只能让对齐更严格(值更大),而 #pragma pack
可以指定更宽松的对齐要求。
#pragma pack(对齐) #pragma pack()
#pragma pack
可接受一个参数 对齐
,它需要是 2 的整数次幂。当写下这条预处理指令后,之后的结构体对齐皆设置为 对齐
。#pragma pack()
取消此设置,恢复默认对齐。比如:
这里,我使用 alignof
运算符检查某个类型或对象的对其要求。
但是,如果你使用 GCC 或 Clang,我们更推荐用特性(Attribute)来实现:
#line
用于覆盖设置当前的行号或者当前的文件名。此语法一般用于机器生成的代码。
#line 行号 #line 行号"文件名"