这是一个非常有深度的问题。

粉丝提问
有一个无源码的C++ DLL,有头文件,但是没有LIB文件的情况下,如何动态导出其中的类。
嗯,这个问题涉及动态加载 DLL,类构造函数的底层实现,底层内存布局,C++编译原理等。
强烈建议所有C++程序员能仔细看看这个视频,它将帮助你更深入的理解C++的底层实现。

正式开始演示
好,我们正式开始。
做为演示,我针对粉丝提出的问题写了一个测试DLL,在这个DLL中它导出了一个函数叫ShowMessage。
在这个函数中,它会弹出一个消息框。
1. 导出一个 C 风格的函数
#ifdef __cplusplus
extern "C" {
#endif
// 此函数会弹出一个消息框,表明该函数已被调用
__declspec(dllexport) void ShowMessage()
{
MessageBox(NULL, L"这是一个从 DLL 导出的函数",
L"DLL Message", MB_OK);
}
#ifdef __cplusplus
}
#endif
2. 导出一个 C++ 类(Dog类)
// 导出一个 C++ 类(狗狗类)
class __declspec(dllexport) Dog {
public:
// 构造函数
Dog() {
}
// 析构函数
~Dog() {
}
// static void* operator new(size_t size) {
// MessageBoxA(NULL, "自定义 operator new 被调用", "Memory Allocation", MB_OK);
// return malloc(size);
// }
// static void operator delete(void* ptr) {
// MessageBoxA(NULL, "自定义 operator delete 被调用", "Memory Deallocation", MB_OK);
// free(ptr);
// }
// 成员函数:发出狗叫声
void Bark() {
MessageBox(NULL, L"汪汪!",
L"Dog Bark", MB_OK);
}
};
测试程序:动态加载DLL
我另外再写一个测试程序,用来加载这个DLL。
因为粉丝的问题是没有LIB文件,所以在这个程序中,我们得采用动态加载DLL的方式来调用导出函数和导出类。
首先使用LoadLibrary加载这个DLL,同时我们定义好ShowMessage的函数指针类型,然后使用GetProcAddress获得ShowMessage的内存地址,这样就可以调用ShowMessage了。
#include <windows.h>
#include <iostream>
// 为了动态调用函数,先定义函数指针类型
typedef void (*PFN_ShowMessage)();
int main()
{
std::cout << "正在加载 DLL" << std::endl;
// 加载 DLL
HMODULE hDLL = LoadLibrary(L"dllproj.dll");
if (hDLL == NULL) {
std::cerr << L"加载 DLL 失败!错误代码:"
<< GetLastError() << std::endl;
return -1;
}
// 获取 ShowMessage 函数的地址
PFN_ShowMessage ShowMessage = (PFN_ShowMessage)
GetProcAddress(hDLL, "ShowMessage");
if (ShowMessage == NULL) {
std::cerr << L"获取 ShowMessage 地址失败!错误代码:"
<< GetLastError() << std::endl;
FreeLibrary(hDLL);
return -1;
}
// 调用 ShowMessage 函数
ShowMessage();
return 1;
}
通过这种动态加载DLL的方式调用导出函数非常简单,人人都会是不是?
核心问题:如何动态使用导出的C++类?
现在粉丝的问题是,这种动态加载DLL的方式要如何使用它导出的类呢?
其实是有办法的。
使用DUMPBIN查看DLL导出表
首先咱们用VS自带的DUMPBIN工具看一下DLL的导出信息:
1 0 000112D0 ??0Dog@@QAE@XZ = @ILT+715(??0Dog@@QAE@XZ)
2 1 000112C6 ??1Dog@@QAE@XZ = @ILT+705(??1Dog@@QAE@XZ)
3 2 00011280 ??4Dog@@QAEAAV0@ABV0@@Z = @ILT+635(??4Dog@@QAEAAV0@ABV0@@Z)
4 3 0001100A ?Bark@Dog@@QAEXXZ = @ILT+5(?Bark@Dog@@QAEXXZ)
5 4 000110D7 ShowMessage = @ILT+210(_ShowMessage)
从导出表中可以看到,第一个导出符号是??0,然后是DOG……
我们能猜出它肯定是和DOG类有关,但是这些符号到底是什么呢?
C++编译器名称修饰规则(VS编译器)
其实这是编译器的导出符号修饰符,在不同的编译器下会有些差别,因为我们用的是VS编译器。
在VS中,??0开头表示类构造函数,??1表示类析构函数,??2表示new操作符重载,??3表示delete操作符重载,而??4表示赋值运算符,以一个?问号开头的则表示成员函数。
这是VS编译器的规则,如果是GCC又会不同,如果你采用GCC编译器那它的构造函数会是这样_ZN7DogC1Ev。
基于这些修饰规则,现在我们知道第一个导出符号是Dog类的构造函数,第二个是析构函数,第三个是赋值运算符,第四个是Dog类的Bark函数。
C++类对象创建的底层原理
那我们要如何创建这个导出的类对象呢?
当我们有一个类Dog,我们要创建它的类对象时,在C++中我们会这样写:
Dog* dog = new Dog();
其实它等价于这样:
void* memory = operator new(sizeof(Dog));
Dog* dog = static_cast<Dog*>(memory);
dog->Dog();
首先创建内存,然后将内存转换成Dog结构,然后调用构造函数。
在这里,我们看到构造函数Dog被调用时并没有传入任何参数。
然而,在底层二进制实现中,C++ 编译器会隐式地将 this 指针作为第一个参数传递给所有的非静态成员函数,包括构造函数和析构函数。
划重点,这一句理解本视频知识的关键。
完整实现:动态调用导出类
明白了这点之后,再来解决粉丝的问题就容易了。
首先,因为粉丝说他是有类的头文件的,所以我们把Dog类的声明写在这里。
class Dog {
public:
Dog();
~Dog();
// 成员函数:发出狗叫声
void Bark();
};
然后我们先分别定义构造函数,析构函数,以及成员函数Bark的函数指针类型。
然后我们依然调用GetProcAddress分别获取这几个函数在内存中的地址。
现在传给GetProcAddress的函数名称务必是编译器添加修饰符后的完整名称。
// 为了动态调用函数,先定义函数指针类型
typedef void (*PFN_ShowMessage)();
// 构造函数:接受 this 指针
typedef void (*ConstructorFunc)(void*);
// 析构函数:接受 this 指针
typedef void (*DestructorFunc)(void*);
// 成员函数:接受 this 指针
typedef void (*BarkFunc)(void*);
主函数完整代码
int main()
{
std::cout << "正在加载 DLL" << std::endl;
// 加载 DLL
HMODULE hDLL = LoadLibrary(L"dllproj.dll");
if (hDLL == NULL) {
std::cerr << L"加载 DLL 失败!错误代码:"
<< GetLastError() << std::endl;
return -1;
}
// 获取 ShowMessage 函数的地址
PFN_ShowMessage ShowMessage = (PFN_ShowMessage)
GetProcAddress(hDLL, "ShowMessage");
if (ShowMessage == NULL) {
std::cerr << L"获取 ShowMessage 地址失败!错误代码:"
<< GetLastError() << std::endl;
FreeLibrary(hDLL);
return -1;
}
// 调用 ShowMessage 函数
ShowMessage();
ConstructorFunc Constructor = (ConstructorFunc)GetProcAddress(hDLL, "??0Dog@@QAE@XZ");
DestructorFunc Destructor = (DestructorFunc)GetProcAddress(hDLL, "??1Dog@@QAE@XZ");
BarkFunc Bark = (BarkFunc)GetProcAddress(hDLL, "?Bark@Dog@@QAEXXZ");
// 正确的内存分配,确保对齐
void* dogMemory = ::operator new(sizeof(Dog));
// 4. 调用构造函数,初始化对象(此时 this 指针就是 dogMemory)
Constructor(dogMemory);
// 5. 使用对象,调用成员函数
Bark(dogMemory);
// 6. 调用析构函数,销毁对象
Destructor(dogMemory);
// 7. 释放内存
::operator delete(dogMemory);
// 8. 卸载 DLL
FreeLibrary(hDLL);
return 1;
}
运行结果与总结
好,现在我们运行这个测试程序。
看到了吗,首先是弹出了ShowMessage函数中的消息框,然后弹出了Bark类成员函数中的消息框。
说明我们成功使用动态加载DLL的方式创建这个导出类对象并调用了它的成员函数。
怎么样,你们学会了吗?
本文适合中高级C++开发者阅读,掌握此技术后,你将能更灵活地处理各种DLL集成场景。


