第三节:CMake 工程实践场景笔记
1. 基础场景:可执行文件 (Executable)
这是最简单的场景,目标是编译出一个可以直接运行的程序(如 hello.exe 或 hello),并把它安装到系统目录。
1.1 核心流程
- 定义版本与项目:规定最低版本,给项目起名。
- 生成目标:告诉 CMake 我要造一个可执行程序。
- 安装 :定义编译好的程序要去哪里(比如
/usr/local/bin)。
1.2 关键代码模板
cmake
cmake_minimum_required(VERSION 3.18)
project(InstallHello VERSION 1.0.0 LANGUAGES CXX)
# 1. 生成可执行文件
# 语法:add_executable(名字 源文件)
add_executable(hello main.cpp)
# 2. 安装配置 (重要!)
# 引入 GNU 标准安装目录模块,方便跨平台兼容
include(GNUInstallDirs)
# 语法:install(TARGETS 目标名 ...)
# 这里的 hello 就是上面定义的目标
install(TARGETS hello)
# 默认安装位置:Linux 下通常是 /usr/local/bin
1.3 常用命令与变量笔记
cmake --install .:这是 CMake 3.15+ 推荐的安装命令,比传统的make install更通用。CMAKE_INSTALL_PREFIX:控制安装的根目录。- 默认是
/usr/local。 - 修改方法:
cmake -DCMAKE_INSTALL_PREFIX=/my/custom/path ..
- 默认是
message(STATUS "内容"):类似printf,用于构建过程中打印日志,调试变量很有用。
2. 进阶场景:静态库 (Static Library)
在大型项目中,我们通常把通用功能(如数学计算、网络请求)封装成库,供主程序调用。静态库在编译时会将代码"拷贝"进主程序,生成的可执行文件比较大,但运行时不需要依赖外部文件。
2.1 目录结构管理 (add_subdirectory)
不要把所有文件堆在一个目录。通常做法是:
app/:放主程序代码 (main.cpp)my_lib/:放库代码 (add.cpp, sub.cpp)
在根目录的 CMakeLists.txt 中使用 add_subdirectory(my_lib),CMake 就会自动进入子目录处理那个文件夹下的 CMakeLists.txt。
2.2 生成静态库
在 my_lib/CMakeLists.txt 中:
cmake
# 收集源文件
file(GLOB SRC_LISTS "src/*.cpp")
# 生成静态库
# 语法:add_library(库名 STATIC 源文件)
# STATIC 表示静态库 (Linux下生成 .a, Windows下生成 .lib)
add_library(MyMath STATIC ${SRC_LISTS})
2.3 链接静态库 (核心难点)
这是 CMake 的精髓------Target-based(基于目标) 的管理方式。
在 app/CMakeLists.txt 中:
cmake
add_executable(main main.cpp)
# 链接库:告诉 main 程序,你需要用到 MyMath 这个库
target_link_libraries(main PRIVATE MyMath)
问题 :编译器怎么知道库的头文件在哪里?
旧方法 :用 include_directories(全局污染,不推荐)。
新方法:让库自己告诉使用者,"我的头文件在哪里"。
在库的 CMakeLists.txt 中添加:
cmake
# INTERFACE 意思是:我自己编译不需要这个路径,但谁链接我,谁就得包含这个路径
target_include_directories(MyMath PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
这样,只要 main 链接了 MyMath,CMake 会自动把 MyMath 的头文件路径加给 main。
3. 核心概念:Public, Private, Interface
这三个关键字决定了属性(头文件路径、编译选项、宏定义)的传播范围。通俗理解:
- PRIVATE (私有) :
- 含义:我自己用,不告诉别人。
- 例子:我做面包(构建库)需要用烤箱,但吃面包的人(链接库的人)不需要知道烤箱的存在。
- INTERFACE (接口) :
- 含义:我自己不用,但别人用我时必须得有。
- 例子:我卖给你个电器,我自己不插电,但你用的时候必须找个插座(头文件)。纯头文件库常用这个。
- PUBLIC (公开) :
- 含义:我自己用,别人用我时也得用。
- 例子:传染病。我有,接触我的人也会有。
4. 进阶场景:动态库 (Shared Library)
动态库(Shared Library)在程序运行时才被加载。Linux 下是 .so,Windows 下是 .dll。
4.1 生成动态库
只需将 STATIC 改为 SHARED:
cmake
add_library(MyMath SHARED ${SRC_LISTS})
4.2 动态库的坑:RPATH
编译能通过,运行却报错 cannot open shared object file?
这是因为程序在运行时 找不到 .so 文件在哪里。静态库都塞进 exe 里了所以没这问题。
解决方法 :需要设置 RPATH (Run-time Search Path)。
CMake 默认在 build 目录下会自动设置 RPATH,但在安装(install)后会由系统把 RPATH 抹除。如果需要发布,通常要配置 RPATH 相关的变量。
4.3 查看链接情况
- ldd 命令 :
ldd ./main- 查看可执行程序依赖了哪些动态库,以及是否找到(not found)。
- readelf 命令 :
readelf -d ./main- 查看二进制文件头部的 RPATH 信息。
5. 库的安装与导出 (Export)
如果要让自己写的库像 OpenCV 或 Boost 一样,能被别人的项目通过 find_package(MyMath) 找到,需要做"导出"工作。这部分比较繁琐,是专业库开发的标准动作。
5.1 安装流程
- 安装文件:库文件 (.a/.so) 和 头文件 (.h)。
- 生成导出文件 :
install(EXPORT ...)。这会生成一个.cmake文件,里面记录了库的绝对路径、依赖关系等信息。 - 生成版本配置 :
write_basic_package_version_file。让别人能检查版本兼容性。 - 生成核心配置 :
configure_package_config_file。生成MyMathConfig.cmake。
别人使用时:
cmake
# 在别人的 CMakeLists.txt 中
find_package(MyMath REQUIRED) # CMake 会去系统目录找 MyMathConfig.cmake
target_link_libraries(UserApp PRIVATE MyMath::MyMath)
6. 常用辅助命令笔记
6.1 file 命令
用于文件操作。
file(GLOB 变量 "src/*.cpp"):傻瓜式搜索目录下所有 cpp 文件赋值给变量。- 缺点:如果新建了文件,不重新运行 cmake 这种通配符可能扫描不到。
6.2 set_target_properties
这是 CMake 的"万能修改器"。可以修改目标的各种属性。
- 修改输出目录 :
ARCHIVE_OUTPUT_DIRECTORY(静态库位置),LIBRARY_OUTPUT_DIRECTORY(动态库位置),RUNTIME_OUTPUT_DIRECTORY(exe位置)。 - 修改输出名字 :
OUTPUT_NAME(比如把libMyMath.so改名为libmath.so)。 - 设置版本 :
VERSION和SOVERSION(生成libfoo.so.1.2这种带版本号的库)。
6.3 Generator Expressions (生成器表达式)
长得像 $<...> 的东西。
- 作用 :在 配置阶段 不知道具体值,等到 生成构建系统阶段 才计算出的值。
- 常用 :
$<TARGET_FILE:MyMath>。- 含义:我不关心
MyMath到底编译到哪个文件夹了,也不关心它是 .a 还是 .so,总之把那个最终文件的绝对路径填在这里。
- 含义:我不关心
7. 总结回顾
- 编译 exe 用
add_executable。 - 编译库 用
add_library(分 STATIC 和 SHARED)。 - 建立依赖 永远用
target_link_libraries。 - 管理头文件 永远用
target_include_directories。 - Scope 很重要:Private 自己用,Public 大家用,Interface 别人用。
- 安装与导出 :
install只是复制文件,但配合EXPORT可以生成专业的 SDK 配置,支持find_package。