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。

小技巧:如果不指定STATIC或SHARED,默认是STATIC。我个人建议显式写清楚,避免别人看代码时猜来猜去。

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_directoriesPUBLIC 关键字,意思是这个头文件路径不仅自己用,别人链接这个库时也能找到。如果你用 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,导出符号;别人使用时没有这个宏,就变成导入符号。

注意:Linux和macOS上不需要这些宏,所有非静态函数默认都是导出的。但为了跨平台,我建议还是加上。

10.6 库的版本管理

动态库还有个版本号的问题。比如 libfoo.so.1.0.0,这涉及到 SOVERSIONVERSION 属性。

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.1libmy_math_lib_shared.so
  • macOS: libmy_math_lib_shared.1.0.0.dylib
  • Windows: 不支持这种命名方式,忽略即可

我曾经在项目里没设SOVERSION,结果升级库之后,旧程序直接崩溃——因为动态库接口变了。从那以后,我每次发布动态库都会带上版本号。

10.7 知识体系图

下面这张图帮你理清库文件的核心脉络:

CMake 库文件知识体系 add_library STATIC 静态库 SHARED 动态库 编译时嵌入可执行文件 生成 .a (Linux) / .lib (Windows) 运行时加载,可替换 生成 .so (Linux) / .dll (Windows) / .dylib (macOS) 需处理导出符号 + 版本号 核心命令:add_library(目标名 STATIC/SHARED 源文件列表)

10.8 避坑指南

最后分享几个我踩过的坑:

  • 静态库链接顺序:我曾经在Linux上遇到链接错误,折腾了半天才发现是库的顺序问题。GCC链接器是从右向左解析符号的,所以被依赖的库要放在右边。CMake的 target_link_libraries 会自动处理,但如果你手动写gcc命令,一定要注意。
  • 动态库的RPATH:编译出来的动态库,运行时找不到是常事。我建议在CMake里设置 CMAKE_INSTALL_RPATH,或者用 set(CMAKE_BUILD_RPATH "$ORIGIN") 让程序从自身所在目录找库。
  • Windows上的DLL地狱:不同版本的DLL混用会导致程序崩溃。我的做法是给每个版本的动态库加上不同的名字,比如 mylib_v1.dllmylib_v2.dll,避免覆盖。

总结一下:

  • 静态库用 STATIC,动态库用 SHARED
  • 动态库跨平台要处理导出符号和版本号
  • 头文件路径用 PUBLIC 还是 PRIVATE,取决于是否想让外部看到
  • 链接顺序和RPATH是常见的坑,提前设好能省很多事

好了,关于库文件的创建就讲到这里。你可以在自己的项目里试试,先创建一个静态库,再改成动态库,感受一下两者的区别。动手实践永远是最好的学习方式。

公众号:蓝海资料掘金营,微信 deep3321