12、CMake中的编译选项:add_compile_options()、target_compile_options()、CMAKE_CXX_FLAGS、编译优化级别
编译选项这东西,说白了就是告诉编译器「你想怎么干活」。你是要快一点编译,还是要生成的程序跑得快一点?是要严格检查代码,还是睁一只眼闭一只眼?这些都由编译选项来控制。
我在做嵌入式项目时,就吃过编译选项的亏。有一次程序在开发板上跑得好好的,换到另一块板子上就崩了。查了半天,发现是优化级别太高,编译器把一些变量给优化掉了。嗯,从那以后我对编译选项就格外上心。
一、全局编译选项:add_compile_options()
这个命令是给整个项目里的所有目标(target)添加编译选项。你只要写一次,所有源文件编译时都会带上这些选项。
add_compile_options(-Wall -Wextra -Werror)
上面这行代码,会让项目中所有源文件都开启警告全开,并且把警告当作错误处理。我个人习惯在开发阶段加上 -Wall -Wextra,但不会加 -Werror,因为有些第三方库的警告你管不了,一加就编译不过。
二、目标级编译选项:target_compile_options()
这个就灵活多了。你可以只给某个特定的库或可执行文件加选项,其他目标不受影响。
add_library(my_lib STATIC src/my_lib.cpp)
target_compile_options(my_lib PRIVATE -fno-exceptions)
这里 PRIVATE 表示这个选项只对 my_lib 本身生效。还有 INTERFACE 和 PUBLIC 两种可见性:
| 可见性 | 对目标本身 | 对链接者 |
|---|---|---|
| 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。
五、知识体系结构图
下面这张图,帮你理清编译选项的几种设置方式及其关系:
六、实战建议
说了这么多,到底怎么选?我总结几条经验:
- 开发阶段:用
-O0 -g,配合-Wall -Wextra。调试信息完整,警告全开,问题早发现。 - 测试阶段:切到
-O2,看看有没有优化导致的 bug。很多问题在 -O0 下不出现,一开优化就暴露。 - 发布阶段:根据场景选
-O2或-O3。如果是嵌入式,考虑-Os。 - 特殊模块:用
target_compile_options()单独控制。比如某个加密算法库,可以单独开-O3。
volatile 关键字才解决。所以,多线程代码建议先用 -O0 调试通过,再逐步提高优化级别。
好了,编译选项这块就聊到这儿。记住一个原则:能用 target_compile_options() 就别用全局方式。这样你的项目结构更清晰,别人接手时也更容易理解每个模块的编译需求。