尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
浅析C/C++中动态链接库的创建和调用
前言动态链接库dynamic link libraryDLL是 Windows 上把可执行代码拆分成独立模块的机制。它的价值在于多个程序可以共享同一份代码、库可以独立升级而不必重新链接主程序、插件式架构可以在运行期决定加载哪个模块。Linux 和 macOS 上对应的概念是共享对象shared object.so与动态库.dylib机制相似但细节不同本文以 Windows 为主线并在需要时指出 POSIX 平台的差异。初学者最常见的误解有三个。第一以为把.dll文件复制到可执行文件旁边就一定能被加载——实际加载还受依赖项、位数、运行库版本和搜索路径影响。第二以为 C 函数导出后还能按原名字调用——C 有名字修饰name mangling导出名往往不是源码里写的那个。第三把「动态链接」和「运行时用LoadLibrary加载」混为一谈其实前者是链接器生成的导入表机制后者是显式的运行时 API两者用途不同。本文先讲动态链接的模型再分别给出创建 DLL、隐式调用、显式调用的完整写法最后集中讨论跨模块边界时真正容易出问题的地方ABI、CRT、异常和内存所有权。文中的 Windows 专有写法都标注了平台标准 C 部分则以 C17 为基准。一、动态链接的基本模型一个 Windows 可执行文件PE 格式在加载时由系统加载器解析导入表import table。表中的每一项记录「需要哪个 DLL 的哪个导出符号」。链接器在构建可执行文件时需要两样东西声明函数原型的头文件以及一个导入库import library扩展名也是.lib但内容和静态库不同里面只有跳转桩。真正的代码在.dll里运行时才被映射进进程地址空间。这一点解释了为什么「链接成功」和「运行成功」是两件事导入库只保证链接期能找到符号运行期找不到 DLL 或找不到对应导出时程序会在启动阶段就失败而不是在调用点失败隐式链接的情形。对比项静态库.lib动态库.dll 导入库.lib代码归属链接时复制进可执行文件独立文件运行时映射可执行文件体积较大较小库升级必须重新链接替换 DLL 即可前提是 ABI 不变部署只需一个 exeexe 加若干 DLL 及其依赖加载失败时机不会发生隐式链接在进程启动时失败多进程共享每个进程各一份代码段可共享数据段写时复制需要注意的是DLL 的「共享」仅指物理内存中的代码页可以被多个进程映射每个进程仍有自己的数据段和全局变量副本。不要指望用 DLL 里的全局变量做跨进程通信。导出符号有两种描述方式在源码里写__declspec(dllexport)或者提供一个模块定义文件.def。后者列出导出名可以避免名字修饰带来的不确定性适合需要稳定导出名的场景。导入端则用__declspec(dllimport)它让编译器生成更直接的调用序列对普通函数漏写dllimport通常也能链接成功但对导出数据或某些优化场景就可能出问题因此成对使用是更稳妥的做法。二、创建 DLLWindows 平台下面是一个最小但完整的例子头文件用宏在「构建 DLL」和「使用 DLL」两种场景间切换并用extern C抑制 C 名字修饰。extern C只影响链接名不影响函数体内部使用 C 特性。// 适用于 C17仅限 WindowsMSVC 或 MinGW// mathlib.h#pragma once#ifdef MATHLIB_EXPORTS#define MATHLIB_API __declspec(dllexport)#else#define MATHLIB_API __declspec(dllimport)#endifextern C {MATHLIB_API int add(int a, int b);MATHLIB_API double average(const double* data, int count);}实现文件在编译时定义MATHLIB_EXPORTS于是同一个头文件展开成导出声明// 适用于 C17仅限 Windows// mathlib.cpp#define MATHLIB_EXPORTS#include mathlib.hint add(int a, int b) {return a b;}double average(const double* data, int count) {if (data nullptr || count 0) {return 0.0;}double sum 0.0;for (int i 0; i count; i) {sum data[i];}return sum / count;}使用 MSVC 时构建命令大致如下/LD表示生成 DLL/EHsc启用标准 C 异常处理cl /nologo /std:c17 /EHsc /LD mathlib.cpp /Fe:mathlib.dll这条命令会同时产出mathlib.dll和导入库mathlib.lib。用 MinGW-w64 时则是g -stdc17 -shared -o mathlib.dll mathlib.cpp -Wl,--out-implib,libmathlib.a如果不想在源码里写__declspec可以改用.def文件LIBRARY mathlibEXPORTSaddaverage.def里的名字必须是链接器看到的最终符号名。对extern C函数来说就是源码里的名字对 C 成员函数来说则是修饰后的名字写起来非常不直观所以导出 C 类时通常还是用__declspec(dllexport)。三、隐式调用与显式调用隐式调用load-time linking的写法与使用静态库几乎一样包含头文件、链接导入库、把 DLL 放到运行时可找到的位置。// 适用于 C17仅限 Windows// main.cpp#include iostream#include vector#include mathlib.hint main() {std::vectordouble values{1.5, 2.5, 3.5, 4.5};std::cout add add(2, 3) \n;std::cout average average(values.data(), static_castint(values.size())) \n;return 0;}链接命令MSVCcl /nologo /std:c17 /EHsc main.cpp mathlib.lib /Fe:app.exe如果mathlib.dll不在搜索路径中程序在启动时会报「找不到 mathlib.dll」。这不是代码问题而是部署问题。显式调用run-time linking不链接导入库而是在运行时用LoadLibrary取得模块句柄再用GetProcAddress取函数地址。它的好处是DLL 缺失时可以走降级逻辑、可以按配置加载不同实现、可以做插件系统。代价是需要手动管理句柄和函数指针类型。// 适用于 C17仅限 Windows#include windows.h#include iostreamint main() {HMODULE module ::LoadLibraryW(Lmathlib.dll);if (module nullptr) {std::cerr load failed, error ::GetLastError() \n;return 1;}using AddFn int (*)(int, int);auto addFn reinterpret_castAddFn(::GetProcAddress(module, add));if (addFn nullptr) {std::cerr symbol not found\n;::FreeLibrary(module);return 1;}std::cout add addFn(2, 3) \n;::FreeLibrary(module);return 0;}几个细节值得强调。GetProcAddress返回FARPROC把它转成具体函数指针类型属于平台约定的做法在 C 标准层面函数指针与对象指针、函数指针之间的reinterpret_cast并不都受保证这里是 Windows API 明确支持的用法。用GetProcAddress取extern C函数时传的是未修饰名取 C 函数时传的是修饰名可以用dumpbin /exports mathlib.dll查看实际导出表。另外LoadLibraryW接受宽字符串源码文件若不是 UTF-8 带 BOM直接写非 ASCII 路径可能出问题稳妥做法是从std::filesystem::path取c_str()。维度隐式调用显式调用链接期依赖需要导入库不需要加载时机进程启动调用LoadLibrary时缺失 DLL 的表现启动失败可捕获并降级函数解析开销加载器一次性完成每次GetProcAddress查表适用场景固定依赖插件、可选功能、延迟加载四、跨模块边界真正要小心的事第一是 ABI。C 标准没有规定名字修饰规则、虚表布局、类对象布局和异常传播机制因此 MSVC 编译的 DLL 与 MinGW 编译的 DLL 通常不能直接互相传递 C 对象。跨编译器、跨版本时最稳的边界是 C 风格接口基本类型、指针、不透明句柄opaque handle。上面的extern C例子就是这种思路。第二是内存所有权。如果 DLL 里new一个对象、主程序里delete两边必须使用同一套堆实现。静态链接 CRT 时每个模块各有一份堆跨模块释放是严重错误动态链接 CRTMSVC 的/MD时共享同一份才相对安全。更稳妥的约定是「谁分配谁释放」并提供配套的销毁函数。第三是 STL 类型跨边界。std::string和std::vector的布局属于实现细节两个模块若用不同标准库版本传值可能崩溃。接口上应传const char*加长度或者传调用方提供的缓冲区返回数据时由调用方传缓冲区进来填充。第四是异常。让异常穿过 DLL 边界需要两边使用兼容的异常实现和运行库。对外接口建议用返回码或noexcept在 DLL 内部捕获所有异常并转换成错误码。第五是 DLL 搜索顺序与安全。直接用LoadLibrary加载裸文件名时系统会按一套搜索顺序查找当前目录可能参与其中攻击者放置同名 DLL 就形成「DLL 预加载」问题。防御手段包括使用绝对路径、用LoadLibraryEx配合LOAD_LIBRARY_SEARCH_APPLICATION_DIR等标志、在进程启动早期调用SetDefaultDllDirectories。这些 API 的具体组合以 Microsoft 官方文档为准。常见坑点❌ 只在源码里写__declspec(dllexport)头文件不区分导出与导入。 ✅ 用MATHLIB_EXPORTS之类的宏在构建期切换dllexport与dllimport保证两端声明一致。❌ 导出 C 函数却不加extern C然后在GetProcAddress里用源码名查找。 ✅ 要么加extern C要么用dumpbin /exports查出修饰名再查需要长期稳定的接口优先选前者。❌ 在 DLL 接口里按值传std::string并假设主程序能正确析构。 ✅ 接口层用const char*与长度或者定义不透明句柄并由 DLL 提供创建、销毁函数。❌ DLL 里malloc主程序里free或者一边静态 CRT、一边动态 CRT。 ✅ 统一运行库设置并遵守「谁分配谁释放」把释放封装进 DLL 导出的函数。❌ 用LoadLibrary(mathlib.dll)依赖当前目录搜索。 ✅ 使用绝对路径或用LoadLibraryEx指定搜索范围避免被同名恶意 DLL 劫持。❌ 显式加载后忘记FreeLibrary或在其他线程仍在使用函数指针时卸载模块。 ✅ 配对调用FreeLibrary并保证卸载发生在所有调用结束之后必要时用引用计数管理生命周期。❌ 把 Linux 的.so构建命令直接套到 Windows 上期待__declspec生效。 ✅ 区分平台Windows 用__declspec(dllexport)GCC/Clang 用__attribute__((visibility(default)))配合-fvisibilityhidden。总结主题关键结论DLL 的本质PE 格式的独立代码模块靠导入表在加载期或运行期解析导出方式__declspec(dllexport)或.def文件C 名字会被修饰调用方式隐式链接需要导入库显式链接用LoadLibrary与GetProcAddress稳定边界跨编译器优先用extern C加基本类型或不透明句柄资源管理统一 CRT遵守「谁分配谁释放」安全避免裸文件名加载控制 DLL 搜索路径动态链接库的核心矛盾是「模块解耦」与「ABI 约束」接口越接近 C 风格跨模块越稳越想直接暴露 C 类型越容易被编译器和标准库版本绑定。设计接口时先想清楚谁能编译这个 DLL、谁会调用它再决定导出粒度比事后排查崩溃要省力得多。
RELATED

相关推荐

数字工厂PPT不是汇报材料,而是可执行的技术契约

数字工厂PPT不是汇报材料,而是可执行的技术契约

简介:本资源是一份面向制造业数字化转型从业者、企业IT规划人员及工业自动化工程师的《数字工厂规划蓝图报告》PPT,系统梳理了数字化工厂从需求分析、蓝图设计到实施落地的全周期方法论。报告共69页,涵盖项目整体进度管理、六大核心专业&…

📅 2026/10/22 17:59:14
浅谈C++ 容器查找效率

浅谈C++ 容器查找效率

前言 「哪个容器查找最快」是 C 里最容易被一句话回答、也最容易被这句话误导的问题。教科书式的答案是「unordered_map 是 O(1),map 是 O(log n),vector 是 O(n)」,但真实程序里的差异往往来自常数因子、内存布局与数据规模,而不…

📅 2026/10/22 17:59:14
6G太赫兹信道建模:从物理约束到可复现CIR生成

6G太赫兹信道建模:从物理约束到可复现CIR生成

简介:本资源是一份面向通信工程高年级本科生、研究生及6G前沿研究者的专业技术综述文档,聚焦6G无线信道建模这一核心基础问题,系统梳理全频谱(毫米波/太赫兹/光波段)、全场景(卫星、无人机、海洋、水声、高…

📅 2026/10/22 17:59:14
MORE NEWS

更多资讯

📰

大数据面试核心考点:HDFS读写、Spark调度与Flink状态机制全解析

去面大数据岗位,不管是校招还是社招,问来问去其实就那么几类问题。早年大家还在背“Hadoop生态有哪些组件”,现在面试官更爱追问原理、场景题和手写SQL。我在一线做了几年数仓和实时计算,也面过不少人,今天就把大数据面…

📰

人工智能、医学工程和教育:医工交叉的闭环逻辑与落地实践

一、这本书的出现,本质上是在解决一场跨学科的对话困境 我有幸在整理电子书库时翻到这本《人工智能、医学工程与教育》,起初以为它只是把三个热门关键词打包在一起的合集,翻了几章后才发现,它实际上是用一整本书的篇幅&#xff0c…

📰

OpenClaw自动记忆引擎:行为触发式知识沉淀系统

1. 项目概述:这不是一个“记忆软件”,而是一套可嵌入、可演化的自动记忆行为引擎“OpenClaw 自动记忆系统设计的技术探讨”——光看标题,很多人第一反应是“又一个笔记AI摘要工具?”但实际完全不是。我接触过这个项目的早期原型后…

📰

Ubuntu 18.04远程访问实战:MobaXterm SSH连接配置与排查指南

1. 先别急着敲命令,把整条链路想清楚1.1 你面对的不只是“装个系统”这么简单“装Ubuntu 18.04通过MobaXterm的ssh远程访问”这个标题看起来很直白,但真正在电脑面前操作过一轮就会发现,这里面至少叠了三层任务:第一层是把Ubuntu …

📰

免安装Android Studio,用命令行工具管理Android SDK与模拟器

简介:这是一份面向Windows平台的Android命令行工具包,专为不需要安装完整Android Studio的开发者设计,可解决SDK组件下载、应用编译、设备连接与调试等常用开发问题。压缩包共108个文件,其中95个为jar格式的工具组件,1…

📰

SSM商城项目实战:Java初学者掌握Spring+MyBatis核心配置的跳板

简介:这是一套基于SSM(SpringSpringMVCMyBatis)框架与MySQL数据库开发的完整线上商城系统,含前台用户端与后台管理平台,面向Java Web初学者及中级开发者,适用于课程设计、毕业项目或SSM技术栈实战训练。资源…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬