在 C++ 程序中,普通的条件语句是在程序运行起来之后才判断真假的。不管运行时走哪一条分支,所有分支的代码都会编译进最终的可执行程序中。
但在跨平台开发或调试程序时,我们常常希望在正式编译开始前,只挑选符合条件的那一部分代码参与编译,把其余代码直接忽略掉。这种做法在 C++ 中就叫做 “条件编译”,常用的条件编译指令如下表所示。
| 预处理指令 | 英文原意 | 作用说明 |
|---|---|---|
| #if | if(如果) | 如果后面的常量表达式结果非 0,则保留对应代码参与编译 |
| #elif | else if(否则如果) | 当前面的条件不成立时,继续判断新的常量表达式 |
| #else | else(否则) | 当前面所有条件都不成立时,保留对应代码参与编译 |
| #ifdef | if defined(如果已定义) | 如果指定的宏名已经定义过,则保留对应代码参与编译 |
| #ifndef | if not defined(如果未定义) | 如果指定的宏名还没有定义过,则保留对应代码参与编译 |
| #endif | end if(结束判断) | 结束一段 #if、#ifdef 或 #ifndef 条件编译块 |
#if、#elif 与 #else 指令
在 C++ 中,#if、#elif 和 #else 按照 “常量表达式的值” 来判断保留哪一段代码。它的分支逻辑和前面学过的 if...else 语句非常相似,只不过是在编译开始前的预处理阶段执行的。
语法:
#if 常量表达式1
// 代码段 1
#elif 常量表达式2
// 代码段 2
#else
// 代码段 3
#endif说明:
这组指令通常会配合前面学过的宏定义一起使用,具体需要注意以下 3 点:
- 条件必须是常量表达式:
#if和#elif后面跟的必须是预处理阶段就能算出结果的整型常量表达式,比如整数字面量或常量宏。如果结果非 0,则保留下方代码;如果结果为 0,则直接跳过。 - 分支可以按需省略:
#elif可以写多个也可以不写,#else同样属于可选分支,我们可以根据实际需要省略。 - 必须以 #endif 结尾:每一组
#if条件编译块的最后,我们都必须写上#endif来标志结束,不能遗漏。
示例 1:切换编译模式
#include <iostream>
#define BUILD_MODE 2
int main() {
#if BUILD_MODE == 1
std::cout << "调试模式\n";
#elif BUILD_MODE == 2
std::cout << "正式发布模式\n";
#else
std::cout << "其他模式\n";
#endif
return 0;
}运行结果如下。
正式发布模式分析:
在这个例子中,我们在开头通过 #define BUILD_MODE 2 将宏 BUILD_MODE 的值设为了 2。预处理器检查到 #elif BUILD_MODE == 2 时发现条件成立,于是只把输出 “正式发布模式” 的那一行代码留下来交给编译器,另外两个不成立的分支在预处理阶段就会直接忽略掉。
另外,函数内部的 #if、#elif 等指令之所以顶格书写,是因为预处理指令不受花括号 {} 的缩进限制,顶格写更容易和普通的 C++ 语句区分开来。
需要特别注意的是,小伙伴们千万不要把普通的 C++ 变量写进 #if 条件里。下面这种写法就是错误的。
// 错误方式
int mode = 2;
#if mode == 2
std::cout << "正式发布模式\n";
#endif为什么这样写不行呢?因为预处理器工作时,程序还没有开始编译和运行,普通变量 mode 根本不存在。预处理器会把没有定义成宏的普通名称一律当成 0 来处理,导致 #if mode == 2 变成 #if 0 == 2,条件永远不成立。
因此,如果我们需要在程序运行期间根据普通变量的值走不同分支,直接使用普通的 if...else 语句就可以了。
#ifdef 与 #ifndef 指令
有时候我们并不关心某个宏的具体数值是多少,只关心 “这个宏名之前有没有定义过”。这时使用 #ifdef 和 #ifndef 会更加直观。
语法:
#ifdef 宏名
// 代码段
#endif
#ifndef 宏名
// 代码段
#endif说明:
其中,#ifdef 表示如果前面已经定义过指定的宏,就保留中间的代码段。#ifndef 则恰恰相反,表示如果前面还没有定义过该宏,才保留中间的代码段。比如前面学习头文件保护时,我们就是借助 #ifndef 判断宏名是否存在,防止头文件重复展开。
对于 #ifdef 来说,只要前面使用 #define 定义过这个宏名,哪怕没有给它写具体数值,条件依然成立。比如我们只写一行空宏 #define DEBUG_LOG,就能在实际开发中拿来做调试日志的总开关。
示例 2:控制调试日志开关
#include <iostream>
#define DEBUG_LOG // 调试开关宏
int main() {
int price = 100;
#ifdef DEBUG_LOG
std::cout << "[Debug] price = " << price << '\n';
#endif
std::cout << "应付金额:" << price << '\n';
return 0;
}运行结果如下。
[Debug] price = 100
应付金额:100分析:
#ifdef DEBUG_LOG 表示检查前面是否定义过 DEBUG_LOG 宏。在开发调试阶段,我们在文件顶部保留 #define DEBUG_LOG,程序运行时就会把变量 price 的中间状态打印出来。
等项目开发完毕准备正式发布时,我们只需要把 #define DEBUG_LOG 这一行删掉或注释掉,所有用 #ifdef DEBUG_LOG 包裹的调试代码就会在预处理阶段自动忽略。
此外,判断宏是否定义过还可以改用 #if 配合 defined() 操作符来写。对于 #ifdef DEBUG_LOG 来说,下面 2 种方式是等价的。
// 方式 1
#ifdef DEBUG_LOG
// 方式 2
#if defined(DEBUG_LOG)同理,#ifndef DEBUG_LOG 也等价于 #if !defined(DEBUG_LOG)。当只需要判断单个宏是否存在时,方式 1 写起来更简短。如果需要配合 && 或 || 同时判断多个宏是否存在,使用方式 2 写成 #if defined(A) && defined(B) 会比嵌套多层 #ifdef 清晰得多。
提示 在实际开发中,除了在代码里写 #define DEBUG_LOG,我们也可以在编译命令中通过 -D 参数直接定义宏,比如执行 g++ -DDEBUG_LOG main.cpp -o app。这样不用改动源代码,就能在编译时灵活开启调试日志。
在使用条件编译时,很多初学的小伙伴容易把 #ifdef 和 #if 混为一谈。其实,“宏已经定义过” 和 “宏的值非 0” 完全是两码事。如果我们写下 #define FEATURE_LOG 0,结果会怎样呢?下面我们来看一个对比示例。
示例 3:对比 #ifdef 与 #if
#include <iostream>
#define FEATURE_LOG 0 // 宏的值设为 0
int main() {
#ifdef FEATURE_LOG
std::cout << "#ifdef 分支生效\n";
#endif
#if FEATURE_LOG
std::cout << "#if 分支生效\n";
#endif
return 0;
}运行结果如下。
#ifdef 分支生效分析:
从运行结果可以看出,程序只输出了 “#ifdef 分支生效”。这是因为 #ifdef FEATURE_LOG 只检查 FEATURE_LOG 这个宏名存不存在。由于文件开头确实定义了它,哪怕它的值是 0,条件依然成立。
而下面的 #if FEATURE_LOG 检查的是 “宏展开后的常量值是不是非 0”。由于 FEATURE_LOG 展开后是 0,条件不成立,这一分支在预处理阶段就直接跳过了。小伙伴们在阅读项目源码里的功能开关宏时,一定要看清判断条件写的是 #ifdef 还是 #if。
C++ 条件编译的应用场景
除了前面演示的模式切换与调试日志开关,条件编译在实际项目中还常用于以下两个场景。
1. 跨平台代码适配
不同操作系统下的编译器,都会默认预定义好代表当前平台的内置宏。比如 32 位和 64 位 Windows 系统都会自动定义 _WIN32,Linux 系统会自动定义 __linux__,macOS 系统则会自动定义 __APPLE__。
我们可以借助条件编译检查这些内置宏,让同一份 C++ 源代码在不同操作系统下自动挑选对应的代码参与编译。
示例 4:识别当前操作系统
#include <iostream>
int main() {
#if defined(_WIN32)
std::cout << "当前系统:Windows\n";
#elif defined(__linux__)
std::cout << "当前系统:Linux\n";
#elif defined(__APPLE__)
std::cout << "当前系统:macOS\n";
#else
std::cout << "当前系统:其他平台\n";
#endif
return 0;
}运行结果如下。
当前系统:Linux分析:
由于在线运行环境位于 Linux 系统下,编译器默认定义了 __linux__ 宏,因此预处理器只保留了输出 “当前系统:Linux” 的那一行代码。如果小伙伴们把这段代码复制到自己的 Windows 电脑上编译运行,它就会自动输出 “当前系统:Windows”。
2. 临时屏蔽大段代码
在调试程序时,我们有时想把一大段代码暂时屏蔽掉,不让它参与编译。由于 C++ 的多行注释 /* ... */ 不支持嵌套,如果那段代码内部原本就已经写有 /* ... */ 注释,再在外面套一层就会直接引发编译报错。
遇到这种情况,我们可以使用 #if 0 配合 #endif 来屏蔽整段代码:
#if 0
/* 内部原本就有多行注释也不会报错 */
int a = 10;
std::cout << a << '\n';
#endif因为常量 0 永远代表条件不成立,所以 #if 0 和 #endif 之间的整段代码在预处理阶段就会直接忽略掉。以后想要恢复这段代码时,只需要把 #if 0 改成 #if 1,或者删掉首尾两行指令就可以了。
