CMake入门——简介和基本使用

CMake 简介与使用方法


1. cmake 简介

什么是 cmake?

cmake 是一个跨平台的自动构建工具,它的诞生主要是为了解决直接使用 make + Makefile 方式无法实现跨平台的问题。

cmake 的主要优点

优点 说明
开放源代码 可从 cmake 官网 https://cmake.org/ 下载源代码
跨平台 cmake 不直接编译出最终文件,而是允许开发者编写与平台无关的 CMakeLists.txt,cmake 解析后根据当前编译平台生成本地化的 Makefile,最后由 make 编译
语法规则简单 Makefile 语法复杂且不同平台不统一;cmake 的 CMakeLists.txt 语法与平台无关,简单易懂

cmake 的工作流程

img

本质:cmake 只是一个"生成器",真正执行编译的还是 make 工具。


2. cmake 和 Makefile

  • Makefile:Linux 下传统的构建脚本,直接由 make 工具解析执行。不同平台需要编写不同的 Makefile,跨平台能力弱。
  • cmake:自动生成 Makefile 的工具。开发者只需编写一份平台无关的 CMakeLists.txt,cmake 会根据当前平台自动生成对应的 Makefile。

除了 cmake 之外,还有 automakeautoconf 等,但 cmake 是目前最主流的选择。


3. cmake 的使用方法

3.1 安装 cmake

在 Ubuntu 系统中,可以通过 apt-get 在线安装:

sudo apt-get install cmake

安装完成后查看版本号:

cmake --version

3.2 官方学习资源


示例一:单个源文件

工程结构:

├── CMakeLists.txt
└── main.c

main.c:

#include <stdio.h>

int main()
{
    printf("Hello World!\n");
    return 0;
}

CMakeLists.txt:

project(HELLO)
add_executable(hello ./main.c)

构建步骤:

cmake ./         # 生成 Makefile
make             # 编译得到可执行文件 hello
./hello          # 运行

CMakeLists.txt 说明:

CMakeLists.txt 中的命令用法类似于 C 语言函数——命令名后跟一对括号,多个参数之间用空格分隔,而不是逗号。

  • project(HELLO) —— project 命令用于设置工程的名称。括号中的 HELLO 就是要设置的工程名。虽然设不设置工程名不是强制性的,但最好加上。设置工程名后,cmake 会自动定义两个变量:PROJECT_SOURCE_DIR(源码路径)和 PROJECT_BINARY_DIR(构建路径),后续会用到。

  • add_executable(hello ./main.c) —— add_executable 命令用于生成可执行文件。第一个参数 hello 指定生成的可执行文件名;第二个参数 ./main.c 指定源文件路径。cmake 解析此命令后,会在生成的 Makefile 中写入对应的编译规则,最终 make 编译出来就是一个名为 hello 的可执行文件。


out-of-source 构建(推荐)

为了避免 cmake 生成的中间文件与源代码混在一起,推荐使用 out-of-source 方式构建:

├── build/          # 新建 build 目录
├── CMakeLists.txt
└── main.c
cd build/
cmake ../           # 在 build 目录中运行 cmake
make                # 编译

为什么是 cmake ../

cmake 命令的参数指定的是 CMakeLists.txt 所在路径,而非当前工作目录。

  • 此时我们在 build/ 目录下,而 CMakeLists.txt 在上层(工程根目录)
  • ../ 表示上层目录,cmake 会去那里寻找 CMakeLists.txt 并解析
  • 生成的 MakefileCMakeCache.txt 等中间文件全部输出到当前目录(即 build/),不会污染源码目录
  • 如果要清理工程,只需 rm -rf build/ 即可

所有中间文件和最终产物都在 build/ 目录下,清理时直接删除 build/ 目录即可。

img


示例二:多个源文件

工程结构:

├── build/
├── CMakeLists.txt
├── hello.c
├── hello.h
└── main.c

CMakeLists.txt:

project(HELLO)

set(SRC_LIST main.c hello.c)
add_executable(hello ${SRC_LIST})

关键点:

  • set(SRC_LIST main.c hello.c) —— set 命令用于定义变量(类似于 C 语言的宏定义)。这里定义了一个名为 SRC_LIST 的变量,其值为源文件列表 main.c hello.c。变量名一般用大写,以便与命令名区分。

  • ${SRC_LIST} —— 在 cmake 中,${变量名} 表示引用变量的值。cmake 预处理时会将 ${SRC_LIST} 展开为 main.c hello.c,等价于直接写 add_executable(hello main.c hello.c)。你也可以不定义变量,直接列出源文件:

    add_executable(hello main.c hello.c)
    
  • 关于 ./main.cmain.c 的区别 —— 在示例一中我们写的是 ./main.c,这里写的是 main.c,两者在 cmake 中是完全等价的,都表示当前目录下的 main.c 文件。./ 只是显式地标明了"当前目录",加不加没有区别。第一个例子加上 ./ 是为了让初学者更清楚地看出路径关系,后续示例为了简洁就去掉了。


示例三:生成库文件

hello.c 编译为库文件,并链接到可执行文件。

CMakeLists.txt:

project(HELLO)
add_library(libhello hello.c)
add_executable(hello main.c)
target_link_libraries(hello libhello)

构建结果:

├── build/
│   ├── hello
│   └── liblibhello.a
├── CMakeLists.txt
├── hello.c
├── hello.h
└── main.c

相关命令详解:

命令 作用
add_library(libhello hello.c) 生成静态库文件 liblibhello.a(默认生成静态库)
add_library(libhello SHARED hello.c) 生成动态库文件(共享库 .so
add_library(libhello STATIC hello.c) 生成静态库文件(显式指定)
target_link_libraries(hello libhello) libhello 库链接到 hello 可执行文件

什么叫"链接到可执行文件"?

先回顾一下 C/C++ 程序的编译过程:

源文件(.c)  →  编译(compile)  →  目标文件(.o)
目标文件(.o) + 库文件(.a/.so)  →  链接(link)  →  可执行文件

编译阶段:每个 .c 文件被单独编译成 .o 目标文件。此时如果 main.c 中调用了 hello() 函数,编译器在 main.o 中只会留下一个"未解析的符号"(函数调用地址待定)。

链接阶段:链接器将各个 .o 文件拼在一起,同时去指定的库文件中查找那些"未解析的符号"。如果找到了,就把库中对应的机器码合并到最终的可执行文件中。

举个例子 —— 本例中:

  • main.c 调用了 hello("World") 函数
  • 这个函数定义在 hello.c 中,并被编译到了 libhello.a(或 libhello.so)中
  • 如果没有 target_link_libraries(hello libhello),链接器就不知道上哪去找 hello 函数的实现,会报"未定义的引用"错误
  • 加上之后,链接器就会去 libhello 库中查找并解析这个符号,最终成功生成可执行文件

静态链接和动态链接:

类型 命令 原理说明 优点 缺点
静态库 (.a) add_library(libhello hello.c) 链接时,链接器把 libhello.ahello() 函数的机器码原样拷贝一份,塞进 hello 这个可执行文件里。最终 hello 文件本身就包含了 hello() 的全部代码。 生成的可执行文件不依赖外部的库文件,拷贝到别的机器上也能直接运行。 ① 如果 10 个程序都用到了同一个静态库,每个程序都含有一份拷贝,磁盘和内存浪费;② 如果库修复了一个 bug,所有用到它的程序都得重新链接
动态库 (.so) add_library(libhello SHARED hello.c) 链接时,链接器不拷贝代码,只在 hello 可执行文件中记下一笔:"我需要 libhello.so 中的 hello() 函数"。等到真正运行 ./hello 时,系统才去硬盘上找到 libhello.so,加载到内存,再让 hello 程序去调用它。 ① 多个程序可以共享内存中的同一份 .so(物理内存只存一份),节省内存;② 单独更新 .so 文件就能修复所有程序的 bug,无需重新链接 运行环境必须存在对应的 .so 文件,否则程序启动会报错 "cannot open shared object file"。

修改输出库文件名称:

默认会生成 liblibhello.alib + 目标名 libhello + .a),前缀和后缀重复了,如果想生成 libhello.a 就需要修改输出名称:

set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")

语法解释: set_target_properties(目标名 PROPERTIES 属性名 "属性值")

  • libhello —— 要修改的目标名,即 add_library(libhello hello.c) 中定义的那个目标
  • PROPERTIES —— 关键字,告诉 cmake 后面要跟属性-值对
  • OUTPUT_NAME —— 属性名,表示"输出文件的基础名称(不含前缀后缀)"
  • "hello" —— 属性值,将输出名称设为 hello

Linux 下库文件的命名规则是:lib + 基础名称 + .后缀。所以 OUTPUT_NAME 设为 "hello" 后,最终生成的文件就是 libhello.so(或 libhello.a),而不是默认的 liblibhello.a

完整示例:

cmake_minimum_required(VERSION 3.5)
project(HELLO)

add_library(libhello SHARED hello.c)
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")

add_executable(hello main.c)
target_link_libraries(hello libhello)

cmake_minimum_required(VERSION 3.5):设置当前工程的最低 cmake 版本要求,非强制但建议加上。
目标名(target name)在整个工程中必须唯一,不可重复。


示例四:将源文件组织到不同目录

当源文件较多时,应按功能/模块拆分到不同目录。

工程结构:

├── build/
├── CMakeLists.txt                 # 顶层
├── libhello/
│   ├── CMakeLists.txt             # 子目录
│   ├── hello.c
│   └── hello.h
└── src/
    ├── CMakeLists.txt             # 子目录
    └── main.c

顶层 CMakeLists.txt:

cmake_minimum_required(VERSION 3.5)
project(HELLO)

add_subdirectory(libhello)
add_subdirectory(src)

src/CMakeLists.txt:

include_directories(${PROJECT_SOURCE_DIR}/libhello)
add_executable(hello main.c)
target_link_libraries(hello libhello)

libhello/CMakeLists.txt:

add_library(libhello hello.c)
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")

关键命令:

命令/变量 作用
add_subdirectory(libhello) 告诉 cmake 去子目录寻找 CMakeLists.txt 并解析
include_directories(...) 指定头文件搜索路径
${PROJECT_SOURCE_DIR} cmake 内置变量,指向工程源码根目录

构建结果:

├── build/
│   ├── libhello/
│   │   └── libhello.a
│   └── src/
│       └── hello
├── CMakeLists.txt
├── libhello/
│   ├── CMakeLists.txt
│   ├── hello.c
│   └── hello.h
└── src/
    ├── CMakeLists.txt
    └── main.c

示例五:分离可执行文件和库文件

在示例四的基础上,让可执行文件输出到 build/bin,库文件输出到 build/lib,不再和 cmake 中间文件混在一起。本例同样是三个 CMakeLists.txt

构建前目录结构(源码组织):

├── build/                         # 空目录,用于 out-of-source 构建
├── CMakeLists.txt                 # ⬅ 顶层 CMakeLists.txt
├── libhello/                      # 库模块目录
│   ├── CMakeLists.txt             # ⬅ 子目录 CMakeLists.txt
│   ├── hello.c
│   └── hello.h
└── src/                           # 主程序目录
    ├── CMakeLists.txt             # ⬅ 子目录 CMakeLists.txt
    └── main.c

三个 CMakeLists.txt 的内容及职责:

① 顶层 CMakeLists.txt —— 与示例四完全相同:

cmake_minimum_required(VERSION 3.5)
project(HELLO)

add_subdirectory(libhello)
add_subdirectory(src)

② src/CMakeLists.txt —— 与示例四相比,多了一行 EXECUTABLE_OUTPUT_PATH,指定可执行文件的输出目录:

include_directories(${PROJECT_SOURCE_DIR}/libhello)

set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
add_executable(hello main.c)
target_link_libraries(hello libhello)

③ libhello/CMakeLists.txt —— 与示例四相比,多了一行 LIBRARY_OUTPUT_PATH,指定库文件的输出目录:

set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

add_library(libhello hello.c)
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")

关键变量:

变量 作用
EXECUTABLE_OUTPUT_PATH 控制可执行文件的输出路径
LIBRARY_OUTPUT_PATH 控制库文件的输出路径
${PROJECT_BINARY_DIR} cmake 内置变量,指向构建目录的根目录(即 build 目录)
变量 谁定义的 值(以本示例结构为例)
PROJECT_SOURCE_DIR project() 自动定义 顶层 CMakeLists.txt 所在目录,即 /project/
PROJECT_BINARY_DIR project() 自动定义 执行 cmake 的目录,即 /project/build/
EXECUTABLE_OUTPUT_PATH 需用户手动 set() 不设置则默认为 PROJECT_BINARY_DIR
LIBRARY_OUTPUT_PATH 需用户手动 set() 不设置则默认为 PROJECT_BINARY_DIR

所以本例中 set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) 等价于设置输出路径为 /project/build/bin

构建后目录结构(cd build && cmake .. && make 之后):

├── build/                         # 所有构建产物都在这里
│   ├── bin/
│   │   └── hello                  # ⬅ 可执行文件单独放在 bin/
│   ├── lib/
│   │   └── libhello.a             # ⬅ 库文件单独放在 lib/
│   ├── CMakeCache.txt             # cmake 生成的中间文件
│   ├── CMakeFiles/
│   ├── cmake_install.cmake
│   └── Makefile
├── CMakeLists.txt                 # 源码不变
├── libhello/
│   ├── CMakeLists.txt
│   ├── hello.c
│   └── hello.h
└── src/
    ├── CMakeLists.txt
    └── main.c

4. 常用命令速查表

命令 功能
project(name) 设置工程名称
cmake_minimum_required(VERSION x.y) 设置最低 cmake 版本
add_executable(target src) 生成可执行文件
add_library(target src) 生成库文件(默认静态库)
add_library(target SHARED src) 生成动态库
add_library(target STATIC src) 生成静态库
target_link_libraries(target lib) 链接库到目标
set(var value) 设置变量
${var} 引用变量
add_subdirectory(dir) 添加并解析子目录的 CMakeLists.txt
include_directories(path) 指定头文件搜索路径
set_target_properties(...) 设置目标属性
posted @ 2026-07-23 17:02  Javenwww  阅读(45)  评论(0)    收藏  举报