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平台
- 访问 CMake官网 根据系统位数下载对应版本
- 运行安装程序,建议选择"为所有用户添加CMake到系统PATH"
- 安装完成后,打开命令提示符(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项目。
理解构建过程:
- cmake.. :CMake读取上级目录的CMakeLists.txt,生成构建文件(如Makefile)
- make :使用生成的Makefile编译源代码,生成可执行文件
- ./hello_cmake :运行生成的可执行文件
清理构建缓存
如果需要重新配置项目(比如修改了CMakeLists.txt),可以清理CMake缓存:
# 方法1:删除整个构建目录(最彻底) cd .. rm -rf build mkdir build cd build cmake .. # 方法2:只删除缓存文件 cd build rm -rf CMakeCache.txt CMakeFiles/ cmake ..
练习建议:
- 修改
main.c,让它打印更多信息,然后重新构建 - 尝试修改项目名称,观察生成的可执行文件名称变化
- 尝试创建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
练习建议:
- 为计算器添加乘法和除法功能
- 尝试将每个功能模块放在单独的子目录中
- 尝试配置不同的输出路径(如debug和release目录)
第三部分:库的使用
学习目标:
- 理解为什么需要库以及库的作用
- 学会创建静态库和动态库
- 掌握如何链接和使用库
- 了解静态库和动态库的区别
在前面的章节中,我们的所有代码都编译成一个可执行文件。但在实际开发中,我们经常需要将通用的功能封装成库,供多个项目复用。
为什么需要库
什么是库? 库是预先编译好的代码集合,包含可重用的函数和数据结构。
使用库的好处:
- 代码复用:一次编写,多处使用
- 模块化管理:将功能分解为独立模块,便于维护
- 隐藏实现细节:只暴露接口(头文件),保护源代码
- 加快编译速度:库一旦编译好,使用时无需重新编译
静态库 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.a或libcalc.so - 找到后将完整路径保存到变量中
- 默认优先查找动态库,如果只有静态库则使用静态库
target_link_libraries(目标库):将库链接到目标
构建和运行:
mkdir buildcd buildcmake ..make../bin/calculator
指定使用静态库或动态库:
如果同时存在静态库和动态库,你可以明确指定使用哪个:
# 明确指定使用静态库find_library(CALC_LIB libcalc.a HINTS ${PROJECT_SOURCE_DIR}/testFunc/lib)# 或明确指定使用动态库find_library(CALC_LIB libcalc.so HINTS ${PROJECT_SOURCE_DIR}/testFunc/lib)
验证使用了哪个库:
在Linux下,可以使用readelf命令查看可执行文件的依赖:
readelf -d bin/calculator
如果链接的是动态库,会显示依赖libcalc.so;如果是静态库,不会显示这个依赖。
动态库的运行时加载:
如果使用动态库,运行时需要能找到库文件。有几种方法:
- 将库复制到系统库目录(需要root权限):
sudo cp lib/libcalc.so /usr/local/lib/sudo ldconfig
- 设置LDLIBRARYPATH环境变量(临时):
export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH./calculator
- 在CMakeLists.txt中设置RPATH(推荐):
set(CMAKE_INSTALL_RPATH "${PROJECT_SOURCE_DIR}/testFunc/lib")set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
查看系统中是否已安装某个库:
ldconfig -p | grep calc
ldconfig会在默认搜索目录(/lib、/usr/lib)和配置文件(/etc/ld.so.conf)中搜索动态库。
练习建议:
- 将计算器的每个功能(加、减、乘、除)封装为独立的库
- 创建一个项目,同时链接多个库
- 尝试同时编译静态库和动态库,并分别测试
第四部分:编译配置
学习目标:
- 掌握常用的编译选项配置方法
- 学会使用条件编译控制代码
- 理解CMake变量的使用
- 学会配置Debug和Release模式
有时我们需要为编译器添加特定的选项,或者根据不同条件编译不同的代码。CMake提供了灵活的配置方式。
编译选项
指定编译器:
# 指定C编译器set(CMAKE_C_COMPILER gcc)# 指定C++编译器set(CMAKE_CXX_COMPILER g++)
设置编译选项的三种方法:
方法1:addcompileoptions
add_compile_options(-Wall -Werror -Wstrict-prototypes)
这会为所有编译器(C和C++)添加选项。
方法2:add_definitions
add_definitions("-Wall -Werror -Wstrict-prototypes")
同样为所有编译器添加选项。注意需要用引号包裹所有选项。
方法3:set命令修改CMAKECFLAGS或CMAKECXXFLAGS
# 只针对C编译器set(CMAKE_C_FLAGS "-Wall -Werror -Wstrict-prototypes")# 只针对C++编译器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。
项目结构:
conditional/├── CMakeLists.txt└── src/├── CMakeLists.txt├── main1.c└── 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_LIST、MY_INCLUDE_DIRS - 避免使用CMake保留的变量名
访问环境变量:
# 读取环境变量 set(MY_PATH $ENV{PATH}) message(STATUS "System PATH: ${MY_PATH}") # 设置环境变量(只在CMake执行期间有效) set(ENV{MY_VAR} "some_value")
练习建议:
- 创建Debug和Release两个构建配置,观察生成的可执行文件大小差异
- 使用option创建一个"日志级别"选项,控制程序输出详细程度
- 尝试使用变量来管理项目版本号
第五部分: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 .
练习建议:
- 创建一个包含多个子目录的项目,使用target_*命令配置每个目标
- 使用message()输出构建信息,观察变量的值
- 编写一个循环,批量处理源文件或模块
第六部分:完整案例
学习目标:
- 综合运用前面学到的所有知识
- 构建一个结构清晰的完整项目
- 理解实际项目的组织方式
一个完整的项目示例
让我们构建一个完整的计算器项目,包含:
- 核心计算库(静态库)
- 命令行应用程序
- 条件编译功能
- Debug和Release配置
项目需求:
- 将计算功能(加、减、乘、除)封装为库
- 提供命令行程序使用该库
- 支持Debug模式输出详细信息
- 使用条件编译控制高级功能
目录结构:
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
场景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
场景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]
项目亮点:
- 模块化设计:库和应用分离
- 条件编译:使用选项控制功能
- 调试支持:Debug模式输出详细信息
- 清晰的依赖关系:使用target_*命令
- 统一的输出目录:便于查找生成的文件
- 详细的构建信息:使用message()输出配置
扩展练习:
- 添加更多算术运算(求幂、求余等)
- 创建一个测试程序,自动测试所有功能
- 将应用程序改为交互式(从用户输入读取操作)
- 尝试生成动态库版本
学习路径总结:
单文件项目 → 多文件项目 → 目录组织 → 创建库 → 使用库 → 编译配置 → 完整项目
进一步学习:
- CMake官方文档:https://cmake.org/documentation/
- 学习如何使用CMake管理依赖(find_package、FetchContent)
- 学习交叉编译和工具链文件
- 研究大型开源项目的CMake配置
最后的建议:
- 实践是最好的老师,多动手尝试
- 遇到问题时,善用message()调试
- 保持代码简洁,避免过度复杂的配置
- 遵循最佳实践,使用target_*命令
希望这个教程能帮助你从CMake新手成长为熟练使用者!
浙公网安备 33010602011771号