提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 一、静态库无需导出符号的核心原因
- 二、动态库(DLL):头文件不写导出符号**不行**
-
- 核心规则
- 动态库导出符号的两种标准方案
-
- [方案1:使用 `__declspec` 修饰符(VS 最常用、推荐)](#方案1:使用
__declspec修饰符(VS 最常用、推荐)) - 方案2:使用模块定义文件(`.def`)
- [方案1:使用 `__declspec` 修饰符(VS 最常用、推荐)](#方案1:使用
- 三、完整示例:基于加法函数的动态库实现
-
- [1. 目录结构](#1. 目录结构)
- [2. 核心代码修改](#2. 核心代码修改)
- [3. CMakeLists.txt(编译动态库)](#3. CMakeLists.txt(编译动态库))
- [4. 编译与运行(VS 环境)](#4. 编译与运行(VS 环境))
- [5. 测试「不写导出符号」的失败场景](#5. 测试「不写导出符号」的失败场景)
- [补充方案:使用 `.def` 文件导出符号](#补充方案:使用
.def文件导出符号)
- 四、关键补充说明
-
- [1. 关于 `extern "C"` 的作用](#1. 关于
extern "C"的作用) - [2. 动态库运行依赖](#2. 动态库运行依赖)
- [3. CMake 自动宏的特性](#3. CMake 自动宏的特性)
- [1. 关于 `extern "C"` 的作用](#1. 关于
- 总结
一、静态库无需导出符号的核心原因
底层原理
-
静态库本质
静态库是编译后的目标文件(.obj)的打包归档,它本身不是可独立运行的程序。 -
链接机制
生成可执行文件(.exe)时,链接器会将静态库中被用到的代码/数据直接复制到最终的.exe内部 ,属于静态链接。 -
符号可见性
Windows 平台下,静态库的全局函数、变量默认全部公开可见 ,链接器可以直接解析所有符号,不需要__declspec(dllexport)这类导出修饰符。 -
关键区分:两种
.lib文件
VS 中会出现两种后缀都是.lib的文件,作用完全不同:库类型 文件说明 链接阶段行为 运行时依赖 静态库 纯代码归档文件 代码复制进 .exe无额外依赖 动态库配套 导入库(索引文件) 仅记录符号地址,不复制代码 必须依赖 .dll
二、动态库(DLL):头文件不写导出符号不行
核心规则
Windows 平台的 DLL(动态链接库) 有严格的符号隔离机制:
默认情况下,DLL 内的所有全局符号都是隐藏的,不会自动暴露给外部程序 。
如果不做显式导出声明,会直接导致:
- DLL 编译成功,但不会生成导出表;
- 外部程序链接时,报 无法解析的外部符号 错误;
- 无法调用 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 环境)
- 用 CMake 生成 VS 解决方案,编译工程;
- 输出目录会生成两个关键文件:
mylib.dll(运行时库)、mylib.lib(导入库); - 直接运行
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 的依据,无需手动定义。
总结
- 静态库(
.lib) :无需任何导出符号,全局符号默认公开,链接时代码直接嵌入.exe,这是你能直接调用函数的原因; - Windows 动态库(
.dll) :必须显式导出符号 ,不写导出修饰符会导致链接失败,推荐用__declspec(dllexport/dllimport)+ 宏封装的标准写法; - 两种
.lib文件切勿混淆:静态库是代码归档,动态库配套的.lib仅为符号索引(导入库)。