10、生成库文件:add_library创建静态库(.a/.lib)和动态库(.so/.dll/.dylib)
说到库文件,这几乎是每个C++项目都绕不开的话题。我刚开始用CMake那会儿,也搞不清楚静态库和动态库到底该怎么选,更别提跨平台时那些.a、.so、.dll后缀名了。今天咱们就把它彻底讲明白。
10.1 库文件是什么?为什么需要它?
说白了,库文件就是一堆编译好的目标文件的集合。你写了很多功能函数,不想每次都重新编译,也不想把源码全扔给用户,那就打包成库。
我个人习惯把项目拆成三层:
- 可执行文件:main函数所在的地方
- 库文件:核心业务逻辑、算法、工具函数
- 第三方依赖:别人写好的库
这样做的好处很明显——编译快、复用性强、代码更干净。
10.2 静态库 vs 动态库:一张表说清楚
| 对比项 | 静态库 (.a / .lib) | 动态库 (.so / .dll / .dylib) |
|---|---|---|
| 链接时机 | 编译时直接嵌入可执行文件 | 运行时加载 |
| 文件大小 | 可执行文件变大 | 可执行文件小,库单独存在 |
| 部署 | 单个文件,省心 | 需要带上库文件,容易丢 |
| 更新 | 必须重新编译整个程序 | 替换库文件即可 |
| 内存占用 | 每个进程都有一份拷贝 | 多个进程可共享同一份 |
嗯,这里要注意:静态库虽然部署方便,但如果你有10个程序都用同一个静态库,那内存里就有10份拷贝。动态库则只加载一次。
10.3 add_library 的基本用法
CMake里创建库文件,核心命令就是 add_library。语法很简单:
add_library(<目标名> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
<源文件列表>)
举个例子,假设我有一个数学工具库:
# 创建静态库
add_library(math_utils STATIC
src/add.cpp
src/sub.cpp
src/mul.cpp
src/div.cpp
)
# 创建动态库
add_library(math_utils_shared SHARED
src/add.cpp
src/sub.cpp
src/mul.cpp
src/div.cpp
)
你看,就一行命令,CMake会自动帮你处理不同平台的后缀名。在Linux上生成libmath_utils.a和libmath_utils_shared.so,在Windows上生成math_utils.lib和math_utils_shared.dll。
10.4 实战:一个完整的库项目
咱们来写一个完整的例子。假设项目结构是这样的:
my_math_lib/
├── CMakeLists.txt
├── include/
│ └── math_utils.h
└── src/
├── add.cpp
├── sub.cpp
└── main.cpp (测试用)
顶层CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(MyMathLib VERSION 1.0.0 LANGUAGES CXX)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 头文件路径
target_include_directories(my_math_lib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# 创建静态库
add_library(my_math_lib STATIC
src/add.cpp
src/sub.cpp
)
# 创建可执行文件用于测试
add_executable(math_test
src/main.cpp
)
# 链接库
target_link_libraries(math_test PRIVATE my_math_lib)
头文件 include/math_utils.h:
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
namespace math_utils {
int add(int a, int b);
int sub(int a, int b);
}
#endif
源文件 src/add.cpp:
#include "math_utils.h"
namespace math_utils {
int add(int a, int b) {
return a + b;
}
}
嗯,这里有个细节:target_include_directories 用 PUBLIC 关键字,意思是这个头文件路径不仅自己用,别人链接这个库时也能找到。如果你用 PRIVATE,那只有库内部能引用。
10.5 动态库的特殊处理
动态库比静态库多了一些讲究。最典型的就是导出符号的问题。
在Windows上,动态库的符号默认是不导出的。你得用 __declspec(dllexport) 和 __declspec(dllimport) 显式声明。我建议用宏来封装:
#ifdef _WIN32
#ifdef MATH_UTILS_EXPORTS
#define MATH_UTILS_API __declspec(dllexport)
#else
#define MATH_UTILS_API __declspec(dllimport)
#endif
#else
#define MATH_UTILS_API
#endif
namespace math_utils {
MATH_UTILS_API int add(int a, int b);
MATH_UTILS_API int sub(int a, int b);
}
然后在CMake里加上:
target_compile_definitions(my_math_lib_shared
PRIVATE
MATH_UTILS_EXPORTS
)
这样,编译动态库时自动定义 MATH_UTILS_EXPORTS,导出符号;别人使用时没有这个宏,就变成导入符号。
10.6 库的版本管理
动态库还有个版本号的问题。比如 libfoo.so.1.0.0,这涉及到 SOVERSION 和 VERSION 属性。
add_library(my_math_lib_shared SHARED ...)
set_target_properties(my_math_lib_shared PROPERTIES
VERSION 1.0.0
SOVERSION 1
)
这样生成的文件名会是:
- Linux:
libmy_math_lib_shared.so.1.0.0,同时有软链接libmy_math_lib_shared.so.1和libmy_math_lib_shared.so - macOS:
libmy_math_lib_shared.1.0.0.dylib - Windows: 不支持这种命名方式,忽略即可
我曾经在项目里没设SOVERSION,结果升级库之后,旧程序直接崩溃——因为动态库接口变了。从那以后,我每次发布动态库都会带上版本号。
10.7 知识体系图
下面这张图帮你理清库文件的核心脉络:
10.8 避坑指南
最后分享几个我踩过的坑:
- 静态库链接顺序:我曾经在Linux上遇到链接错误,折腾了半天才发现是库的顺序问题。GCC链接器是从右向左解析符号的,所以被依赖的库要放在右边。CMake的
target_link_libraries会自动处理,但如果你手动写gcc命令,一定要注意。 - 动态库的RPATH:编译出来的动态库,运行时找不到是常事。我建议在CMake里设置
CMAKE_INSTALL_RPATH,或者用set(CMAKE_BUILD_RPATH "$ORIGIN")让程序从自身所在目录找库。 - Windows上的DLL地狱:不同版本的DLL混用会导致程序崩溃。我的做法是给每个版本的动态库加上不同的名字,比如
mylib_v1.dll、mylib_v2.dll,避免覆盖。
总结一下:
- 静态库用
STATIC,动态库用SHARED - 动态库跨平台要处理导出符号和版本号
- 头文件路径用
PUBLIC还是PRIVATE,取决于是否想让外部看到 - 链接顺序和RPATH是常见的坑,提前设好能省很多事
好了,关于库文件的创建就讲到这里。你可以在自己的项目里试试,先创建一个静态库,再改成动态库,感受一下两者的区别。动手实践永远是最好的学习方式。