news 2026/10/1 4:54:15

CMake实战指南:从Makefile迁移到多目录构建与动态库生成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CMake实战指南:从Makefile迁移到多目录构建与动态库生成

简介:这份《CMake实战》PDF面向需要掌握跨平台自动化构建的C/C++开发者、嵌入式工程师及在校学生,尤其适合正在从手写Makefile或Autotools转向CMake的中级读者。内容围绕CMake的安装配置、CMakeLists.txt语法、静态库与动态库构建、外部库查找与链接、常用变量与环境变量、INSTALL与FIND系列指令,以及自定义Find模块和多目录工程模板展开,并配有Hello World、共享库、模块化工程等递进式实例。资源包内共1个PDF文件,约998KB,篇幅紧凑,便于按章节查阅与随身携带。目前已有978人学习,说明其在构建工具学习群体中具备一定参考价值。读者可借此系统梳理CMake的构建逻辑,理解cmake与make的协作方式,掌握库依赖处理与模块扩展思路,为大型项目或多平台构建打下基础。

1. 从一份 CMake 实战 PDF 说起:为什么手写 Makefile 的团队最后都换了

如果你维护过一个超过 20 个源文件的 C/C++ 工程,大概率经历过这种场景:新增一个模块,Makefile 里要改三处依赖;换台机器编译,路径全挂;想把静态库换成动态库,链接顺序调半天。这份《CMake 实战.pdf》就是冲着这类问题来的——它不是官方文档的翻译,而是一份从 helloworld 一路推到多目录、带 so 生成的实战笔记,覆盖安装、外部构建、静态库/动态库、INSTALL 指令、自定义 Find 模块这些真正会在工程里用到的环节。适合谁?正在从手写 Makefile 或 autotools 往 CMake 迁移的 C/C++ 开发者,以及需要给团队搭一套可复用构建模板的人。它不教你 C++ 语法,只解决一件事:让构建这件事从“编译专家的黑匣子”变成普通工程师能改、能扩、能交接的东西。

2. 安装与第一个可执行目标:把 cmake 和 make 的工具链跑通

2.1 为什么优先用官方二进制包而不是包管理器

很多人第一反应是apt install cmake或yum install cmake,但发行版仓库里的版本往往偏旧,而 CMake 的语法和模块在不同版本间有实际差异——比如target_link_libraries的PUBLIC/PRIVATE关键字、FetchContent的行为,老版本上跑不通的写法在新版本里可能是标准做法。这份 PDF 选择的是官方预编译二进制包,解压即用,不污染系统包管理,也方便在同一台机器上并存多个版本。常见做法是把解压目录放到/opt下,再用软链接把bin里的可执行文件挂到/usr/bin,这样终端里直接敲cmake就能命中。

# 卸载旧版本(非必需,仅在包管理器装过时执行) apt-get autoremove cmake # 下载官方预编译包(版本按需替换) wget https://cmake.org/files/v3.9/cmake-3.9.1-Linux-x86_64.tar.gz # 解压 tar zxvf cmake-3.9.1-Linux-x86_64.tar.gz # 移动到 /opt 并建立软链接 mv cmake-3.9.1-Linux-x86_64 /opt/cmake-3.9.1 ln -sf /opt/cmake-3.9.1/bin/* /usr/bin/

这里几个参数值得说清楚:tar zxvf里的z是 gzip 解压、x是提取、v是显示过程、f指定文件名,顺序不能乱,f必须紧跟文件名。ln -sf的-s是符号链接(软链接),-f是强制覆盖已存在的同名链接——如果你之前装过别的版本,不加-f会报“文件已存在”然后静默失败,这是新手最常踩的坑之一。解压后的目录结构里,bin下有cmake、ccmake、cmake-gui、cpack、ctest五个程序,日常构建只用cmake,ccmake是终端里的交互式配置界面,cmake-gui是图形版,调试复杂配置时比反复改命令行参数舒服。

2.2 helloworld 的三个指令:PROJECT、SET、ADD_EXECUTABLE

先建一个练习目录,把最小工程跑起来。这一步的目的不是学会写 helloworld,而是理解 CMake 的“两段式”构建:先由cmake读取CMakeLists.txt生成 Makefile,再由make执行真正的编译链接。

mkdir -p cmake/t1 && cd cmake/t1

main.c内容:

#include <stdio.h> int main() { printf("Hello World from t1 Main!\n"); return 0; }

CMakeLists.txt内容:

PROJECT(HELLO) SET(SRC_LIST main.c) MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR}) MESSAGE(STATUS "This is SOURCE dir " ${HELLO_SOURCE_DIR}) ADD_EXECUTABLE(hello ${SRC_LIST})

逐条拆:PROJECT(HELLO)定义工程名,同时隐式创建HELLO_BINARY_DIR和HELLO_SOURCE_DIR两个变量,前者指向构建目录,后者指向源码目录。内部构建时两者相同,外部构建时才会分开——这也是后面要讲外部构建的伏笔。SET(SRC_LIST main.c)是显式定义变量,多个源文件写成SET(SRC_LIST main.c t1.c t2.c)。MESSAGE(STATUS ...)向终端输出信息,STATUS前缀是--,另有SEND_ERROR(产生错误但继续)和FATAL_ERROR(立即终止)两种级别。ADD_EXECUTABLE(hello ${SRC_LIST})定义生成名为hello的可执行文件,源文件列表来自变量。

注意${}是变量引用语法,但在IF控制语句里是直接用变量名、不加${}——这是 CMake 语法里最容易记混的一条。指令本身大小写无关,PROJECT和project等价,但参数和变量名大小写敏感,SRC_LIST和src_list是两个不同的变量。工程名HELLO和生成的可执行文件名hello没有任何绑定关系,ADD_EXECUTABLE(t1 main.c)照样能生成t1。

构建过程:

cmake . make ./hello

cmake .后面那个点代表当前目录,执行后会生成CMakeFiles、CMakeCache.txt、cmake_install.cmake和Makefile。make的输出里能看到Scanning dependencies、Building C object、Linking C executable三个阶段。想看完整编译命令加make VERBOSE=1,排查链接错误时这个参数几乎是必用的。

2.3 内部构建的代价与外部构建的切换

上面用的是内部构建(in-source build),中间文件和源码混在一起,CMakeCache.txt一旦生成,改CMakeLists.txt后有时不会重新配置,得手动删缓存。更麻烦的是make distclean在 CMake 工程里是无效的——官方明确不提供这个目标,因为CMakeLists.txt可以执行任意脚本生成临时文件,CMake 无法追踪到底生成了哪些,提供一个“看起来能清理干净”的目标反而会误导人。

外部构建的做法是单独建build目录:

# 先清掉内部构建产生的中间文件,关键是 CMakeCache.txt rm -rf CMakeFiles CMakeCache.txt cmake_install.cmake Makefile mkdir build && cd build cmake .. make

cmake ..里的..指向父目录,因为CMakeLists.txt在那里。构建产物全部落在build目录内,源码目录保持干净。此时HELLO_SOURCE_DIR仍指向源码路径,而HELLO_BINARY_DIR指向build路径,两者正式分家。这也是为什么建议统一用PROJECT_BINARY_DIR和PROJECT_SOURCE_DIR这两个预定义变量——它们不随工程名变化,改工程名时不用同步改一堆引用。

3. 让工程像个工程:ADD_SUBDIRECTORY、输出路径与 INSTALL 规则

3.1 用 ADD_SUBDIRECTORY 拆分源码目录

helloworld 跑通后,下一步是把源码挪进src子目录,让工程结构清晰。每个需要被管理的目录都要有自己的CMakeLists.txt,这是 CMake 的硬性约定。

mkdir src mv main.c src

根目录CMakeLists.txt:

PROJECT(HELLO) ADD_SUBDIRECTORY(src bin)

src/CMakeLists.txt:

ADD_EXECUTABLE(hello main.c)

ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])三个参数:第一个是源目录,第二个是编译输出目录(中间结果和目标文件都放这里),第三个EXCLUDE_FROM_ALL表示该目录默认不参与构建,适合example这类需要单独编译的子工程。上面写成ADD_SUBDIRECTORY(src bin),编译后hello会出现在build/bin下;如果省略bin,则落在build/src下。老写法SUBDIRS(src)已不推荐,它不支持指定输出目录,且会保留完整的目录层级。

3.2 用 EXECUTABLE_OUTPUT_PATH 控制产物落点

ADD_SUBDIRECTORY控制的是整个子目录的输出位置,如果只想改最终可执行文件或库的落点、不动中间文件,用SET重定义两个变量:

SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

EXECUTABLE_OUTPUT_PATH管可执行文件,LIBRARY_OUTPUT_PATH管库文件,都不影响.o这类中间产物。写在哪?原则很简单:哪里出现ADD_EXECUTABLE或ADD_LIBRARY,就写在哪。本例中就是src/CMakeLists.txt。${PROJECT_BINARY_DIR}在外部构建时指向build目录,所以最终路径是build/bin和build/lib。

3.3 INSTALL 指令的四种安装类型

安装规则用INSTALL指令定义,配合CMAKE_INSTALL_PREFIX变量控制安装根路径,作用类似 autotools 的--prefix。命令行指定方式:

cmake -DCMAKE_INSTALL_PREFIX=/usr .. make make install

INSTALL按安装对象分四类,参数各有侧重:

安装类型指令形式默认权限典型用途
目标文件INSTALL(TARGETS ...)按类型可执行文件、动态库、静态库
普通文件INSTALL(FILES ...)644配置文件、文档
可执行脚本INSTALL(PROGRAMS ...)755shell 脚本、辅助工具
目录INSTALL(DIRECTORY ...)继承源资源目录、头文件目录

目标文件安装要区分类型:

INSTALL(TARGETS myrun mylib mystaticlib RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION libstatic)

RUNTIME对应可执行二进制,LIBRARY对应动态库,ARCHIVE对应静态库。DESTINATION如果以/开头就是绝对路径,CMAKE_INSTALL_PREFIX失效;写相对路径才会拼接到前缀后面。目录安装有个容易翻车的细节:INSTALL(DIRECTORY abc DESTINATION share)会把abc目录本身装过去,而INSTALL(DIRECTORY abc/ DESTINATION share)只装abc里的内容、不含目录本身,末尾那个斜杠的区别必须记住。

4. 静态库、动态库与外部依赖:从 ADD_LIBRARY 到自定义 Find 模块

4.1 ADD_LIBRARY 构建共享库与静态库

库的构建用ADD_LIBRARY,语法是ADD_LIBRARY(libname [SHARED|STATIC|MODULE] [EXCLUDE_FROM_ALL] source1 source2 ...)。SHARED生成动态库(.so),STATIC生成静态库(.a),不指定则根据BUILD_SHARED_LIBS变量决定,默认是静态。

# 构建共享库 ADD_LIBRARY(hello SHARED hello.c) # 构建静态库 ADD_LIBRARY(hello_static STATIC hello.c)

同一个源文件不能同时用于两个同名目标,所以静态库和动态库要用不同目标名。如果想让两者输出同名文件(比如都叫libhello),需要设置OUTPUT_NAME属性:

SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello")

动态库版本号通过VERSION和SOVERSION两个属性控制:

SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1)

VERSION是完整版本号,SOVERSION是 API 兼容版本号。生成的libhello.so.1.2是实际文件,libhello.so.1和libhello.so是指向它的软链接,运行时加载器按SOVERSION找库。

4.2 头文件搜索路径与 target 链接库

使用外部库时,编译器需要找到头文件,链接器需要找到库文件。CMake 里对应两个指令:

INCLUDE_DIRECTORIES(/usr/include/hello) TARGET_LINK_LIBRARIES(main hello)

INCLUDE_DIRECTORIES把路径加到编译器的-I参数里,TARGET_LINK_LIBRARIES把库加到链接器的-l参数里。顺序上,TARGET_LINK_LIBRARIES要写在ADD_EXECUTABLE之后,因为它的第一个参数是已经定义好的 target 名。

如果不想在CMakeLists.txt里硬编码路径,可以用环境变量CMAKE_INCLUDE_PATH和CMAKE_LIBRARY_PATH来补充搜索路径:

export CMAKE_INCLUDE_PATH=/usr/local/include/hello export CMAKE_LIBRARY_PATH=/usr/local/lib

这两个变量影响的是FIND_PATH和FIND_LIBRARY的搜索行为,不是直接加到编译参数里,所以配合FIND_系列指令用才有效。

4.3 自定义 FindHELLO 模块查找非标准库

当依赖的库没有提供 CMake 配置文件时,需要自己写Find<Package>.cmake模块。模块放在CMAKE_MODULE_PATH指向的目录下,用FIND_PACKAGE调用。

# FindHELLO.cmake FIND_PATH(HELLO_INCLUDE_DIR hello.h /usr/include/hello /usr/local/include/hello) FIND_LIBRARY(HELLO_LIBRARY NAMES hello PATH /usr/lib /usr/local/lib) IF (HELLO_INCLUDE_DIR AND HELLO_LIBRARY) SET(HELLO_FOUND TRUE) ENDIF () IF (HELLO_FOUND) IF (NOT HELLO_FIND_QUIETLY) MESSAGE(STATUS "Found Hello: ${HELLO_LIBRARY}") ENDIF () ELSE () IF (HELLO_FIND_REQUIRED) MESSAGE(FATAL_ERROR "Could not find hello library") ENDIF () ENDIF ()

FIND_PATH找头文件所在目录,FIND_LIBRARY找库文件,NAMES后面跟库名(不带lib前缀和.so/.a后缀)。HELLO_FIND_QUIETLY和HELLO_FIND_REQUIRED是FIND_PACKAGE自动传入的变量,分别对应QUIET和REQUIRED参数。工程里这样调用:

SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) FIND_PACKAGE(HELLO) IF (HELLO_FOUND) INCLUDE_DIRECTORIES(${HELLO_INCLUDE_DIR}) TARGET_LINK_LIBRARIES(main ${HELLO_LIBRARY}) ENDIF ()

CMAKE_MODULE_PATH告诉 CMake 去哪里找自定义模块,不设置的话FIND_PACKAGE(HELLO)会去系统模块目录找,找不到就报错。

5. 避坑与排查:那些让构建失败但报错看不懂的地方

5.1 现象:改了 CMakeLists.txt 但 make 没反应

原因:CMake 把配置结果缓存在CMakeCache.txt里,某些改动(尤其是变量定义和FIND_指令)不会触发自动重新配置。解决:删掉CMakeCache.txt和CMakeFiles目录重新cmake ..,或者直接删整个build目录重建。外部构建的好处在这里体现得最明显——删build不影响源码。

5.2 现象:链接时报 undefined reference,但库明明装了

原因:TARGET_LINK_LIBRARIES里库的顺序不对。链接器从左到右解析符号,被依赖的库要放在依赖它的目标后面。如果libA依赖libB,写成TARGET_LINK_LIBRARIES(main A B)是对的,写成B A就可能报错。解决:调整顺序,或者用LINK_INTERFACE_LIBRARIES声明库之间的依赖关系让 CMake 自动排序。

5.3 现象:INSTALL 目录时多了一层或少了内容

原因:INSTALL(DIRECTORY abc DESTINATION share)和INSTALL(DIRECTORY abc/ DESTINATION share)行为不同,前者装abc目录本身,后者只装内容。解决:根据需求决定末尾加不加斜杠,装头文件目录时通常希望保留目录名,装资源文件时通常只装内容。

5.4 现象:动态库运行时找不到

原因:libhello.so装到了/usr/local/lib,但系统加载器不搜这个路径。解决:把路径加到/etc/ld.so.conf后执行ldconfig,或者设置LD_LIBRARY_PATH环境变量。更规范的做法是在CMakeLists.txt里用SET_TARGET_PROPERTIES设置INSTALL_RPATH,把运行时搜索路径编进二进制。

5.5 现象:IF 语句里变量判断永远为假

原因:在IF里写了${VAR}而不是VAR。CMake 的IF直接使用变量名,写成${VAR}会被展开成变量的值,然后IF去判断一个叫那个值的变量,自然找不到。解决:IF里去掉${},写成IF (HELLO_FOUND)而不是IF (${HELLO_FOUND})。

6. 多目录模板与 so 生成的完整骨架

把前面所有片段拼成一个可直接复用的多目录模板,这是这份 PDF 最后给出的实战结构,也是我日常起新工程时直接抄的骨架。目录结构如下:

project/ ├── CMakeLists.txt ├── src/ │ ├── CMakeLists.txt │ ├── main.c │ └── hello.c ├── include/ │ └── hello.h └── build/

根目录CMakeLists.txt:

CMAKE_MINIMUM_REQUIRED(VERSION 3.5) PROJECT(HELLO C) # 自定义模块搜索路径 SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) # 全局输出路径 SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib) # 头文件目录 INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR}/include) # 添加子目录,指定输出到 bin ADD_SUBDIRECTORY(src bin) # 安装规则 INSTALL(TARGETS hello RUNTIME DESTINATION bin) INSTALL(FILES ${PROJECT_SOURCE_DIR}/include/hello.h DESTINATION include)

src/CMakeLists.txt:

# 生成共享库 ADD_LIBRARY(hello SHARED hello.c) SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.0 SOVERSION 1) # 生成可执行文件并链接共享库 ADD_EXECUTABLE(main main.c) TARGET_LINK_LIBRARIES(main hello)

构建与验证:

mkdir -p build && cd build cmake -DCMAKE_INSTALL_PREFIX=/usr/local .. make # 检查产物 ls ../lib/ # 应看到 libhello.so.1.0、libhello.so.1、libhello.so ls ../bin/ # 应看到 main # 安装 make install # 验证安装结果 ls /usr/local/lib/libhello* ls /usr/local/bin/main

这个模板里几个关键点:CMAKE_MINIMUM_REQUIRED声明最低版本,避免老版本 CMake 解析新语法时报奇怪的错;PROJECT(HELLO C)显式声明只支持 C 语言,不写的话默认启用所有语言,配置阶段会多花时间检测 C++ 编译器;INCLUDE_DIRECTORIES放在根目录,子目录自动继承;ADD_SUBDIRECTORY(src bin)把src的产物输出到build/bin,配合全局的EXECUTABLE_OUTPUT_PATH和LIBRARY_OUTPUT_PATH,最终可执行文件和库分别落在build/bin和build/lib。

验证动态库版本号是否正确,用readelf -d libhello.so.1.0 | grep SONAME,应该看到SONAME是libhello.so.1。验证可执行文件链接的是哪个库,用ldd bin/main,输出里libhello.so.1应该指向你构建的那个路径。这两个命令是我每次改完库配置后必跑的,比看编译日志快得多。

从那以后我每次新建工程,都强制先跑一遍cmake .. && make && ldd三连,确认产物路径和链接关系没问题再往里加业务代码。构建系统这东西,前期多花十分钟验证,后期少花十小时排查。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 4:53:07

YOLOv8喷码检测实战:从环境踩坑到工业部署闭环

简介&#xff1a;本资源是一套基于YOLOv8实现的食品包装日期喷码缺失检测系统&#xff0c;面向计算机、人工智能、自动化等专业的本科生及初学者&#xff0c;专为毕业设计、课程设计与项目实践打造。系统具备完整目标检测能力&#xff0c;支持可视化界面操作、模型训练与推理全…

作者头像 李华
网站建设 2026/10/1 4:52:59

ZCode开源:AI原生编程助手使用教程与避坑指南

智谱那套东西我之前一直在关注&#xff0c;毕竟GLM系列模型在国产阵营里讨论度不低。这次“ZCode 开源”的消息一出来&#xff0c;热搜词里立刻就堆满了“zcode使用教程”“zcode安装”“zcode下载”这些词。说实话&#xff0c;第一反应是&#xff1a;终于开源了&#xff1f;赶…

作者头像 李华
网站建设 2026/10/1 4:52:52

SVM分类实战:基于Iris鸢尾花数据集的完整项目解析与避坑指南

简介&#xff1a;这是一份面向机器学习课程期末大作业的SVM分类项目资源&#xff0c;基于经典的Iris鸢尾花数据集&#xff0c;提供完整Python源码与配套实验报告。代码基于Python 3.9 IDLE开发&#xff0c;整合sklearn、numpy、matplotlib等模块&#xff0c;覆盖数据读取、标准…

作者头像 李华
网站建设 2026/10/1 4:52:00

磁盘满了?8款跨平台磁盘空间分析利器详解

磁盘又满了&#xff1f;这句话我几乎每周都能在办公室听到一次。SSD 越做越大&#xff0c;C 盘照样说红就红&#xff0c;D 盘也经不起游戏、虚拟机、无脑缓存的轮番折腾。每次想清理&#xff0c;打开资源管理器一层层点进文件夹&#xff0c;想看大小还得右键属性&#xff0c;几…

作者头像 李华
网站建设 2026/10/1 4:51:50

隐私手机号分享功能的设计与实现:从需求到实战

1. 需求拆解&#xff1a;用户要的是“分享”&#xff0c;不是“公开”1.1 先说清楚这个需求从哪来3月中旬接了一个需求&#xff0c;原型页面上写得很简单&#xff1a;用户A可以把手机号分享给用户B&#xff0c;B能看到A的联系方式并主动联系。产品经理的原话是“就一个分享功能…

作者头像 李华
网站建设 2026/10/1 4:51:42

SpringBoot社区老人健康管理系统:从数据库到部署全解析

最近不少同学在找“社区老人健康管理系统”这类毕设项目&#xff0c;网上能搜到的代码仓库看起来不少&#xff0c;但真正结构清楚、能跑通、还能附带完整文档的&#xff0c;确实需要挑一挑。我这里说的这套&#xff0c;是基于SpringBoot的社区老人健康管理系统&#xff0c;我本…

作者头像 李华