CMake 从入门到精通

本教程将通过一个完整的"计算器项目"带你系统学习CMake,从简单的Hello World到构建复杂的项目结构。


第一部分:快速入门

什么是CMake

CMake是一个跨平台的、开源的构建系统生成器(Build System Generator)。它不是直接编译你的代码,而是生成适合不同平台的构建文件:

  • 在Unix/Linux系统上生成Makefile
  • 在Windows上可以生成Visual Studio项目文件
  • 在macOS上可以生成Xcode项目文件

为什么需要CMake?

想象一下,你在Mac上开发了一个C++项目,直接用g++命令编译。当你的同事在Linux或Windows上想要构建这个项目时,他们需要:

  • 手动调整编译命令
  • 处理不同的路径分隔符
  • 适配不同的编译器选项

CMake解决了这个问题:你只需要编写一次CMakeLists.txt配置文件,CMake就能在任何平台上生成对应的构建文件

CMake的工作流程:编写CMakeLists.txt cmake配置并生成构建文件 make/构建工具编译 生成可执行文件

安装CMake

Windows平台

  1. 访问 CMake官网 根据系统位数下载对应版本
  2. 运行安装程序,建议选择"为所有用户添加CMake到系统PATH"
  3. 安装完成后,打开命令提示符(cmd),输入以下命令验证: cmake --version 

如果显示版本号,说明安装成功。

Linux平台

本文以Ubuntu 18.04为例,其他Linux发行版(如CentOS、Fedora)需要使用对应的包管理器。

方法一:使用包管理器安装(推荐新手)

这是最简单的安装方式: sudo apt install cmake 

安装完成后验证: cmake --version 

方法二:使用官方预编译包(最快捷)

1. 访问 CMake官网下载页面

2. 在Binary distributions区域选择对应平台的包(如Linux x86_64)

3. 下载后解压并添加到PATH:

# 解压
tar xf cmake-3.17.0-Linux-x86_64.tar.gz
sudo mv cmake-3.17.0-Linux-x86_64 /opt/cmake
# 添加到环境变量
echo 'export PATH=/opt/cmake/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
# 验证安装
cmake --version

第一个CMake项目:Hello World

现在让我们创建第一个CMake项目。这个项目很简单,只有一个C源文件和一个CMake配置文件。

步骤1:创建项目目录和源文件

创建一个项目目录,并在其中创建main.c文件:

mkdir hello_cmake
cd hello_cmake

创建main.c文件,内容如下:

#include <stdio.h>
int main(void) {
  printf("Hello, CMake!\n");
  return 0;
}

步骤2:编写CMakeLists.txt

在同一目录下创建CMakeLists.txt文件:

# 指定CMake最低版本要求
cmake_minimum_required(VERSION 3.10)
# 定义项目名称
project(hello_cmake)
# 设置C语言标准
set(CMAKE_C_STANDARD 11)
# 生成可执行文件
add_executable(hello_cmake main.c)

让我们理解每一行的含义:

  • cmake_minimum_required :指定项目所需的CMake最低版本
  • project:定义项目名称
  • set(CMAKE_C_STANDARD11):设置C语言标准为C11
  • add_executable:指定要生成的可执行文件名称和源文件

步骤3:构建项目(Out-of-source方式)

CMake支持两种构建方式:

  • In-source构建:在源代码目录下直接构建,会产生大量中间文件(不推荐)
  • Out-of-source构建:在单独的构建目录下构建,保持源码目录干净(强烈推荐)

我们使用Out-of-source构建方式:

# 创建构建目录并进入
mkdir build
cd build
# 运行cmake配置项目,生成构建文件
cmake ..
# 编译项目
make
# 或者使用跨平台的方式:cmake --build .
# 运行生成的可执行文件
./hello_cmake

你应该会看到输出: Hello, CMake! 

恭喜! 你已经成功创建并构建了第一个CMake项目。

理解构建过程:

  1.  cmake.. :CMake读取上级目录的CMakeLists.txt,生成构建文件(如Makefile)
  2.  make :使用生成的Makefile编译源代码,生成可执行文件
  3.  ./hello_cmake :运行生成的可执行文件

清理构建缓存

如果需要重新配置项目(比如修改了CMakeLists.txt),可以清理CMake缓存:

# 方法1:删除整个构建目录(最彻底)
cd ..
rm -rf build
mkdir build
cd build
cmake ..
# 方法2:只删除缓存文件
cd build
rm -rf CMakeCache.txt CMakeFiles/
cmake ..

练习建议:

  1. 修改main.c,让它打印更多信息,然后重新构建
  2. 尝试修改项目名称,观察生成的可执行文件名称变化
  3. 尝试创建In-source构建,观察源代码目录的变化,体会为什么推荐Out-of-source构建

第二部分:项目结构进阶

学习目标:

  • 掌握在项目中添加多个源文件的方法
  • 学会组织项目目录结构(分离头文件和源文件)
  • 通过"计算器项目"实践项目演进过程
  • 理解如何配置输出路径

在上一部分,我们创建了一个只有单个源文件的项目。实际项目中,代码会分散在多个文件中,我们需要学会如何管理这些文件。

添加更多源文件

假设我们现在要开发一个简单的计算器程序,包含加法和减法功能。我们将代码拆分为多个文件:

项目结构:

calculator/
├── CMakeLists.txt
├── main.c
├── add.c
└── subtract.c

add.c(加法函数):

int add(int a, int b) {
    return a + b;
}

subtract.c(减法函数):

int subtract(int a, int b) {
    return a - b;
}

main.c(主程序):

#include <stdio.h>
    // 函数声明
    int add(int, int);
    int subtract(int, int);
    int main(void) {
    printf("10 + 5 = %d\n", add(10, 5));
    printf("10 - 5 = %d\n", subtract(10, 5));
    return 0;
}

现在我们有三个源文件,如何配置CMakeLists.txt呢?

方法1:逐个添加源文件

cmake_minimum_required(VERSION 3.10)
project(calculator)
add_executable(calculator
    main.c
    add.c
    subtract.c
) 

这种方式最清晰,适合源文件不多的项目。

方法2:使用auxsourcedirectory

cmake_minimum_required(VERSION 3.10)
project(calculator)
# 将当前目录下的所有源文件收集到变量src_list
aux_source_directory(. src_list)
add_executable(calculator ${src_list})

这种方式会自动收集指定目录下的所有源文件。但有个问题:它会把所有源文件都加进来,可能包含你不想编译的测试文件。

方法3:使用set命令指定源文件(推荐)

cmake_minimum_required(VERSION 3.10)
project(calculator)
# 明确指定源文件列表
set(SRC_LIST
    main.c
    add.c
    subtract.c
)
add_executable(calculator ${SRC_LIST})

这种方式最灵活,你可以精确控制哪些文件被编译。

最佳实践:推荐使用set命令明确指定源文件列表,避免误加入不需要的文件。

三种方法对比:

方法优点缺点适用场景
逐个添加 简单直观 文件多时代码冗长 文件少(≤5个)
auxsourcedirectory 自动收集 可能包含不需要的文件 快速原型开发
set命令 精确控制 需要手动维护列表 生产项目(推荐)

组织项目目录

随着项目变大,我们需要更好的目录组织方式。一个常见的做法是将头文件和源文件分开存放。

为什么要分离头文件和源文件?

  • 清晰的代码组织:接口(头文件)和实现(源文件)分离
  • 便于维护:容易找到对应的文件
  • 支持库的创建:头文件可以作为库的公共接口

让我们重新组织计算器项目:

新的项目结构:

calculator/
├── CMakeLists.txt
├── include/
│ └── calc.h
├── src/
│ ├── main.c
│ ├── add.c
│ └── subtract.c
└── build/

include/calc.h(头文件):

#ifndef CALC_H
#define CALC_H
int add(int a, int b);
int subtract(int a, int b);
#endif

src/main.c(主程序):

#include <stdio.h>
#include "calc.h"
int main(void) {
  printf("10 + 5 = %d\n", add(10, 5));
  printf("10 - 5 = %d\n", subtract(10, 5));
  return 0;
}

src/add.c 和 src/subtract.c

#include "calc.h"
  int add(int a, int b) {
  return a + b;
}

配置CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(calculator)
# 添加头文件搜索路径
include_directories(include)
# 指定源文件
set(SRC_LIST
  src/main.c
  src/add.c
  src/subtract.c
)
add_executable(calculator ${SRC_LIST})

关键点:

  • include_directories(include):告诉编译器在include目录下查找头文件
  • 源文件路径现在需要包含src/前缀

使用add_subdirectory管理子目录

对于更复杂的项目,我们可以在子目录中放置独立的CMakeLists.txt:

项目结构:

calculator/
├── CMakeLists.txt # 主CMakeLists.txt
├── include/
│ └── calc.h
├── src/
│ ├── CMakeLists.txt # src目录的CMakeLists.txt
│ ├── main.c
│ ├── add.c
│ └── subtract.c
└── build/

主CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(calculator)
# 添加头文件搜索路径
include_directories(include)
# 添加src子目录
add_subdirectory(src)

src/CMakeLists.txt:

# 指定源文件
set(SRC_LIST
  main.c
  add.c
  subtract.c
)
add_executable(calculator ${SRC_LIST})

这种方式让每个子目录负责管理自己的源文件,适合大型项目。

实战案例:计算器项目演进

现在让我们通过一个完整的案例,看看如何一步步构建一个结构良好的项目。

阶段1:最简单的版本

从一个单文件开始:

// main.c
#include <stdio.h>
  int main(void) {
  int a = 10, b = 5;
  printf("%d + %d = %d\n", a, b, a + b);
  printf("%d - %d = %d\n", a, b, a - b);
  return 0;
}

阶段2:拆分功能模块

将加法和减法提取为独立函数:

calculator/
├── CMakeLists.txt
├── main.c
├── add.c
└── subtract.c

阶段3:添加头文件

创建清晰的接口:

calculator/
├── CMakeLists.txt
├── include/
│ └── calc.h
└── src/
├── main.c
├── add.c
└── subtract.c

阶段4:配置输出路径

让我们进一步改进,将生成的可执行文件输出到bin目录:

完整的项目结构:

calculator/
├── CMakeLists.txt
├── include/
│ └── calc.h
├── src/
│ ├── main.c
│ ├── add.c
│ └── subtract.c
├── build/ # 构建目录
└── bin/ # 输出目录

CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(calculator)
# 设置可执行文件输出路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
# 添加头文件搜索路径
include_directories(include)
# 指定源文件
set(SRC_LIST
  src/main.c
  src/add.c
  src/subtract.c
)
add_executable(calculator ${SRC_LIST})

关键变量说明:

  • PROJECT_SOURCE_DIR:项目根目录的绝对路径
  • EXECUTABLE_OUTPUT_PATH:可执行文件的输出路径

构建项目:

mkdir build
cd build
cmake ..
make

现在可执行文件会生成在bin目录下: ../bin/calculator 

练习建议:

  1. 为计算器添加乘法和除法功能
  2. 尝试将每个功能模块放在单独的子目录中
  3. 尝试配置不同的输出路径(如debug和release目录)

第三部分:库的使用

学习目标:

  • 理解为什么需要库以及库的作用
  • 学会创建静态库和动态库
  • 掌握如何链接和使用库
  • 了解静态库和动态库的区别

在前面的章节中,我们的所有代码都编译成一个可执行文件。但在实际开发中,我们经常需要将通用的功能封装成,供多个项目复用。

为什么需要库

什么是库? 库是预先编译好的代码集合,包含可重用的函数和数据结构。

使用库的好处:

  1. 代码复用:一次编写,多处使用
  2. 模块化管理:将功能分解为独立模块,便于维护
  3. 隐藏实现细节:只暴露接口(头文件),保护源代码
  4. 加快编译速度:库一旦编译好,使用时无需重新编译

静态库 vs 动态库

特性静态库(.a / .lib)动态库(.so / .dll)
链接方式 编译时链接到可执行文件 运行时动态加载
可执行文件大小 较大(包含库代码) 较小(只包含引用)
运行时依赖 无需外部库文件 需要库文件存在
内存占用 每个程序有独立副本 多个程序共享一份
更新库 需要重新编译程序 只需替换库文件
适用场景 独立部署、不希望暴露实现 多程序共享、独立更新

创建和使用静态库

让我们将计算器的功能封装为一个静态库。

项目结构:

calculator_lib/
├── CMakeLists.txt
├── calc/
│ ├── include/
│ │ └── calc.h
│ └── src/
│ ├── add.c
│ └── subtract.c
└── lib/ # 库文件输出目录

calc/include/calc.h:

#ifndef CALC_H
#define CALC_H
int add(int a, int b);
int subtract(int a, int b);
#endif

calc/src/add.c 和 subtract.c:

#include "calc.h"
int add(int a, int b) {
  return a + b;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(calculator_lib)
# 指定源文件
set(SRC_LIST
calc/src/add.c
calc/src/subtract.c
)
# 添加头文件搜索路径
include_directories(calc/include)
# 生成静态库
add_library(calc_static STATIC ${SRC_LIST})
# 设置库的输出名称(去掉_static后缀)
set_target_properties(calc_static PROPERTIES OUTPUT_NAME "calc")
# 设置库文件输出路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

关键指令说明:

  • add_library(库名类型源文件):创建库
  • 第1个参数:库的目标名称(CMake内部使用)
  • 第2个参数:STATIC(静态库)或SHARED(动态库)
  • 第3个参数:源文件列表
  • set_target_properties:设置目标属性
  • OUTPUT_NAME:设置最终生成的库文件名称
  • LIBRARY_OUTPUT_PATH:库文件的输出路径

构建库:

mkdir build
cd build
cmake ..
make

生成的静态库位于lib目录下:

lib/libcalc.a # Linux/macOS
lib/calc.lib # Windows

注意:Linux/macOS下库文件会自动添加lib前缀和.a后缀。

创建和使用动态库

动态库的创建过程与静态库类似,只需将STATIC改为SHARED

CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(calculator_lib)
set(SRC_LIST
  calc/src/add.c
  calc/src/subtract.c
)
include_directories(calc/include)
# 生成静态库
add_library(calc_static STATIC ${SRC_LIST})
set_target_properties(calc_static PROPERTIES OUTPUT_NAME "calc")
# 生成动态库
add_library(calc_shared SHARED ${SRC_LIST})
set_target_properties(calc_shared PROPERTIES OUTPUT_NAME "calc")
# 设置库文件输出路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

这样会同时生成静态库和动态库:

lib/libcalc.a # 静态库
lib/libcalc.so # 动态库(Linux)
lib/libcalc.dylib # 动态库(macOS)
lib/calc.dll # 动态库(Windows)

选择建议

  • 内部使用的工具库 → 推荐静态库
  • 需要被多个程序共享或独立更新的库 → 推荐动态库

链接库文件

现在我们有了库,如何在其他项目中使用它呢?

新建一个使用库的项目:

项目结构:

calculator_app/
├── CMakeLists.txt
├── src/
│ └── main.c
├── testFunc/
│ ├── inc/
│ │ └── calc.h # 从库项目复制
│ └── lib/
│ └── libcalc.a # 从库项目复制
└── bin/ # 可执行文件输出目录

src/main.c:

#include <stdio.h>
#include "calc.h"
int main(void) {
printf("10 + 5 = %d\n", add(10, 5));
printf("10 - 5 = %d\n", subtract(10, 5));
return 0;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(calculator_app)
# 设置可执行文件输出路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
# 添加头文件搜索路径
include_directories(${PROJECT_SOURCE_DIR}/testFunc/inc)
# 指定源文件
set(SRC_LIST src/main.c)
# 在指定路径下查找库,并将库的绝对路径存放到变量中
find_library(CALC_LIB calc HINTS ${PROJECT_SOURCE_DIR}/testFunc/lib)
# 创建可执行文件
add_executable(calculator ${SRC_LIST})
# 链接库文件
target_link_libraries(calculator ${CALC_LIB})

关键指令说明:

  • find_library(变量名库名HINTS路径):查找库文件
  • 会在指定路径下查找libcalc.alibcalc.so
  • 找到后将完整路径保存到变量中
  • 默认优先查找动态库,如果只有静态库则使用静态库
  • target_link_libraries(目标库):将库链接到目标

构建和运行:

  1. mkdir build
  2. cd build
  3. cmake ..
  4. make
  5. ../bin/calculator

指定使用静态库或动态库:

如果同时存在静态库和动态库,你可以明确指定使用哪个:

  1. # 明确指定使用静态库
  2. find_library(CALC_LIB libcalc.a HINTS ${PROJECT_SOURCE_DIR}/testFunc/lib)
  3. # 或明确指定使用动态库
  4. find_library(CALC_LIB libcalc.so HINTS ${PROJECT_SOURCE_DIR}/testFunc/lib)

验证使用了哪个库:

在Linux下,可以使用readelf命令查看可执行文件的依赖:

  1. readelf -d bin/calculator

如果链接的是动态库,会显示依赖libcalc.so;如果是静态库,不会显示这个依赖。

动态库的运行时加载:

如果使用动态库,运行时需要能找到库文件。有几种方法:

  1. 将库复制到系统库目录(需要root权限):
  1. sudo cp lib/libcalc.so /usr/local/lib/
  2. sudo ldconfig
  1. 设置LDLIBRARYPATH环境变量(临时):
  1. export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH
  2. ./calculator
  1. 在CMakeLists.txt中设置RPATH(推荐):
  1. set(CMAKE_INSTALL_RPATH "${PROJECT_SOURCE_DIR}/testFunc/lib")
  2. set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)

查看系统中是否已安装某个库:

  1. ldconfig -p | grep calc

ldconfig会在默认搜索目录(/lib/usr/lib)和配置文件(/etc/ld.so.conf)中搜索动态库。

练习建议:

  1. 将计算器的每个功能(加、减、乘、除)封装为独立的库
  2. 创建一个项目,同时链接多个库
  3. 尝试同时编译静态库和动态库,并分别测试

第四部分:编译配置

学习目标:

  • 掌握常用的编译选项配置方法
  • 学会使用条件编译控制代码
  • 理解CMake变量的使用
  • 学会配置Debug和Release模式

有时我们需要为编译器添加特定的选项,或者根据不同条件编译不同的代码。CMake提供了灵活的配置方式。

编译选项

指定编译器:

  1. # 指定C编译器
  2. set(CMAKE_C_COMPILER gcc)
  3. # 指定C++编译器
  4. set(CMAKE_CXX_COMPILER g++)

设置编译选项的三种方法:

方法1:addcompileoptions

  1. add_compile_options(-Wall -Werror -Wstrict-prototypes)

这会为所有编译器(C和C++)添加选项。

方法2:add_definitions

  1. add_definitions("-Wall -Werror -Wstrict-prototypes")

同样为所有编译器添加选项。注意需要用引号包裹所有选项。

方法3:set命令修改CMAKECFLAGS或CMAKECXXFLAGS

  1. # 只针对C编译器
  2. set(CMAKE_C_FLAGS "-Wall -Werror -Wstrict-prototypes")
  3. # 只针对C++编译器
  4. set(CMAKE_CXX_FLAGS "-Wall -Werror")

这种方式可以分别为C和C++编译器设置不同的选项。

三种方法对比:

方法作用范围特点
addcompileoptions C和C++编译器 接受空格分隔的选项列表
add_definitions C和C++编译器 需要用引号包裹,主要用于添加宏定义
set(CMAKEC/CXXFLAGS) 分别针对C或C++编译器 更精确的控制

常用编译选项:

# Debug模式:包含调试信息,不优化
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_C_FLAGS_DEBUG "-g -O0")
# Release模式:优化代码,不包含调试信息
set(CMAKE_BUILD_TYPE Release)
set(CMAKE_C_FLAGS_RELEASE "-O3")
# 添加宏定义
add_definitions(-DDEBUG -DVERSION="1.0")
# 常用警告选项
add_compile_options(
-Wall # 启用大部分警告
-Wextra # 启用额外警告
-Werror # 将警告视为错误
-pedantic # 严格遵循标准
)

实际示例:

cmake_minimum_required(VERSION 3.10)
project(calculator)
# 设置为Debug模式
set(CMAKE_BUILD_TYPE Debug)
# 添加编译选项
add_compile_options(-Wall -Wextra -g)
# 添加宏定义
add_definitions(-DDEBUG_MODE)
include_directories(include)
set(SRC_LIST src/main.c src/add.c)
add_executable(calculator ${SRC_LIST})

在代码中可以使用这个宏:

#include <stdio.h>
int main(void) {
#ifdef DEBUG_MODE
printf("[DEBUG] Program started\n");
#endif
printf("Hello, World!\n");
return 0;
}

条件编译

CMake的option命令允许我们定义可选的编译选项,从而实现条件编译。

场景1:选择性编译目标

假设我们有两个程序:main1和main2,我们想通过选项控制是否编译main2。

项目结构:

  1. conditional/
  2. ├── CMakeLists.txt
  3. └── src/
  4. ├── CMakeLists.txt
  5. ├── main1.c
  6. └── main2.c

主CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(conditional_build)
# 定义选项(默认为OFF)
option(BUILD_MAIN2 "Build main2 executable" OFF)
# 设置输出路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
# 添加src子目录
add_subdirectory(src)

src/CMakeLists.txt:

# 始终编译main1
add_executable(main1 main1.c)
# 根据选项决定是否编译main2
if(BUILD_MAIN2)
add_executable(main2 main2.c)
message(STATUS "Building main2")
else()
message(STATUS "Skipping main2 (use -DBUILD_MAIN2=ON to enable)")
endif()

构建:

# 默认只编译main1
cmake ..
make
# 编译main1和main2
cmake .. -DBUILD_MAIN2=ON
make

option指令说明:

  • 第1个参数:选项名称
  • 第2个参数:描述信息
  • 第3个参数:默认值(ON或OFF)

场景2:使用宏控制代码

假设我们想在代码中通过宏来控制打印的内容。

main.c:

#include <stdio.h>
int main(void) {
  #ifdef FEATURE_A
  printf("Feature A is enabled\n");
  #endif
  #ifdef FEATURE_B
  printf("Feature B is enabled\n");
  #endif
  printf("Program running\n");
  return 0;
}

 

CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(feature_flags)
# 定义选项
option(ENABLE_FEATURE_A "Enable feature A" OFF)
option(ENABLE_FEATURE_B "Enable feature B" OFF)
# 根据选项添加宏定义
if(ENABLE_FEATURE_A)
add_compile_options(-DFEATURE_A)
endif()
if(ENABLE_FEATURE_B)
add_compile_options(-DFEATURE_B)
endif()
add_executable(app main.c)

构建不同配置:

# 默认配置(两个功能都关闭)
cmake .. && make
./app
# 输出: Program running
# 启用Feature A
rm -rf *
cmake .. -DENABLE_FEATURE_A=ON && make
./app
# 输出:
# Feature A is enabled
# Program running
# 同时启用两个功能
rm -rf *
cmake .. -DENABLE_FEATURE_A=ON -DENABLE_FEATURE_B=ON && make
./app
# 输出:
# Feature A is enabled
# Feature B is enabled
# Program running

重要提示:缓存问题

CMake会缓存选项值。如果你修改了选项,但没有看到效果,需要清理缓存:

# 方法1:删除缓存文件
rm CMakeCache.txt
# 方法2:重新指定所有选项
cmake .. -DENABLE_FEATURE_A=OFF -DENABLE_FEATURE_B=ON

变量和宏

CMake预定义变量:

CMake提供了许多预定义变量,常用的包括:

变量名含义
PROJECT_SOURCE_DIR 项目根目录
PROJECT_BINARY_DIR 构建目录
CMAKE_CURRENT_SOURCE_DIR 当前CMakeLists.txt所在目录
CMAKE_CURRENT_BINARY_DIR 当前CMakeLists.txt的构建目录
EXECUTABLE_OUTPUT_PATH 可执行文件输出路径
LIBRARY_OUTPUT_PATH 库文件输出路径
CMAKE_C_COMPILER C编译器路径
CMAKE_CXX_COMPILER C++编译器路径
CMAKE_BUILD_TYPE 构建类型(Debug/Release)
CMAKE_C_FLAGS C编译选项
CMAKE_CXX_FLAGS C++编译选项

自定义变量:

使用set命令定义变量:

# 定义简单变量
set(MY_NAME "Calculator")
set(MY_VERSION 1.0)
# 定义列表变量
set(SRC_LIST main.c add.c subtract.c)
# 使用变量
message(STATUS "Project: ${MY_NAME} v${MY_VERSION}")
add_executable(${MY_NAME} ${SRC_LIST})

变量命名规范:

  • 预定义变量:通常全大写,如CMAKE_CXX_FLAGS
  • 自定义变量:建议全大写或大写+下划线,如SRC_LISTMY_INCLUDE_DIRS
  • 避免使用CMake保留的变量名

访问环境变量:

# 读取环境变量
set(MY_PATH $ENV{PATH})
message(STATUS "System PATH: ${MY_PATH}")
# 设置环境变量(只在CMake执行期间有效)
set(ENV{MY_VAR} "some_value")

练习建议:

  1. 创建Debug和Release两个构建配置,观察生成的可执行文件大小差异
  2. 使用option创建一个"日志级别"选项,控制程序输出详细程度
  3. 尝试使用变量来管理项目版本号

第五部分:CMake实用技能

学习目标:

  • 掌握CMake常用指令
  • 学会使用控制流指令
  • 掌握CMake调试技巧
  • 了解CMake最佳实践

这一部分将介绍CMake的常用指令和实用技巧,帮助你更高效地使用CMake。

常用指令速查

基础指令:

指令语法说明
cmake_minimum_required cmake_minimum_required(VERSION3.10) 指定CMake最低版本
project project(项目名) 定义项目名称
set set(变量名值) 定义变量
message message(STATUS"信息") 输出信息

目录和文件管理:

指令语法说明
include_directories include_directories(路径1路径2...) 添加头文件搜索路径
add_subdirectory add_subdirectory(子目录名) 添加子目录
aux_source_directory aux_source_directory(目录变量名) 收集目录下所有源文件

注意include_directories是全局的,推荐使用target_include_directories为特定目标添加头文件路径:

target_include_directories(目标名 PUBLIC include)

 

目标管理:

指令语法说明
add_executable add_executable(名称源文件...) 生成可执行文件
add_library add_library(名称STATIC/SHARED源文件...) 生成库文件
target_link_libraries target_link_libraries(目标库...) 链接库到目标
set_target_properties set_target_properties(目标PROPERTIES属性值) 设置目标属性
add_dependencies add_dependencies(目标依赖...) 指定目标依赖关系

依赖查找:

指令语法说明
find_library find_library(变量名库名HINTS路径) 查找库文件
find_package find_package(包名REQUIRED) 查找并加载外部包

find_package示例:

# 查找OpenCV包
find_package(OpenCV REQUIRED)
# 使用OpenCV
include_directories(${OpenCV_INCLUDE_DIRS})
target_link_libraries(my_app ${OpenCV_LIBS})

 

文件操作:

指令说明
FILE(WRITE文件名"内容") 写入文件
FILE(APPEND文件名"内容") 追加到文件
FILE(READ文件名变量) 读取文件
FILE(GLOB变量表达式) 收集文件
FILE(MAKE_DIRECTORY目录) 创建目录
FILE(REMOVE文件) 删除文件

FILE(GLOB)示例:

# 收集所有.c文件
FILE(GLOB SRC_LIST "src/*.c")
# 递归收集
FILE(GLOB_RECURSE SRC_LIST "src/*.c")

定义和宏:

指令语法说明
add_definitions add_definitions(-DDEBUG) 添加宏定义
option option(选项名"描述"ON/OFF) 定义可选项

控制流指令

IF条件判断:

if(条件)
# 命令
elseif(条件)
# 命令
else()
# 命令
endif()

常用条件:

# 变量是否为真(非空、非0、非FALSE、非OFF)
if(MY_VAR)
message("MY_VAR is true")
endif()
# 变量是否为假
if(NOT MY_VAR)
message("MY_VAR is false")
endif()
# 逻辑运算
if(VAR1 AND VAR2)
message("Both are true")
endif()
if(VAR1 OR VAR2)
message("At least one is true")
endif()
# 字符串比较
if(MY_VAR STREQUAL "Hello")
message("MY_VAR equals Hello")
endif()
# 数值比较
if(MY_NUM LESS 10)
message("MY_NUM < 10")
endif()
if(MY_NUM GREATER 10)
message("MY_NUM > 10")
endif()
if(MY_NUM EQUAL 10)
message("MY_NUM == 10")
endif()
# 文件/目录检查
if(EXISTS ${FILE_PATH})
message("File exists")
endif()
if(IS_DIRECTORY ${DIR_PATH})
message("Is a directory")
endif()
# 命令检查
if(COMMAND add_executable)
message("add_executable command is available")
endif()
# 正则匹配
if(MY_VAR MATCHES "^Hello.*")
message("MY_VAR starts with Hello")
endif()

FOREACH循环:

# 遍历列表
set(ITEMS apple banana cherry)
foreach(item ${ITEMS})
message(STATUS "Item: ${item}")
endforeach()
# 遍历范围
foreach(i RANGE 5)
message(STATUS "Number: ${i}") # 0, 1, 2, 3, 4, 5
endforeach()
# 指定起始和结束
foreach(i RANGE 1 5)
message(STATUS "Number: ${i}") # 1, 2, 3, 4, 5
endforeach()
# 指定步长
foreach(i RANGE 0 10 2)
message(STATUS "Number: ${i}") # 0, 2, 4, 6, 8, 10
endforeach()

WHILE循环:

set(COUNT 0)
while(COUNT LESS 5)
message(STATUS "Count: ${COUNT}")
math(EXPR COUNT "${COUNT} + 1")
endwhile()

实际应用示例:

# 根据编译器类型设置不同选项
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
message(STATUS "Using GCC")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")
elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
message(STATUS "Using Clang")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Weverything")
elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
message(STATUS "Using MSVC")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W4")
endif()
# 批量添加源文件
set(MODULES add subtract multiply divide)
foreach(module ${MODULES})
set(SRC_LIST ${SRC_LIST} src/${module}.c)
endforeach()

调试技巧

1. 使用message()输出调试信息

# 不同级别的消息
message(STATUS "This is a status message") # 普通信息
message(WARNING "This is a warning") # 警告
message(FATAL_ERROR "This stops configuration") # 致命错误,停止配置
# 输出变量值
message(STATUS "Source files: ${SRC_LIST}")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
# 输出多个变量
message(STATUS "Compiler: ${CMAKE_C_COMPILER}")
message(STATUS "Compiler ID: ${CMAKE_C_COMPILER_ID}")
message(STATUS "Compiler version: ${CMAKE_C_COMPILER_VERSION}")

2. 启用详细编译输出

# 方法1:命令行参数
make VERBOSE=1
# 方法2:在CMakeLists.txt中设置
set(CMAKE_VERBOSE_MAKEFILE ON)
# 方法3:使用--verbose
cmake --build . --verbose

3. 查看所有CMake变量

# 列出所有变量
get_cmake_property(_variableNames VARIABLES)
foreach(_variableName ${_variableNames})
message(STATUS "${_variableName}=${${_variableName}}")
endforeach()

4. 常见问题排查

问题1:找不到头文件

# 检查include路径
get_target_property(INCLUDES my_target INCLUDE_DIRECTORIES)
message(STATUS "Include directories: ${INCLUDES}")
# 或者输出CMAKE_INCLUDE_PATH
message(STATUS "CMAKE_INCLUDE_PATH: ${CMAKE_INCLUDE_PATH}")

问题2:链接错误

# 检查链接的库
get_target_property(LIBS my_target LINK_LIBRARIES)
message(STATUS "Linked libraries: ${LIBS}")
# 检查库搜索路径
message(STATUS "Library path: ${CMAKE_LIBRARY_PATH}")

问题3:缓存问题导致配置不生效

# 删除缓存重新配置
rm -rf CMakeCache.txt CMakeFiles/
cmake ..
# 或者删除整个build目录
cd ..
rm -rf build
mkdir build
cd build
cmake ..

问题4:检查生成的文件

# 查看生成的Makefile(部分内容)
cat Makefile
# 查看详细的构建规则
cat CMakeFiles/my_target.dir/build.make

5. 使用--trace调试CMake脚本

# 跟踪所有CMake命令的执行
cmake --trace ..
# 只跟踪特定文件
cmake --trace-source=CMakeLists.txt ..

最佳实践

1. 始终使用Out-of-source构建

# 推荐
mkdir build && cd build && cmake ..
# 不推荐:在源代码目录下直接构建
cmake .

2. 使用target_*系列命令

# 推荐:针对特定目标设置
target_include_directories(my_app PUBLIC include)
target_link_libraries(my_app my_lib)
target_compile_options(my_app PRIVATE -Wall)
# 不推荐:全局设置,影响所有目标
include_directories(include)
link_libraries(my_lib)
add_compile_options(-Wall)

target_*系列命令的好处:

  • 精确控制每个目标的配置
  • 避免不同目标之间的配置冲突
  • 更清晰的依赖关系

3. 明确指定源文件

# 推荐:明确列出源文件
set(SRC_LIST
main.c
add.c
subtract.c
)
# 不推荐:使用GLOB(可能包含不需要的文件)
file(GLOB SRC_LIST "*.c")

4. 合理组织目录结构

project/
├── CMakeLists.txt # 主CMakeLists.txt
├── include/ # 公共头文件
├── src/ # 源文件
│ ├── CMakeLists.txt
│ └── ...
├── lib/ # 库文件(如果有)
├── tests/ # 测试代码
│ └── CMakeLists.txt
├── docs/ # 文档
├── build/ # 构建目录(git忽略)
└── bin/ # 输出目录(git忽略)

5. 使用版本控制

# .gitignore文件
build/
bin/
lib/
*.exe
*.a
*.so
*.dll
CMakeCache.txt
CMakeFiles/

6. 设置合理的项目属性

cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0.0 LANGUAGES C CXX)
# 设置C/C++标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 启用导出编译命令(用于IDE和工具)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# 设置输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)

7. 添加安装规则

# 安装可执行文件
install(TARGETS my_app DESTINATION bin)
# 安装库文件
install(TARGETS my_lib
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
# 安装头文件
install(FILES include/my_lib.h DESTINATION include)
# 使用
make install
#
cmake --install .

练习建议:

  1. 创建一个包含多个子目录的项目,使用target_*命令配置每个目标
  2. 使用message()输出构建信息,观察变量的值
  3. 编写一个循环,批量处理源文件或模块

第六部分:完整案例

学习目标:

  • 综合运用前面学到的所有知识
  • 构建一个结构清晰的完整项目
  • 理解实际项目的组织方式

一个完整的项目示例

让我们构建一个完整的计算器项目,包含:

  • 核心计算库(静态库)
  • 命令行应用程序
  • 条件编译功能
  • Debug和Release配置

项目需求:

  1. 将计算功能(加、减、乘、除)封装为库
  2. 提供命令行程序使用该库
  3. 支持Debug模式输出详细信息
  4. 使用条件编译控制高级功能

目录结构:

calculator_project/
├── CMakeLists.txt # 主配置文件
├── calc_lib/ # 计算库
│ ├── CMakeLists.txt
│ ├── include/
│ │ └── calculator.h
│ └── src/
│ ├── add.c
│ ├── subtract.c
│ ├── multiply.c
│ └── divide.c
├── app/ # 应用程序
│ ├── CMakeLists.txt
│ └── main.c
├── build/ # 构建目录
├── bin/ # 输出目录
└── lib/ # 库输出目录

calc_lib/include/calculator.h:

#ifndef CALCULATOR_H
#define CALCULATOR_H
/**
* 基本算术运算库
*/
int add(int a, int b);
int subtract(int a, int b);
#ifdef ADVANCED_FEATURES
int multiply(int a, int b);
int divide(int a, int b);
#endif
#endif // CALCULATOR_H

calc_lib/src/add.c:

#include "calculator.h"
#include <stdio.h>
int add(int a, int b) {
#ifdef DEBUG_MODE
printf("[DEBUG] add(%d, %d)\n", a, b);
#endif
return a + b;
}

calc_lib/src/subtract.c:

#include "calculator.h"
#include <stdio.h>
int subtract(int a, int b) {
#ifdef DEBUG_MODE
printf("[DEBUG] subtract(%d, %d)\n", a, b);
#endif
return a - b;
}

calc_lib/src/multiply.c:

#include "calculator.h"
#include <stdio.h>
int multiply(int a, int b) {
#ifdef DEBUG_MODE
printf("[DEBUG] multiply(%d, %d)\n", a, b);
#endif
return a * b;
}

calc_lib/src/divide.c:

#include "calculator.h"
#include <stdio.h>
int divide(int a, int b) {
#ifdef DEBUG_MODE
printf("[DEBUG] divide(%d, %d)\n", a, b);
#endif
if (b == 0) {
printf("Error: Division by zero!\n");
return 0;
}
return a / b;
}

calc_lib/CMakeLists.txt:

# 计算库的CMakeLists.txt
# 收集源文件
set(LIB_SRC_LIST
src/add.c
src/subtract.c
)
# 如果启用高级功能,添加更多源文件
if(ADVANCED_FEATURES)
list(APPEND LIB_SRC_LIST
src/multiply.c
src/divide.c
)
message(STATUS "Advanced features enabled")
endif()
# 创建静态库
add_library(calculator STATIC ${LIB_SRC_LIST})
# 为这个库设置头文件路径(PUBLIC表示使用这个库的目标也会继承这个路径)
target_include_directories(calculator PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# 如果是Debug模式,添加DEBUG_MODE宏
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(calculator PRIVATE DEBUG_MODE)
endif()
# 如果启用高级功能,添加宏
if(ADVANCED_FEATURES)
target_compile_definitions(calculator PUBLIC ADVANCED_FEATURES)
endif()

app/main.c:

#include <stdio.h>
#include "calculator.h"
int main(void) {
printf("=== Simple Calculator ===\n\n");
int a = 10, b = 5;
printf("Basic operations:\n");
printf("%d + %d = %d\n", a, b, add(a, b));
printf("%d - %d = %d\n", a, b, subtract(a, b));
#ifdef ADVANCED_FEATURES
printf("\nAdvanced operations:\n");
printf("%d * %d = %d\n", a, b, multiply(a, b));
printf("%d / %d = %d\n", a, b, divide(a, b));
// 测试除以零
printf("%d / 0 = %d\n", a, divide(a, 0));
#else
printf("\n[Advanced features disabled]\n");
#endif
return 0;
}

app/CMakeLists.txt:

# 应用程序的CMakeLists.txt
# 创建可执行文件
add_executable(calc_app main.c)
# 链接计算库
target_link_libraries(calc_app calculator)

主CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(CalculatorProject VERSION 1.0.0 LANGUAGES C)
# 设置C标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# 定义选项
option(ADVANCED_FEATURES "Enable advanced features (multiply, divide)" ON)
# 设置输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib)
# 输出构建信息
message(STATUS "=================================")
message(STATUS "Calculator Project Configuration")
message(STATUS "=================================")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS "C Compiler: ${CMAKE_C_COMPILER}")
message(STATUS "Advanced features: ${ADVANCED_FEATURES}")
message(STATUS "=================================")
# 添加编译选项
if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang")
add_compile_options(-Wall -Wextra)
endif()
# 添加子目录
add_subdirectory(calc_lib)
add_subdirectory(app)

构建和运行:

场景1:Release模式,启用高级功能

mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DADVANCED_FEATURES=ON
make
../bin/calc_app

输出:

=== Simple Calculator ===
Basic operations:
10 + 5 = 15
10 - 5 = 5
Advanced operations:
10 * 5 = 50
10 / 5 = 2
Error: Division by zero!
10 / 0 = 0
View Code

场景2:Debug模式,启用高级功能

rm -rf *
cmake .. -DCMAKE_BUILD_TYPE=Debug -DADVANCED_FEATURES=ON
make
../bin/calc_app

输出(包含调试信息):

=== Simple Calculator ===
Basic operations:
[DEBUG] add(10, 5)
10 + 5 = 15
[DEBUG] subtract(10, 5)
10 - 5 = 5
Advanced operations:
[DEBUG] multiply(10, 5)
10 * 5 = 50
[DEBUG] divide(10, 5)
10 / 5 = 2
[DEBUG] divide(10, 0)
Error: Division by zero!
10 / 0 = 0
View Code

场景3:只使用基本功能

rm -rf *
cmake .. -DCMAKE_BUILD_TYPE=Release -DADVANCED_FEATURES=OFF
make
../bin/calc_app

输出:

=== Simple Calculator ===
Basic operations:
10 + 5 = 15
10 - 5 = 5
[Advanced features disabled]
View Code

项目亮点:

  1. 模块化设计:库和应用分离
  2. 条件编译:使用选项控制功能
  3. 调试支持:Debug模式输出详细信息
  4. 清晰的依赖关系:使用target_*命令
  5. 统一的输出目录:便于查找生成的文件
  6. 详细的构建信息:使用message()输出配置

扩展练习:

  1. 添加更多算术运算(求幂、求余等)
  2. 创建一个测试程序,自动测试所有功能
  3. 将应用程序改为交互式(从用户输入读取操作)
  4. 尝试生成动态库版本

学习路径总结:

  1. 单文件项目 多文件项目 目录组织 创建库 使用库 编译配置 完整项目

进一步学习:

  • CMake官方文档:https://cmake.org/documentation/
  • 学习如何使用CMake管理依赖(find_package、FetchContent)
  • 学习交叉编译和工具链文件
  • 研究大型开源项目的CMake配置

最后的建议:

  • 实践是最好的老师,多动手尝试
  • 遇到问题时,善用message()调试
  • 保持代码简洁,避免过度复杂的配置
  • 遵循最佳实践,使用target_*命令

希望这个教程能帮助你从CMake新手成长为熟练使用者!


参考资料

posted @ 2022-05-02 17:35  凌逆战  阅读(6200)  评论(0)    收藏  举报