C++ 条件编译

在 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,或者删掉首尾两行指令就可以了。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号