前置知识: C

构建系统

4 minIntermediate2026/6/14

CMake与构建工具

概述

C语言项目从源代码到可执行文件需要经过编译、链接等步骤。当项目规模增大、源文件增多、需要跨平台支持时,手动执行 gcc 命令变得不切实际。构建系统(Build System)负责自动化管理编译流程,解决依赖关系,并支持跨平台构建。CMake 是目前C/C++项目最主流的构建系统,此外还有 Make、Ninja、Meson 等工具。

基础概念

编译流程

C语言项目从源码到可执行文件的基本流程:

  1. 预处理:展开宏、处理条件编译、包含头文件(.c -> .i
  2. 编译:将预处理后的代码翻译为汇编代码(.i -> .s
  3. 汇编:将汇编代码翻译为目标文件(.s -> .o
  4. 链接:将目标文件和库文件合并为可执行文件(.o -> 可执行文件)

构建系统的作用

  • 自动检测源文件之间的依赖关系
  • 只重新编译发生变化的文件(增量构建)
  • 管理编译选项(优化级别、警告、标准版本)
  • 处理第三方库的查找和链接
  • 支持跨平台构建(生成不同平台的构建文件)

常见构建工具

工具说明
Make最经典的构建工具,使用 Makefile
CMake元构建系统,生成 Makefile 或其他构建文件
Ninja专注于速度的底层构建工具
Meson新一代构建系统,使用 Python 风格语法
AutotoolsGNU 项目的传统构建系统

快速上手

最简单的 CMake 项目

项目结构:

myproject/
  CMakeLists.txt
  src/
    main.c

CMakeLists.txt:

# 最低CMake版本要求
cmake_minimum_required(VERSION 3.20)

# 项目名称和语言
project(MyProject C)

# 设置C标准
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)

# 添加可执行文件
add_executable(program src/main.c)

构建命令:

# 创建构建目录并进入
mkdir build && cd build

# 配置项目(生成构建文件)
cmake ..

# 编译项目
cmake --build .

使用 Make 直接构建

# Makefile 示例
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -O2
TARGET = program
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o)

# 默认目标
all: $(TARGET)

# 链接
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

# 编译
%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

# 清理
clean:
	rm -f $(OBJS) $(TARGET)

# 声明伪目标
.PHONY: all clean

构建命令:

make        # 编译
make clean  # 清理

详细用法

多源文件项目

myproject/
  CMakeLists.txt
  include/
    utils.h
  src/
    main.c
    utils.c
cmake_minimum_required(VERSION 3.20)
project(MyProject C)

set(CMAKE_C_STANDARD 17)

# 添加可执行文件,包含多个源文件
add_executable(program
    src/main.c
    src/utils.c
)

# 添加头文件搜索路径
target_include_directories(program PRIVATE include)

# 添加编译选项
target_compile_options(program PRIVATE -Wall -Wextra -Wpedantic)

构建型与优化

# 默认构建类型(可在命令行覆盖)
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

# 不同构建类型的编译选项
# Debug: -g -O0(调试信息,无优化)
# Release: -O3 -DNDEBUG(最大优化,去除断言)
# RelWithDebInfo: -O2 -g -DNDEBUG(优化加调试信息)
# MinSizeRel: -Os -DNDEBUG(最小体积优化)

# 为 Debug 模式添加额外选项
target_compile_options(program PRIVATE
    $<$<CONFIG:Debug>:-g3 -O0>
    $<$<CONFIG:Release>:-O3>
)

命令行指定构建型:

cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake --build .

查找和使用外部库

cmake_minimum_required(VERSION 3.20)
project(MathApp C)

set(CMAKE_C_STANDARD 17)

add_executable(mathapp src/main.c)

# 查找数学库
find_library(MATH_LIBRARY m)
if(MATH_LIBRARY)
    target_link_libraries(mathapp PRIVATE ${MATH_LIBRARY})
endif()

# 查找 pthread 库
find_package(Threads REQUIRED)
target_link_libraries(mathapp PRIVATE Threads::Threads)

# 查找自定义库(如 libcurl)
find_package(CURL REQUIRED)
if(CURL_FOUND)
    target_include_directories(mathapp PRIVATE ${CURL_INCLUDE_DIRS})
    target_link_libraries(mathapp PRIVATE ${CURL_LIBRARIES})
endif()

安装规则

# 安装可执行文件到 bin 目录
install(TARGETS program
    RUNTIME DESTINATION bin
)

# 安装头文件到 include 目录
install(FILES include/utils.h
    DESTINATION include
)

# 安装库文件
install(TARGETS mylib
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
)

安装命令:

cmake --install . --prefix /usr/local

常见场景

场景一:构建静态库和动态库

cmake_minimum_required(VERSION 3.20)
project(MyLib C)

set(CMAKE_C_STANDARD 17)

# 库的源文件
set(LIB_SOURCES
    src/string_utils.c
    src/file_utils.c
    src/math_utils.c
)

# 构建静态库
add_library(mylib_static STATIC ${LIB_SOURCES})
target_include_directories(mylib_static PUBLIC include)

# 构建动态库
add_library(mylib_shared SHARED ${LIB_SOURCES})
target_include_directories(mylib_shared PUBLIC include)
set_target_properties(mylib_shared PROPERTIES
    OUTPUT_NAME "mylib"    # 输出文件名
    VERSION 1.2.0          # 版本号
    SOVERSION 1            # ABI 版本
)

# 构建使用该库的测试程序
add_executable(test_program tests/test.c)
target_link_libraries(test_program PRIVATE mylib_static)

场景二:多目录大型项目

project/
  CMakeLists.txt          # 顶层
  app/
    CMakeLists.txt        # 应用子目录
    main.c
  lib/
    CMakeLists.txt        # 库子目录
    core/
      core.c
      core.h
    utils/
      utils.c
      utils.h
  tests/
    CMakeLists.txt        # 测试子目录
    test_core.c

顶层 CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(MyProject C)

set(CMAKE_C_STANDARD 17)

# 添加子目录
add_subdirectory(lib)
add_subdirectory(app)

# 可选:添加测试
option(BUILD_TESTS "构建测试" ON)
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

lib/CMakeLists.txt:

# 构建核心库
add_library(core core/core.c)
target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/core)

# 构建工具库
add_library(utils utils/utils.c)
target_include_directories(utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/utils)
target_link_libraries(utils PUBLIC core)

app/CMakeLists.txt:

add_executable(myapp main.c)
target_link_libraries(myapp PRIVATE core utils)

tests/CMakeLists.txt:

add_executable(test_core test_core.c)
target_link_libraries(test_core PRIVATE core)

# 添加测试
add_test(NAME test_core COMMAND test_core)

场景三:交叉编译

# toolchain-arm.cmake - 交叉编译工具链文件
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# 指定交叉编译器
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

# 搜索路径设置
set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)
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=toolchain-arm.cmake ..
cmake --build .

注意事项

构建目录不要放在源码目录中

CMake 推荐使用”外部构建”(out-of-source build),将构建产物放在单独的目录中,避免污染源码:

# 正确:在单独的目录中构建
mkdir build && cd build
cmake ..

# 错误:在源码目录中直接构建(会生成大量临时文件)
cmake .

CMake 版本兼容性

cmake_minimum_required 指定的版本会影响CMake的行为策略。建议使用较新的版本(3.20+),并注意不同版本之间的策略变化。

# PUBLIC: 依赖传递给使用此目标的其他目标
# PRIVATE: 依赖仅用于此目标,不传递
# INTERFACE: 依赖仅传递给使用此目标的其他目标,不用于此目标本身

target_link_libraries(myapp
    PUBLIC core       # core 的头文件对 myapp 的使用者也可见
    PRIVATE utils     # utils 仅 myapp 内部使用
)

避免使用过时的CMake命令

# 过时写法(不推荐)
include_directories(include)
link_libraries(m)
add_definitions(-DDEBUG)

# 推荐写法(基于目标)
target_include_directories(myapp PRIVATE include)
target_link_libraries(myapp PRIVATE m)
target_compile_definitions(myapp PRIVATE DEBUG)

进阶用法

使用 FetchContent 管理第三方依赖

cmake_minimum_required(VERSION 3.20)
project(MyApp C)

include(FetchContent)

# 自动下载并构建 cJSON 库
FetchContent_Declare(
    cjson
    GIT_REPOSITORY https://github.com/DaveGamble/cJSON.git
    GIT_TAG        v1.7.17
)

FetchContent_MakeAvailable(cjson)

add_executable(myapp src/main.c)
target_link_libraries(myapp PRIVATE cjson)

自定义命令和生成代码

# 在构建前生成版本信息头文件
set(VERSION_FILE ${CMAKE_BINARY_DIR}/generated/version.h)

add_custom_command(
    OUTPUT ${VERSION_FILE}
    COMMAND ${CMAKE_COMMAND}
        -DVERSION_FILE=${VERSION_FILE}
        -DPROJECT_VERSION=${PROJECT_VERSION}
        -P ${CMAKE_SOURCE_DIR}/cmake/generate_version.cmake
    DEPENDS ${CMAKE_SOURCE_DIR}/cmake/generate_version.cmake
    COMMENT "生成版本信息头文件"
)

# 将生成的文件加入构建
add_executable(myapp
    src/main.c
    ${VERSION_FILE}
)

target_include_directories(myapp PRIVATE ${CMAKE_BINARY_DIR}/generated)

使用 CMake Presets

CMake Presets(预设)允许在 CMakePresets.json 中定义常用的配置,简化构建命令:

{
  "version": 3,
  "configurePresets": [
    {
      "name": "debug",
      "binaryDir": "${sourceDir}/build/debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_C_FLAGS": "-g3 -O0 -Wall"
      }
    },
    {
      "name": "release",
      "binaryDir": "${sourceDir}/build/release",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_C_FLAGS": "-O3 -Wall"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "debug",
      "configurePreset": "debug"
    },
    {
      "name": "release",
      "configurePreset": "release"
    }
  ]
}

使用预设构建:

cmake --preset debug
cmake --build --preset debug

使用 Ninja 加速构建

Ninja 是一个专注于构建速度的工具,比 Make 更快:

# 安装 Ninja
# Ubuntu: sudo apt install ninja-build
# macOS: brew install ninja

# 使用 Ninja 生成器
cmake -G Ninja ..
cmake --build .

# 或在预设中指定
# "generator": "Ninja"