一、 核心概念:什么是DLL,我们为什么用它?
DLL (Dynamic Link Library),即动态链接库,是包含了可由多个程序同时使用的代码和数据的库。把它想象成一个“功能工具箱”,任何需要这些功能的程序都可以随时调用。
DLL的主要优势 (笔试可能考):
- 代码复用与模块化:将通用的功能(如数学计算、文件操作)封装在DLL中,多个应用程序可以共享,避免重复编写代码。
- 节省内存:当多个程序使用同一个DLL时,该DLL在内存中通常只会加载一份,所有程序共享这一个实例。这与静态库不同,静态库会把代码完整地复制到每个程序中,造成内存冗余。
- 易于维护与升级:如果DLL中的功能需要更新或修复Bug,理论上只需替换这个DLL文件即可,而不需要重新编译所有使用它的应用程序(前提是接口保持不变)。
第二部分:C风格DLL的创建与使用 (基础必会)
这是最基本、兼容性最好的DLL形式。它只导出独立的C函数。
1. 如何创建C风格DLL
目标:创建一个名为 mydll.dll 的库,它导出两个函数:int add(int n) 和 float value(float v1, float v2)。
步骤1:创建项目
在Code::Blocks等IDE中,新建一个项目,选择模板为 Dynamic Link Library (动态链接库)。
步骤2:编写头文件 mydll.h
这个头文件既要给DLL自己用(导出函数),也要给调用它的程序用(导入函数)。
__declspec(dllexport):告诉编译器,这个函数要从DLL中“导出”,给别人用。__declspec(dllimport):告诉编译器,这个函数是从外部DLL中“导入”的。extern "C":这是C++项目中的关键。它告诉C++编译器,请用C语言的方式来处理这些函数名,不要进行C++特有的“名称修串”(Name Mangling),否则外部程序将找不到原始的函数名。
// mydll.h
#ifndef MYDLL_H_INCLUDED
#define MYDLL_H_INCLUDED
// 通过宏定义来区分是DLL项目自身编译,还是被外部调用
#ifdef BUILDING_MYDLL
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif
// 使用 extern "C" 来保证函数名在DLL中不被改变
#ifdef __cplusplus
extern "C" {
#endif
MYDLL_API int add(int n);
MYDLL_API float value(float v1, float v2);
#ifdef __cplusplus
}
#endif
#endif // MYDLL_H_INCLUDED
步骤3:编写源文件 mydll.cpp
在这里实现函数的具体功能。
// mydll.cpp
// 定义这个宏,使得 MYDLL_API 展开为 __declspec(dllexport)
#define BUILDING_MYDLL
#include "mydll.h"
int add(int n) {
return n + 100;
}
float value(float v1, float v2) {
return v1 * v2;
}
步骤4:编译项目
编译成功后,你会在项目的输出目录(通常是 bin/Debug 或 bin/Release)下得到三个非常重要的文件:
mydll.dll:动态链接库文件,程序运行时需要它。libmydll.a(或在Visual Studio下是mydll.lib):静态导入库,在“直接调用”时需要它来帮助链接器找到函数。mydll.h:头文件,给调用方项目使用。
2. 如何使用C风格DLL
使用DLL主要有两种方式:直接调用和间接调用。
方式一:直接调用 (隐式链接/静态加载)
思路:在编译时就将程序和DLL“关联”起来,调用DLL函数就像调用普通本地函数一样简单。
步骤:
- 新建一个应用程序项目 (例如,控制台应用
UseDll.exe)。 - 配置项目:
- 将
mydll.h复制到你的项目目录中,并在main.cpp里#include它。 - 在项目的 Linker settings (链接器设置) 中,添加
libmydll.a(或.lib) 文件。这是为了在编译链接阶段,让编译器知道add和value函数的地址信息在DLL里。
- 将
- 编写代码
main.cpp:直接像调用普通函数一样调用DLL中的函数。
// main.cpp
#include <iostream>
#include "mydll.h" // 包含DLL的头文件
int main() {
int result_add = add(88); // 直接调用
float result_val = value(2.5, 1.2); // 直接调用
std::cout << "add(88) = " << result_add << std::endl;
std::cout << "value(2.5, 1.2) = " << result_val << std::endl;
return 0;
}
- 部署运行:编译生成
UseDll.exe后,必须将mydll.dll文件拷贝到与UseDll.exe相同的目录下,然后才能成功运行。
优点:编码简单,调用直观。
缺点:程序启动时必须找到DLL,否则无法运行。
方式二:间接调用 (显式链接/动态加载)
思路:程序在运行时才去加载DLL并获取函数地址,更加灵活。
步骤:
- 新建一个应用程序项目。这次不需要配置链接器,也不需要
.lib文件。 - 编写代码
main.cpp:- 使用Windows API函数
LoadLibrary()来加载DLL。 - 使用
GetProcAddress()来获取DLL中函数的地址。 - 使用
FreeLibrary()在用完后释放DLL。 - 为了方便调用,通常会用
typedef定义一个和DLL中函数签名完全一致的函数指针类型。
- 使用Windows API函数
// main.cpp
#include <iostream>
#include <windows.h> // 必须包含此头文件
// 定义函数指针类型,必须与DLL中函数的原型完全一致
typedef int (*FuncType_add)(int);
typedef float (*FuncType_value)(float, float);
int main() {
// 1. 加载DLL
HINSTANCE hDll = LoadLibrary("mydll.dll");
if (hDll == NULL) {
std::cout << "Error: Failed to load mydll.dll" << std::endl;
return 1;
}
// 2. 获取函数地址
FuncType_add add_func = (FuncType_add)GetProcAddress(hDll, "add");
FuncType_value value_func = (FuncType_value)GetProcAddress(hDll, "value");
if (add_func == NULL || value_func == NULL) {
std::cout << "Error: Failed to get function address" << std::endl;
FreeLibrary(hDll); // 别忘了释放
return 1;
}
// 3. 通过函数指针调用函数
int result_add = add_func(88);
float result_val = value_func(2.5, 1.2);
std::cout << "add(88) = " << result_add << std::endl;
std::cout << "value(2.5, 1.2) = " << result_val << std::endl;
// 4. 释放DLL
FreeLibrary(hDll);
return 0;
}
- 部署运行:同样,将
mydll.dll文件拷贝到与UseDll.exe相同的目录下。
优点:非常灵活,程序可以在运行时根据条件决定是否加载、加载哪个DLL,常用于实现插件(Plug-in)系统。
缺点:编码相对复杂,需要手动管理DLL的加载和释放。
第三部分:C++风格DLL (进阶核心)
直接在DLL中导出整个C++类是可行的,但存在严重问题,因此有一种更推荐的、基于接口的模式。
1. 不推荐的方法:直接导出C++类
做法:在类的定义前直接加上 __declspec(dllexport)。
// mydll.h - 不推荐的方式
class MYDLL_API Cat {
public:
void setAge(int age);
int getAge() const;
private:
int m_age; // 私有成员
};
使用:和直接调用C函数类似,链接 .lib 文件,#include 头文件,然后直接创建 Cat 对象。
致命缺点 (笔试高频考点):
- 编译器和版本强绑定:DLL和调用它的EXE必须使用完全相同的编译器和相同版本进行编译,否则会因为C++ ABI(应用二进制接口)不兼容导致程序崩溃。
- 实现细节暴露:类的所有私有成员(如
int m_age;)都定义在头文件中,完全暴露给了调用者,破坏了类的封装性。 - 依赖地狱:如果
Cat类继承了其他类,或者其成员变量是另一个自定义类,那么这些相关的类也必须全部导出,非常麻烦。
结论:在实际项目中,应极力避免使用这种方法。
2. 推荐的方法:基于“纯虚接口 + 工厂函数” (机试核心)
这是现代C++中跨模块传递对象的标准做法,它稳定、安全且灵活。
思路:
- 我们不导出具体的实现类
Cat。 - 我们定义一个只包含纯虚函数的接口类
ICat,并在DLL中导出它。 - 我们导出一个C风格的工厂函数
createCat(),这个函数负责在DLL内部创建Cat的实例,并返回一个指向接口ICat的指针。 - 调用方通过这个接口指针来操作对象,完全不知道具体实现类的存在。
步骤1:创建推荐风格的DLL
- 修改头文件
mydll.h- 定义接口类
ICat,它只有纯虚函数和一个虚析构函数。 - 声明一个用
extern "C"修饰的工厂函数。
- 定义接口类
// mydll.h - 推荐的方式
#ifndef MYDLL_H_INCLUDED
#define MYDLL_H_INCLUDED
#include <memory> // 为了使用智能指针 std::shared_ptr
#ifdef BUILDING_MYDLL
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif
// 1. 定义接口类
class ICat {
public:
virtual ~ICat() {} // 接口类必须有虚析构函数!
virtual void setAge(int age) = 0;
virtual int getAge() const = 0;
virtual void passYears(int years) = 0;
};
// 2. 声明C风格的工厂函数,返回一个指向接口的智能指针
#ifdef __cplusplus
extern "C" {
#endif
// 使用智能指针可以自动管理内存,防止内存泄漏
MYDLL_API std::shared_ptr<ICat> createCat();
#ifdef __cplusplus
}
#endif
#endif // MYDLL_H_INCLUDED
- 创建内部实现类 (cat.h, cat.cpp) - 这部分不给调用者
cat.h:定义一个具体的Cat类,它继承ICat并实现所有接口。cat.cpp:编写Cat类的具体实现。
// cat.h (DLL内部使用,不公开)
#include "mydll.h"
class Cat : public ICat {
public:
virtual ~Cat() override {}
void setAge(int age) override;
int getAge() const override;
void passYears(int years) override;
private:
int m_age = 0;
};
- 实现工厂函数 (在 mydll.cpp 中)
- 在这个函数里,我们
new一个具体的Cat对象,然后把它包装在智能指针里返回。
- 在这个函数里,我们
// mydll.cpp
#define BUILDING_MYDLL
#include "mydll.h"
#include "cat.h" // 包含内部实现
std::shared_ptr<ICat> createCat() {
return std::make_shared<Cat>();
}
步骤2:使用推荐风格的DLL
- 配置项目:与“直接调用”方式相同,需要链接
libmydll.a(或.lib)。 - 编写代码
main.cpp:#include "mydll.h",但这次头文件里只有接口和工厂函数。- 调用工厂函数
createCat()获取一个指向对象的接口指针。 - 通过这个接口指针调用所有方法。
// main.cpp
#include <iostream>
#include "mydll.h" // 只需包含这个对外接口头文件
int main() {
// 1. 调用工厂函数获取对象实例
std::shared_ptr<ICat> my_cat = createCat();
// 2. 通过接口指针操作对象
my_cat->setAge(5);
std::cout << "Cat's age is: " << my_cat->getAge() << std::endl; // 输出 5
my_cat->passYears(3);
std::cout << "After 3 years, cat's age is: " << my_cat->getAge() << std::endl; // 输出 8
// 3. 不需要手动释放内存,shared_ptr会在离开作用域时自动处理
return 0;
}
优点 (笔试高频考点):
- 完美封装,隐藏实现:调用者只知道
ICat接口,完全不知道内部的Cat类是如何实现的。 - 二进制兼容性好:由于导出的是C函数(工厂函数),并且通过虚函数表(v-table)来调用成员函数,这是一种非常稳定的跨模块调用方式,基本摆脱了编译器版本的束缚。
- 维护性极佳:只要接口
ICat和工厂函数不变,你可以随意修改DLL内部Cat类的实现,甚至换一个完全不同的类,而调用程序完全不需要改动,也不需要重新编译。
2290

被折叠的 条评论
为什么被折叠?



