12、CMake中的编译选项:add_compile_options()、target_compile_options()、CMAKE_CXX_FLAGS、编译优化级别

编译选项这东西,说白了就是告诉编译器「你想怎么干活」。你是要快一点编译,还是要生成的程序跑得快一点?是要严格检查代码,还是睁一只眼闭一只眼?这些都由编译选项来控制。

我在做嵌入式项目时,就吃过编译选项的亏。有一次程序在开发板上跑得好好的,换到另一块板子上就崩了。查了半天,发现是优化级别太高,编译器把一些变量给优化掉了。嗯,从那以后我对编译选项就格外上心。

一、全局编译选项:add_compile_options()

这个命令是给整个项目里的所有目标(target)添加编译选项。你只要写一次,所有源文件编译时都会带上这些选项。

add_compile_options(-Wall -Wextra -Werror)

上面这行代码,会让项目中所有源文件都开启警告全开,并且把警告当作错误处理。我个人习惯在开发阶段加上 -Wall -Wextra,但不会加 -Werror,因为有些第三方库的警告你管不了,一加就编译不过。

注意:add_compile_options() 影响的是当前 CMakeLists.txt 及其子目录中的所有目标。如果你只想控制某个子目录,记得用作用域隔离。

二、目标级编译选项:target_compile_options()

这个就灵活多了。你可以只给某个特定的库或可执行文件加选项,其他目标不受影响。

add_library(my_lib STATIC src/my_lib.cpp)
target_compile_options(my_lib PRIVATE -fno-exceptions)

这里 PRIVATE 表示这个选项只对 my_lib 本身生效。还有 INTERFACEPUBLIC 两种可见性:

可见性 对目标本身 对链接者
PRIVATE 生效 不生效
INTERFACE 不生效 生效
PUBLIC 生效 生效

我曾经在一个大型项目中,把某个核心库的异常支持关掉了(-fno-exceptions),但忘了用 PRIVATE,结果导致所有链接这个库的可执行文件也编译不过。你想想看,排查了整整一个下午才发现是可见性设错了。

三、CMAKE_CXX_FLAGS:最原始的方式

这是 CMake 提供的变量,用来设置 C++ 编译器的全局标志。它和 add_compile_options() 效果类似,但写法更传统。

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -O2")

这里有个坑:CMAKE_CXX_FLAGS 是全局变量,一旦设置,整个项目都会受影响。而且它不支持 PRIVATE/PUBLIC 这种精细控制。所以我的建议是:

  • 能用 target_compile_options() 就别用 CMAKE_CXX_FLAGS
  • 除非你是在写一个超级简单的单文件项目
  • 或者你需要在命令行里临时覆盖编译选项
小技巧:如果你想在命令行临时加个选项,可以这样:cmake -DCMAKE_CXX_FLAGS="-O0 -g" ..。这在调试时特别方便,不用改 CMakeLists.txt。

四、编译优化级别

优化级别是编译选项里最常用的。常见的级别有:

级别 含义 适用场景
-O0 不优化 调试阶段,方便单步跟踪
-O1 轻度优化 日常开发,兼顾速度和调试
-O2 中度优化 发布版本,性能较好
-O3 高度优化 追求极致性能,但可能增加代码体积
-Os 优化体积 嵌入式或存储受限场景
-Og 优化调试体验 比 -O0 快,但调试信息依然完整

在 CMake 中,你可以通过 CMAKE_BUILD_TYPE 来快速切换优化级别:

cmake -DCMAKE_BUILD_TYPE=Release ..   # 默认用 -O3
cmake -DCMAKE_BUILD_TYPE=Debug ..     # 默认用 -O0 -g

但要注意,CMAKE_BUILD_TYPE 只对单配置生成器(如 Makefile、Ninja)有效。对于 Visual Studio 这种多配置生成器,你得在 IDE 里选配置,或者用 cmake --build . --config Release

核心要点:优化级别不是越高越好。我见过有人把嵌入式项目开到 -O3,结果程序跑飞了。因为编译器为了优化,可能会重排指令、合并变量,这些在底层硬件操作时很容易出问题。所以驱动代码、中断处理函数,我一般用 -O0 或 -O1。

五、知识体系结构图

下面这张图,帮你理清编译选项的几种设置方式及其关系:

CMake 编译选项设置方式 add_compile_options() target_compile_options() CMAKE_CXX_FLAGS 全局生效 影响所有目标 适合通用选项 如 -Wall -Wextra 目标级生效 支持 PRIVATE/PUBLIC 精细控制 推荐使用 全局变量 传统方式 适合简单项目 命令行覆盖方便 编译优化级别:-O0 / -O1 / -O2 / -O3 / -Os / -Og 通过 CMAKE_BUILD_TYPE 快速切换:Debug → -O0 -g | Release → -O3

六、实战建议

说了这么多,到底怎么选?我总结几条经验:

  1. 开发阶段:用 -O0 -g,配合 -Wall -Wextra。调试信息完整,警告全开,问题早发现。
  2. 测试阶段:切到 -O2,看看有没有优化导致的 bug。很多问题在 -O0 下不出现,一开优化就暴露。
  3. 发布阶段:根据场景选 -O2-O3。如果是嵌入式,考虑 -Os
  4. 特殊模块:用 target_compile_options() 单独控制。比如某个加密算法库,可以单独开 -O3
避坑指南:我曾经在调试一个多线程程序时,发现某个变量值总是不对。折腾了两天,最后发现是 -O2 优化把变量放到了寄存器里,而另一个线程直接修改了内存。加上 volatile 关键字才解决。所以,多线程代码建议先用 -O0 调试通过,再逐步提高优化级别。

好了,编译选项这块就聊到这儿。记住一个原则:能用 target_compile_options() 就别用全局方式。这样你的项目结构更清晰,别人接手时也更容易理解每个模块的编译需求。


公众号:蓝海资料掘金营,微信deep3321