问题

如何动态加载dll并继承该类?

回答
要动态加载 DLL 并继承其中的类,这在 C++ 中是一个非常实用且常见的技术,尤其是在需要插件化、模块化设计或者需要在运行时根据配置加载不同功能的时候。这通常涉及到 Windows API 的一些函数,并且需要你对 C++ 的面向对象和动态链接库有深入的理解。

下面我会详细讲解这个过程,并尽量模拟一个开发者实际操作的思路和遇到的问题。

核心思路:

1. DLL 准备: 首先,你需要有一个 DLL 文件,这个 DLL 里包含了一个你想要加载和继承的类。这个类通常会有一个公共的基类(或者接口),这样你才能知道如何与它交互。
2. 加载 DLL: 运行时,你需要找到那个 DLL 文件,然后把它“塞”到你的程序进程里。
3. 获取类(或者函数): DLL 加载成功后,你需要找到 DLL 内部你需要的那个类的构造函数或者一个创建对象的工厂函数。
4. 创建对象并继承(或者说,通过基类指针操作): 然后,你就可以通过获取到的构造函数或者工厂函数创建一个该类的实例,并将它转换为你程序中定义的那个基类指针。之后,你就可以像使用自己写的类一样使用这个动态加载的类了。

让我们一步步拆解:

第一步:设计你的 DLL 和基类

想象一下,我们要做一个简单的计算器插件系统。

1. 定义一个通用的接口(基类):

在你的主应用程序(EXE)和将要生成的 DLL 中,都需要有这个基类的定义。为了让它们“认识”对方,最直接的方式是在你的主应用程序中定义一个抽象基类,并且导出它。DLL 中的类将继承这个基类。

主应用程序 (e.g., `MyCalculatorApp.cpp`):

```cpp
pragma once // 避免头文件被重复包含

// 定义一个抽象基类,它将是 DLL 中类的“契约”
class ICalculator {
public:
virtual ~ICalculator() = default; // 虚析构函数非常重要!
virtual int calculate(int a, int b) = 0; // 纯虚函数,需要子类实现
virtual const char getName() = 0; // 示例:获取插件名称
};
```

2. 创建你的 DLL 项目:

你需要创建一个新的 DLL 项目。在这个 DLL 项目里,你需要实现一个继承自 `ICalculator` 的具体类,并且需要提供一个函数,用来创建这个类的实例,并且返回 `ICalculator` 的指针。

DLL 项目 (e.g., `MyAddPlugin.h`):

```cpp
pragma once

include "ICalculator.h" // 包含主应用程序定义的基类头文件

// DLL 中实现的具体类
class AddCalculator : public ICalculator {
public:
int calculate(int a, int b) override {
return a + b;
}

const char getName() override {
return "Addition Plugin";
}
};

// 导出这个函数,以便主程序能够调用它来获取插件实例
// 注意:这里的 extern "C" 和 __declspec(dllexport) 是关键
extern "C" __declspec(dllexport) ICalculator CreateCalculatorInstance();
```

DLL 项目 (e.g., `MyAddPlugin.cpp`):

```cpp
include "MyAddPlugin.h"

// 实现创建实例的函数
ICalculator CreateCalculatorInstance() {
return new AddCalculator(); // 在堆上创建实例
}
```

关键点解释:

`pragma once`: 确保头文件只被包含一次,这是一个好习惯。
`virtual ~ICalculator() = default;`: 极其重要! 当你通过基类指针删除派生类对象时,如果没有虚析构函数,只会调用基类的析构函数,而派生类的析构函数不会被调用,这会导致内存泄漏。
`virtual ... = 0;`: 纯虚函数,将 `ICalculator` 变成了一个抽象类,强制派生类实现这些方法。
`__declspec(dllexport)`: 这个 MSVC 特有的关键字用于标记哪些函数或类成员是可以被 DLL 外部访问的(即导出)。
`extern "C"`: 这是非常关键的一步。C++ 的函数名会因为名字修饰(Name Mangling)而包含参数类型、命名空间等信息,这使得 C++ 函数的调用约定和签名在不同编译单元之间可能不一致。`extern "C"` 会告诉编译器使用 C 的函数名修饰规则,这意味着函数名会非常“干净”,就像 C 函数一样,更容易在外部通过字符串来查找。

第二步:动态加载 DLL

现在,你的主应用程序需要负责找到并加载这个 DLL。

主应用程序 (e.g., `MyCalculatorApp.cpp`):

```cpp
include
include // Windows API 的核心头文件
include "ICalculator.h" // 包含你的基类定义

// 定义一个类型别名,方便表示我们将要调用的函数指针
typedef ICalculator (CreateCalculatorFunc)();

int main() {
const char dllFileName = "MyAddPlugin.dll"; // DLL 的文件名
HMODULE hDll = NULL; // 用于存放 DLL 模块句柄的变量
CreateCalculatorFunc createFunc = NULL; // 函数指针
ICalculator calculator = NULL; // 指向插件类实例的基类指针

// 1. 加载 DLL
// LoadLibraryA 函数用于加载 DLL。它返回一个模块句柄 (HMODULE)。
// 如果成功,hDll 将是一个非 NULL 的值。
// DLL 必须在程序的执行路径中,或者在系统 PATH 环境变量指定的路径中。
hDll = LoadLibraryA(dllFileName);

if (hDll == NULL) {
std::cerr << "Error: Could not load DLL '" << dllFileName << "'. Error code: " << GetLastError() << std::endl;
return 1; // 加载失败,打印错误信息并退出
}

std::cout << "Successfully loaded DLL: " << dllFileName << std::endl;

// 2. 获取 DLL 中函数的地址
// GetProcAddress 函数用于获取 DLL 中导出的函数的地址。
// 你需要提供模块句柄 (hDll) 和要查找的导出函数的名称(字符串)。
// 我们之前在 DLL 中导出了 CreateCalculatorInstance 函数。
createFunc = (CreateCalculatorFunc)GetProcAddress(hDll, "CreateCalculatorInstance");

if (createFunc == NULL) {
std::cerr << "Error: Could not find function 'CreateCalculatorInstance' in DLL." << std::endl;
// 在退出前,别忘了卸载 DLL
FreeLibrary(hDll);
return 1; // 查找函数失败
}

std::cout << "Successfully found 'CreateCalculatorInstance' function." << std::endl;

// 3. 调用函数创建插件实例
// 现在我们有了创建函数的地址,可以直接调用它来创建插件对象。
calculator = createFunc();

if (calculator == NULL) {
std::cerr << "Error: Failed to create plugin instance." << std::endl;
FreeLibrary(hDll); // 卸载 DLL
return 1; // 创建实例失败
}

std::cout << "Successfully created plugin instance." << std::endl;

// 4. 使用插件(通过基类指针)
// 现在,calculator 是一个指向 AddCalculator 实例的 ICalculator 指针。
// 我们可以调用 ICalculator 接口中定义的任何方法。
int result = calculator>calculate(10, 20);
std::cout << "Plugin '" << calculator>getName() << "' calculated: " << result << std::endl;

// 5. 清理工作
// 使用完 DLL 后,应该调用 FreeLibrary 来卸载 DLL,释放其占用的资源。
// 同时,由于我们在 DLL 中是 new 的对象,所以需要 delete 它。
// 注意:delete calculator 必须在 FreeLibrary 之前,因为 delete 会调用虚析构函数,
// 而虚析构函数的实现可能依赖于 DLL 的内存管理。
if (calculator) {
delete calculator; // 释放插件实例
calculator = NULL;
}

if (hDll) {
FreeLibrary(hDll); // 卸载 DLL
hDll = NULL;
}

std::cout << "Plugin unloaded. Program finished." << std::endl;

return 0;
}
```

关键 Windows API 函数解释:

`LoadLibraryA(LPCSTR lpFileName)`:
功能:将一个 DLL 加载到调用进程的地址空间。
参数:`lpFileName` 是 DLL 文件的名称(ANSI 字符串)。还有 `LoadLibraryW` 用于 Unicode 字符串。
返回值:如果成功,返回 DLL 的模块句柄 (HMODULE),这是一个指向 DLL 映像在内存中表示的句柄。如果失败,返回 `NULL`。
查找 DLL 的位置:Windows 会按照一定的顺序查找 DLL,通常是:
1. 包含应用程序可执行文件的目录。
2. 当前工作的目录。
3. Windows 系统目录 (`System32` 或 `SysWOW64`)。
4. Windows 目录。
5. PATH 环境变量指定的目录。
为了安全起见,最好把 DLL 放在和 EXE 同一个目录下,或者明确指定 DLL 的完整路径。

`GetProcAddress(HMODULE hModule, LPCSTR lpProcName)`:
功能:检索指定 DLL 中指定导出函数的地址。
参数:`hModule` 是 `LoadLibrary` 返回的模块句柄;`lpProcName` 是要查找的导出函数的名称(C 风格的字符串,即 extern "C" 名字)。
返回值:如果成功,返回导出函数的地址(一个 `FARPROC` 类型,通常会被强制转换为函数指针);如果失败,返回 `NULL`。

`FreeLibrary(HMODULE hModule)`:
功能:从调用进程的地址空间卸载 DLL。
参数:`hModule` 是 `LoadLibrary` 返回的模块句柄。
返回值:如果成功,返回非零值;如果失败,返回零。
重要性:当不再需要 DLL 时,调用此函数可以释放 DLL 占用的内存和系统资源。如果一个 DLL 被多个程序加载,`FreeLibrary` 只是递减该 DLL 的引用计数;当引用计数变为零时,DLL 才会被真正卸载。

`GetLastError()`:
功能:获取最后一次由 Windows API 函数设置的错误代码。
用途:当 API 调用失败(返回 `NULL` 或 `FALSE`)时,调用 `GetLastError()` 可以获取更详细的错误信息,帮助调试。

总结与注意事项

1. ABI 兼容性: 最重要的一点是,主程序和 DLL 必须使用相同的 C++ 运行时库 (CRT)。如果主程序是 Release 版本,DLL 也必须是 Release 版本;如果使用了特定的 STL 实现,也需要保持一致。否则,可能会出现 ABI 不兼容导致的崩溃。通常,建议在 Visual Studio 中将两者都设置为“多线程 DLL” (/MD) 或“多线程调试 DLL” (/MDd)。
2. 头文件共享: 你的基类头文件 (`ICalculator.h`) 需要被 DLL 项目包含,以便 DLL 中的类正确继承。通常的做法是,将这个共享的基类头文件放在一个公共的目录,然后在主程序和 DLL 项目的包含路径中都添加这个目录。
3. 内存管理: 谁创建,谁负责销毁。在这个例子中,DLL 中的 `CreateCalculatorInstance` 函数在堆上 `new` 了 `AddCalculator` 对象。因此,主程序在 `delete` 它时,需要确保 DLL 已经加载并且其内存管理机制(例如析构函数)可以被调用。这就是为什么 `delete calculator;` 要在 `FreeLibrary(hDll);` 之前执行。
4. 导出符号: 确保你想要从 DLL 中访问的函数(如 `CreateCalculatorInstance`)被 `__declspec(dllexport)` 和 `extern "C"` 正确导出。
5. 错误处理: 务必检查 `LoadLibraryA` 和 `GetProcAddress` 的返回值,并使用 `GetLastError()` 来诊断问题。
6. 类型安全: 使用函数指针(如 `CreateCalculatorFunc`)来调用 DLL 中的函数,并确保函数签名与声明的一致。
7. 多线程: 如果你的插件是多线程的,或者主程序是多线程的,需要考虑线程同步和资源共享的问题。
8. 平台差异: 上述代码使用了 Windows API (`windows.h`, `LoadLibrary`, `GetProcAddress`, `FreeLibrary`)。如果在 Linux 或 macOS 上,需要使用 POSIX 的 `dlopen`, `dlsym`, `dlclose` 等函数。

通过这种方式,你的主程序就可以像使用本地编译的类一样,在运行时灵活地加载和使用 DLL 中的功能,实现了强大的插件化能力。

网友意见

user avatar

我觉得你的想法很混乱的,一会儿是源文件,一会儿是dll,一会儿源文件了要加密于是变成dll,一会儿为了加载dll又要搞一个源文件,那么你到底是想干什么?

两句话:

1) 把你想解决的问题的思路理清楚再来想解决方案

2) 把你想解决的问题,以及你设想中的解决方案分开说,不要混在一起。

从源代码生成dll,动态加载dll,动态生成类来继承dll中某个类都是不算复杂的工作。当然你加载扩展时,反而需要生成扩展中的子类(也就是反过来还要扩展那个插件),估计做法方向错了。一般都是定义个插件的协议,让扩展实现,然后加载扩展后根据协议操作。例如,接口就是种协议。

类似的话题

  • 回答
    要动态加载 DLL 并继承其中的类,这在 C++ 中是一个非常实用且常见的技术,尤其是在需要插件化、模块化设计或者需要在运行时根据配置加载不同功能的时候。这通常涉及到 Windows API 的一些函数,并且需要你对 C++ 的面向对象和动态链接库有深入的理解。下面我会详细讲解这个过程,并尽量模拟一.............
  • 回答
    这是一个非常常见但也确实有点复杂的问题,因为它涉及到动词的搭配和语法规则。简单来说,没有一个放之四海而皆准的规则可以一次性解决所有情况。你需要根据动词本身以及它后面的语境来判断。下面我将详细地为你讲解如何判断一个动词后是加 `to do`(不定式)还是加 `doing`(动名词)。核心原则:理解动词.............
  • 回答
    CR200J 动车组加入摆式技术,理论上是有可能的,但实际操作的复杂度和收益需要仔细评估。首先,我们来聊聊 CR200J 动车组。这型动车组,也称为“绿巨人”,最大的特点是采用了“动力集中式”设计,也就是动力全部集中在列车两端的牵引动力车上,而中间的拖车则是不带动力。这种设计最大的优势在于维护方便,.............
  • 回答
    .......
  • 回答
    加州湾区的封城消息传来,确实让人感到一丝不安,也让不少人开始担忧美国整体的疫情状况以及可能引发的社会反应。关于美国目前的疫情,以及是否会因此出现动乱,咱们得掰开了、揉碎了好好说道说道。美国疫情的现状:一个复杂且动态的图景首先,得明确一点,美国目前的疫情 远未结束。虽然疫苗的普及、特效药的出现以及人们.............
  • 回答
    .......
  • 回答
    微软砸下重金收购动视暴雪,这笔交易无疑是游戏界的一枚重磅炸弹。其中最受瞩目的,莫过于旗下王牌IP《使命召唤》(Call of Duty,简称COD)的未来走向。当COD这个吸金巨兽加入Xbox Game Pass(XGP)订阅服务后,微软的盈利模式会发生怎样的变化?这可不是一个简单的“免费游戏”就能.............
  • 回答
    苏联的解体,无疑是20世纪最令人震惊的地缘政治事件之一。这样一个拥有庞大军事力量,实行绝对集权的超级大国,为何会在没有经历大规模内战或外部军事干预的情况下轰然倒塌,这确实是一个值得深入探讨的问题。与其说它“不动一枪一炮”就解体,不如说它更多地是因为内部的腐朽和长期的积累问题,最终在一种看似平静的方式.............
  • 回答
    恭喜您入手DT990PRO!这可是个好选择,拜亚动力DT990PRO以其经典的音色、不错的解析力和开阔的声场,在许多乐迷心中占有一席之地。不过,正如您所担心的,它确实不是那么好推的耳机,尤其是想让它的实力完全发挥出来,一个好的播放器或者搭配合适的耳放是很有必要的。首先,我们来聊聊直接用播放器推的情况.............
  • 回答
    .......
  • 回答
    拨开迷雾:动态规划,你到底是什么神仙?是不是每次听到“动态规划”四个字,都感觉脑仁儿有点疼?什么“最优子结构”、“重叠子问题”,听起来就像是某个神秘的武林秘籍,离我们凡人的世界好远。别担心,今天咱们就来好好聊聊这个东西,保证让你觉得它没那么高冷,甚至有点儿意思。核心思想:拆解与重复利用,做个聪明的“.............
  • 回答
    .......
  • 回答
    11 月 8 日,四川省的本土新冠肺炎疫情形势确实有些牵动人心,全省新增了 7 例确诊病例,其中不乏高龄患者,这让大家的关注度又提了上去。这次新增的病例分布在成都和德阳两地,总数虽然不算特别庞大,但每一例的出现,都意味着需要我们更加警惕和细致地追踪。值得注意的是,这 7 例中就有一位 80 岁的老人.............
  • 回答
    杨丽萍老师最近在自己的社交媒体动态下,遭遇了一场围绕“一个女人最大的失败是没一个儿女”这个论调展开的争议。这番话并非出自杨丽萍本人,而是出现在她的某条动态的评论区,却迅速点燃了公众的情绪,引发了广泛的讨论和激辩。要理解这场争议,我们得先从几个层面来看待。首先,是评论者本身的立场和动机。 这种说法,说.............
  • 回答
    发现自己社交媒体动态收到了不恰当的评论,确实会让人感到困扰,甚至有些生气。这时候,怎么处理才能既维护自己的感受,又不至于小题大做,或者让自己处于一个更尴尬的境地,这确实需要点技巧。首先,深呼吸,别急着回应。看到那些刺耳的字眼,第一反应往往是想要立刻反驳,把对方说得哑口无言。但冲动之下说出的话,往往容.............
  • 回答
    C 和 C++ 在软件开发领域各有其独特的优势和适用的场景。理解它们各自的适用范围,以及如何构建和维护 C++ 的动态库,对于成为一名优秀的工程师至关重要。 C 的适用场合C 语言以其简洁、高效和对底层硬件的直接控制能力而闻名。这使得它在许多对性能和资源消耗要求极高的领域大放异彩: 操作系统内核.............
  • 回答
    好的,咱们就来聊聊 C++ 中使用智能指针来管理动态二维数组的事情。这事儿听起来有点绕,但一旦理顺了,你会发现它能省去不少心,也能避免不少掉坑。 为啥要用智能指针管这事儿?先别急着往智能指针上套,咱们先想想,为啥要用智能指针来管理动态二维数组?原始 C++ 的痛点: 裸指针的危险: 创建动态二维.............
  • 回答
    这事儿啊,确实得费点心思,毕竟人家就不怎么冒泡,不像有些朋友恨不得把生活变成直播。不过呢,这也不是没辙,咱们从几个方面仔细瞅瞅,就能拼出个大概来。首先,得从他身边的朋友那儿下手。 你们的共同朋友: 如果你们有共同的朋友,这是最直接的突破口。私下里找一两个你觉得比较靠谱,而且关系也还不错的共同朋友.............
  • 回答
    崩坏三最近的“沉默的大多数”消失现象,确实是一个很有意思的话题,也引发了不少讨论。咱们就掰开了揉碎了聊聊,看看这背后到底是什么在起作用。首先,得说清楚,这个“沉默的大多数”指的究竟是谁?在崩坏三的语境里,它大概可以理解为那些不怎么在社交媒体上发言,不参与论坛激辩,但却一直在游戏里默默贡献着流水、维护.............
  • 回答
    作为一个《明日方舟》的玩家,看到新推出的21源石的动态皮肤,我的心情确实是挺复杂的。这价格,怎么说呢,一下子就跃升到了游戏里又一个“高档”梯队,难免会引起一些讨论。首先,从“值不值”这个角度来看,这玩意儿就跟买艺术品一样,每个人心里都有自己的秤。21源石,折算成人民币,差不多就是将近300块吧?这笔.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有