第八讲:指定编译器——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 也会读取 CCCXX 环境变量。如果你同时设置了环境变量和 -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 知识体系图

下面这张图总结了编译器选择的完整流程和三种方式的关系:

编译器选择流程 CMake 配置开始 是否指定编译器? 自动检测系统 默认编译器 使用指定的 编译器 执行 project() 编译器检测完成 自动检测方式 • 系统 PATH 查找 • 环境变量 CC/CXX • 默认编译器 指定编译器方式 ① 命令行 -D 参数 ② CMakeLists.txt 设置 ③ 工具链文件

8.7 总结

指定编译器这件事,说白了就是告诉 CMake:用哪个工具来编译我的代码。

  • 日常开发:用命令行 -D 参数,灵活方便
  • 嵌入式项目:用工具链文件,干净专业
  • 临时测试:直接在 CMakeLists.txt 里 set,但记得放在 project() 前面

我个人强烈建议养成用工具链文件的习惯。尤其是团队协作时,每个人把工具链文件放在项目目录下,一目了然。我曾经见过一个项目,五个人用五种方式指定编译器,最后 CI 环境炸了三天才修好。嗯,工具链文件就是用来避免这种混乱的。