21、CMake实战:单文件项目、多文件项目、库项目(静态库与动态库)
说实话,CMake 这东西,我刚开始接触的时候也觉得挺烦的。明明 Makefile 写得挺顺手,为什么非要再学一套?但后来项目越做越大,跨平台需求一上来,我才发现——嗯,Makefile 那套手写方式,真的扛不住。
今天这一讲,咱们就踏踏实实地把 CMake 的三种典型项目结构走一遍。单文件、多文件、还有库项目。你跟着我敲一遍,基本就能应付日常 80% 的场景了。
21.1 单文件项目:Hello World 的 CMake 写法
先来个最简单的。你有一个 main.c,想编译成可执行文件。用 CMake 怎么写?
项目结构长这样:
hello/
├── CMakeLists.txt
└── main.c
main.c 的内容我就不贴了,就是个打印 "Hello CMake" 的简单程序。关键看 CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(hello VERSION 1.0 LANGUAGES C)
add_executable(hello main.c)
三行搞定。我个人的习惯是,cmake_minimum_required 尽量写一个你实际测试过的版本,别写太老也别写太新。3.10 是个比较稳妥的选择。
编译步骤:
mkdir build && cd build
cmake ..
make
你想想看,比起手写 Makefile,是不是清爽多了?而且 CMake 会自动检测你的编译器、链接器,跨平台的时候特别省心。
21.2 多文件项目:模块化组织代码
实际项目里,没人会把所有代码塞进一个文件。咱们来看一个稍微复杂点的结构:
math_tool/
├── CMakeLists.txt
├── main.c
├── add.c
├── add.h
├── sub.c
└── sub.h
CMakeLists.txt 怎么写?
cmake_minimum_required(VERSION 3.10)
project(math_tool VERSION 1.0 LANGUAGES C)
# 收集所有源文件
set(SOURCES
main.c
add.c
sub.c
)
add_executable(math_tool ${SOURCES})
这里我用 set(SOURCES ...) 把源文件列出来。你可能觉得手动列文件有点傻,但说实话,小项目这样写最清晰。我在项目中见过有人用 file(GLOB ...) 自动收集,结果新加文件忘了重新 cmake,折腾了半天才发现——嗯,手动列其实更靠谱。
头文件不需要列在 add_executable 里,CMake 会自动扫描依赖。但如果你想让头文件出现在 IDE 的项目视图中,可以这样:
add_executable(math_tool ${SOURCES} add.h sub.h)
这样在 Visual Studio 或 CLion 里,头文件也会显示出来,方便编辑。
21.3 库项目:静态库与动态库
这才是重头戏。很多初学者搞不清静态库和动态库的区别,我简单说两句:
- 静态库(.a / .lib):编译时直接嵌入到可执行文件里。优点是部署简单,缺点是文件大,更新库要重新编译。
- 动态库(.so / .dll):运行时加载。优点是节省空间、方便更新,缺点是部署时要带上库文件,搞不好还会出现「找不到动态库」的经典报错。
我曾经在一个嵌入式项目里吃过动态库的亏。交叉编译出来的 .so 版本不对,板子上跑起来直接段错误。查了两天才发现是库的依赖链没处理好。从那以后,嵌入式项目我优先用静态库,除非有明确的动态加载需求。
21.3.1 构建静态库
项目结构:
mylib/
├── CMakeLists.txt
├── src/
│ ├── mylib.c
│ └── mylib.h
└── main.c
CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(mylib VERSION 1.0 LANGUAGES C)
# 构建静态库
add_library(mylib STATIC src/mylib.c)
# 构建可执行文件,并链接静态库
add_executable(app main.c)
target_link_libraries(app PRIVATE mylib)
注意 add_library 的第二个参数 STATIC,明确告诉 CMake 我们要生成静态库。编译后你会得到 libmylib.a(Linux)或 mylib.lib(Windows)。
21.3.2 构建动态库
把 STATIC 换成 SHARED 就行:
add_library(mylib SHARED src/mylib.c)
就这么简单。但动态库有个坑——导出符号。在 Windows 上,你需要用 __declspec(dllexport) 标记要导出的函数。Linux 下默认全部导出,但如果你用了 -fvisibility=hidden,也得手动标记。
CMake 里可以这样控制:
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN 1)
然后在代码里用宏控制导出:
#if defined(_WIN32) || defined(_WIN64)
#define MYLIB_EXPORT __declspec(dllexport)
#else
#define MYLIB_EXPORT __attribute__((visibility("default")))
#endif
MYLIB_EXPORT void mylib_hello(void);
嗯,这部分确实有点繁琐,但跨平台库都得这么搞。我建议你把这个宏定义放到一个单独的 export.h 头文件里,每个需要导出的函数都包一下。一劳永逸。
21.4 知识体系总览
下面这张图,是我梳理的本章核心逻辑。你看一眼,心里就有数了:
21.5 几个实用技巧
最后分享几个我踩过的坑,你遇到了能少走弯路:
在 Linux 下,链接库的顺序很重要。比如你的 app 依赖 libA,libA 依赖 libB,那么链接顺序应该是:
target_link_libraries(app PRIVATE A B)
如果写成 B A,可能会报未定义引用。CMake 的 target_link_libraries 会自动处理依赖顺序,但如果你手写 Makefile 或者用其他构建系统,这个坑很容易踩。
编译出来的动态库,运行时怎么找到?Linux 下可以用 RPATH 或 RUNPATH。CMake 里设置:
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")
$ORIGIN 表示可执行文件所在目录。这样你把可执行文件和 lib 目录一起分发,就能直接运行,不用设置 LD_LIBRARY_PATH。
CMake 支持多配置。调试时加调试符号,发布时优化:
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake -DCMAKE_BUILD_TYPE=Release ..
我个人的习惯是,开发阶段一直用 Debug,准备发布前切到 Release 跑一遍完整测试。有些 bug 只在 Release 模式下出现,比如未初始化的变量——优化器一开,内存布局变了,问题就暴露了。
21.6 总结
这一章的内容,说白了就是 CMake 的三种基本项目骨架。单文件练手,多文件组织模块,库项目实现复用。你把这三种结构吃透了,以后遇到再大的项目,无非就是在这个基础上加目录、加子模块、加第三方依赖。
嗯,CMake 的学习曲线确实有点陡,但一旦上手,你会发现它比手写 Makefile 高效太多了。下一讲我们会深入 CMake 的变量、函数和自定义命令,到时候你就能写出更灵活的构建脚本了。
公众号:蓝海资料掘金营,微信 deep3321