CMake制作动态库与静态库对比

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


一、静态库无需导出符号的核心原因

底层原理

  1. 静态库本质
    静态库是编译后的目标文件(.obj)的打包归档,它本身不是可独立运行的程序。

  2. 链接机制
    生成可执行文件(.exe)时,链接器会将静态库中被用到的代码/数据直接复制到最终的 .exe 内部 ,属于静态链接

  3. 符号可见性
    Windows 平台下,静态库的全局函数、变量默认全部公开可见 ,链接器可以直接解析所有符号,不需要 __declspec(dllexport) 这类导出修饰符

  4. 关键区分:两种 .lib 文件
    VS 中会出现两种后缀都是 .lib 的文件,作用完全不同:

    库类型 文件说明 链接阶段行为 运行时依赖
    静态库 纯代码归档文件 代码复制进 .exe 无额外依赖
    动态库配套 导入库(索引文件) 仅记录符号地址,不复制代码 必须依赖 .dll

二、动态库(DLL):头文件不写导出符号不行

核心规则

Windows 平台的 DLL(动态链接库) 有严格的符号隔离机制:
默认情况下,DLL 内的所有全局符号都是隐藏的,不会自动暴露给外部程序

如果不做显式导出声明,会直接导致:

  1. DLL 编译成功,但不会生成导出表
  2. 外部程序链接时,报 无法解析的外部符号 错误;
  3. 无法调用 DLL 中的函数。

补充对比:Linux/macOS 的动态库(.so/.dylib)默认导出所有符号,无需手动声明;但 Windows 为了性能和安全性,强制要求显式导出,这是平台特性差异。

动态库导出符号的两种标准方案

方案1:使用 __declspec 修饰符(VS 最常用、推荐)

配合宏封装,实现编译DLL时导出、调用DLL时导入的自动切换,这是工业级标准写法。

方案2:使用模块定义文件(.def

无需修改代码,通过配置文件声明导出符号,适合兼容老项目。


三、完整示例:基于加法函数的动态库实现

基于你之前的代码,改造为Windows 动态库标准格式,适配 VS + CMake:

1. 目录结构

复制代码
cmake_demo/
├── include/
│   └── lib.h      # 带导出宏的头文件
├── src/
│   ├── lib.cpp    # 动态库实现
│   └── main.cpp   # 调用方程序
└── CMakeLists.txt

2. 核心代码修改

include/lib.h(带导出/导入宏封装)

为了兼容 C++ 名字粉碎问题,添加 extern "C";通过宏区分编译库使用库的场景:

c 复制代码
#ifndef LIB_H
#define LIB_H

// 宏定义:编译DLL时,MYLIB_EXPORT 等价于 __declspec(dllexport)
// 调用方使用时,MYLIB_EXPORT 等价于 __declspec(dllimport)
#ifdef _WIN32
    #ifdef MYLIB_SHARED_EXPORTS
        #define MYLIB_API __declspec(dllexport)
    #else
        #define MYLIB_API __declspec(dllimport)
    #endif
#else
    // 非Windows平台,无需导出修饰符
    #define MYLIB_API
#endif

// 兼容C调用,防止C++名字改编
extern "C" {
    // 修饰函数:标记为导出/导入符号
    MYLIB_API int add(int a, int b);
}

#endif // LIB_H

src/lib.cpp(实现文件,无需修改修饰符)

c 复制代码
#include "lib.h"

// 实现函数,无需额外修饰
int add(int a, int b) {
    return a + b;
}

src/main.cpp(调用方代码,无修改)

c 复制代码
#include <stdio.h>
#include "lib.h"

int main() {
    int res = add(10, 20);
    printf("10 + 20 = %d\n", res);
    return 0;
}

3. CMakeLists.txt(编译动态库)

关键:用 SHARED 声明动态库,CMake 会自动定义 MYLIB_SHARED_EXPORTS 宏:

cmake 复制代码
cmake_minimum_required(VERSION 3.15)
project(AddDllDemo)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 核心:SHARED 表示编译为 Windows 动态库(DLL)
add_library(mylib SHARED src/lib.cpp)
target_include_directories(mylib PUBLIC include)

# 生成可执行文件
add_executable(main_app src/main.cpp)
# 链接动态库
target_link_libraries(main_app PRIVATE mylib)

4. 编译与运行(VS 环境)

  1. 用 CMake 生成 VS 解决方案,编译工程;
  2. 输出目录会生成两个关键文件:mylib.dll(运行时库)、mylib.lib(导入库);
  3. 直接运行 main_app.exe,可正常调用 DLL 中的 add 函数。

5. 测试「不写导出符号」的失败场景

如果删除头文件中的 MYLIB_API 修饰符,直接声明函数:

c 复制代码
// 错误写法:无导出修饰符
extern "C" int add(int a, int b);

编译后:

  • DLL 可正常生成,但无导出符号
  • 链接 main_app 时,VS 直接报错:无法解析的外部符号 add,程序无法运行。

补充方案:使用 .def 文件导出符号

如果不想修改代码,可以新建 mylib.def 文件,手动声明导出符号:

def 复制代码
LIBRARY mylib
EXPORTS
    add @1

在 CMake 中添加配置:

cmake 复制代码
target_sources(mylib PRIVATE mylib.def)

此时代码中无需任何导出修饰符 ,链接器会根据 .def 文件导出符号,同样可以正常调用。


四、关键补充说明

1. 关于 extern "C" 的作用

C++ 编译器会对函数名做名字粉碎(Name Mangling) ,导致外部调用时符号不匹配;

添加 extern "C" 会强制使用 C 语言的符号命名规则,保证跨调用约定兼容。

2. 动态库运行依赖

静态库编译后,.exe 可独立运行;

动态库编译后,必须将 xxx.dll 放在 .exe 同级目录,否则运行时会报错「找不到依赖库」。

3. CMake 自动宏的特性

当你用 add_library(xxx SHARED ...) 时,CMake 会自动生成 XXX_EXPORTS ,这也是我们封装 MYLIB_API 的依据,无需手动定义。


总结

  1. 静态库(.lib :无需任何导出符号,全局符号默认公开,链接时代码直接嵌入 .exe,这是你能直接调用函数的原因;
  2. Windows 动态库(.dll必须显式导出符号 ,不写导出修饰符会导致链接失败,推荐用 __declspec(dllexport/dllimport) + 宏封装的标准写法;
  3. 两种 .lib 文件切勿混淆:静态库是代码归档,动态库配套的 .lib 仅为符号索引(导入库)。
相关推荐
wWYy.2 小时前
C++—集群聊天室(3)CMake详解
开发语言·c++
在路上看风景2 小时前
16. 指针和引用的区别
c++
兩尛2 小时前
45. 跳跃游戏 II
c++·算法·游戏
睡一觉就好了。2 小时前
C++ 容器
开发语言·c++
怪谈爱好者CMZ2 小时前
【C++学习笔记】图论-最短路径Dijkstra算法
c++·笔记·学习
tangchao340勤奋的老年?2 小时前
ADS通信 C++ 设置通知方式读取指定变量
开发语言·c++·算法
落羽的落羽2 小时前
【Linux系统】从零实现一个简易的shell!
android·java·linux·服务器·c++·人工智能·机器学习
Trouvaille ~2 小时前
【Linux】UDP Socket编程实战(三):多线程聊天室与线程安全
linux·服务器·网络·c++·安全·udp·socket
jiang_changsheng3 小时前
MCP协议的核心架构基础
c语言·开发语言·c++·python·comfyui