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 仅为符号索引(导入库)。
相关推荐
oem1101 天前
C++中的享元模式实战
开发语言·c++·算法
casual~1 天前
第?个质数(埃氏筛算法)
数据结构·c++·算法
Elnaij1 天前
从C++开始的编程生活(20)——AVL树
开发语言·c++
hanbr1 天前
【C++ STL核心】vector:最常用的动态数组容器(第九天核心)
开发语言·c++
仰泳的熊猫1 天前
题目2308:蓝桥杯2019年第十届省赛真题-旋转
数据结构·c++·算法·蓝桥杯
lzksword1 天前
C++ Builder XE OpenDialog1打开多文件并显示xls与xlsx二种格式文件
java·前端·c++
niceffking1 天前
C++内部类的ISO约定和语法细节
开发语言·c++
艾莉丝努力练剑1 天前
【脉脉】AI创作者崛起:掌握核心工具,在AMA互动中共同成长
运维·服务器·c++·人工智能·安全·企业·脉脉
码界奇点1 天前
基于ASP.NET Core的内容管理系统设计与实现
c++·后端·车载系统·毕业设计·asp.net·源代码管理
2401_883035461 天前
C++20概念(Concepts)入门指南
开发语言·c++·算法