第八讲:指定编译器——CMAKE_C_COMPILER 和 CMAKE_CXX_COMPILER 实战
说实话,编译器选择这件事,很多新手觉得没啥好讲的。不就是 gcc 和 g++ 嘛?
但我在实际项目中踩过不少坑。有一次,客户要求必须用 ARM 交叉编译器,结果我忘了设置,直接在 x86 机器上编译了一整天。最后链接时报了一堆莫名其妙的错误。嗯,从那以后,我再也不敢小看这两个变量了。
8.1 为什么需要手动指定编译器?
CMake 默认会去系统 PATH 里找编译器。在 Linux 上,它通常能找到 gcc/g++。在 macOS 上,它可能找到 clang。在 Windows 上,它可能找到 MSVC。
但现实项目哪有这么简单?你想想看:
- 你要交叉编译,比如在 Linux 上编译 ARM 程序
- 你同时装了 gcc-9 和 gcc-11,想用特定版本
- 你的 CI 环境里只有 clang,没有 gcc
- 你用的嵌入式工具链,名字叫 arm-none-eabi-gcc
这些场景下,CMake 自动检测就不够用了。你得手动告诉它:用哪个编译器。
核心变量:
CMAKE_C_COMPILER— C 编译器路径CMAKE_CXX_COMPILER— C++ 编译器路径
8.2 三种指定编译器的方式
我个人习惯用第一种方式,因为它最干净。但每种方式都有适用场景,我一个个说。
方式一:命令行传递(推荐)
这是最直接的方式。在运行 cmake 时,用 -D 参数传进去:
# 用 gcc-9 和 g++-9
cmake -DCMAKE_C_COMPILER=/usr/bin/gcc-9 \
-DCMAKE_CXX_COMPILER=/usr/bin/g++-9 \
..
# 用 clang
cmake -DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
..
# 交叉编译:ARM 工具链
cmake -DCMAKE_C_COMPILER=arm-none-eabi-gcc \
-DCMAKE_CXX_COMPILER=arm-none-eabi-g++ \
..
为什么我推荐这种方式?因为它不会污染 CMakeLists.txt。你想想看,如果我把编译器路径写死在 CMakeLists.txt 里,换个人、换个环境就得改代码,多麻烦。
方式二:在 CMakeLists.txt 中设置
有时候项目固定使用某个编译器,比如嵌入式项目。那写在 CMakeLists.txt 里也可以:
# 必须在 project() 之前设置!
set(CMAKE_C_COMPILER "arm-none-eabi-gcc")
set(CMAKE_CXX_COMPILER "arm-none-eabi-g++")
project(MyProject C CXX)
⚠️ 重要: 这两个变量必须在 project() 命令之前设置。因为 project() 会触发编译器检测,一旦检测完成,再改编译器就无效了。
我曾经犯过这个错——把 set 写在了 project 后面,结果 CMake 还是用了系统默认的 gcc。排查了半天才发现是顺序问题。
方式三:使用工具链文件(Toolchain File)
这是最专业的方式,尤其适合交叉编译。创建一个单独的文件,比如 arm-toolchain.cmake:
# arm-toolchain.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR ARM)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
使用时:
cmake -DCMAKE_TOOLCHAIN_FILE=arm-toolchain.cmake ..
这种方式的好处是:工具链配置和项目代码完全分离。换一个芯片,换个工具链文件就行,CMakeLists.txt 不用动。
8.3 验证编译器是否生效
设置完之后,怎么确认 CMake 真的用了你指定的编译器?我一般用两种方法:
方法一:看 CMake 输出
运行 cmake 时,注意看前几行:
$ cmake ..
-- The C compiler identification is GNU 9.4.0
-- The CXX compiler identification is GNU 9.4.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
...
如果显示的是你指定的版本,那就对了。
方法二:打印变量
在 CMakeLists.txt 里加一行调试信息:
message(STATUS "Using C compiler: ${CMAKE_C_COMPILER}")
message(STATUS "Using CXX compiler: ${CMAKE_CXX_COMPILER}")
8.4 避坑指南
💡 我踩过的坑,你千万别踩:
- 路径带空格: Windows 上有些编译器路径带空格(比如
C:\Program Files\...),记得用引号包起来。 - 编译器版本不匹配: C 和 C++ 编译器最好来自同一套工具链。混用 gcc-9 和 g++-11 可能会链接失败。
- 缓存问题: 如果你之前用 gcc 配置过,想换成 clang,记得先删掉
CMakeCache.txt。否则 CMake 会缓存旧的编译器路径。 - 环境变量 CC 和 CXX: CMake 也会读取
CC和CXX环境变量。如果你同时设置了环境变量和-D参数,-D优先级更高。
8.5 完整示例:一个跨平台项目
假设我们要写一个简单的程序,在 Linux 上用 gcc 编译,在 macOS 上用 clang 编译,还要能交叉编译到 ARM。来看完整的 CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
# 注意:这里不设置编译器,让用户通过命令行或工具链文件指定
project(CompilerDemo C CXX)
# 打印当前使用的编译器
message(STATUS "Compiler: ${CMAKE_C_COMPILER}")
message(STATUS "Compiler version: ${CMAKE_C_COMPILER_VERSION}")
# 根据编译器设置不同的编译选项
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
target_compile_options(myapp PRIVATE -Wall -Wextra)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_compile_options(myapp PRIVATE -Wall -Wextra -Wpedantic)
elseif(MSVC)
target_compile_options(myapp PRIVATE /W4)
endif()
add_executable(myapp main.cpp)
使用方式:
# Linux 上默认用 gcc
cmake ..
make
# macOS 上指定用 gcc-11
cmake -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 ..
make
# ARM 交叉编译
cmake -DCMAKE_TOOLCHAIN_FILE=arm-toolchain.cmake ..
make
8.6 知识体系图
下面这张图总结了编译器选择的完整流程和三种方式的关系:
8.7 总结
指定编译器这件事,说白了就是告诉 CMake:用哪个工具来编译我的代码。
- 日常开发:用命令行
-D参数,灵活方便 - 嵌入式项目:用工具链文件,干净专业
- 临时测试:直接在 CMakeLists.txt 里 set,但记得放在 project() 前面
我个人强烈建议养成用工具链文件的习惯。尤其是团队协作时,每个人把工具链文件放在项目目录下,一目了然。我曾经见过一个项目,五个人用五种方式指定编译器,最后 CI 环境炸了三天才修好。嗯,工具链文件就是用来避免这种混乱的。