概述:在运行时将一个基类对象/指针的虚表指针(vptr)换成子类实例的虚表指针,从而在不改动对象内存布局的前提下,让一个基类对象“临时/动态地”表现出子类的多态行为。
原理
在 C++ 中,只要有虚函数,对象首地址处就会存一个指向**虚函数表(vtable)**的指针(vptr)。多态的本质就是:通过调用对象的虚表指针找到正确的虚函数入口。
所以如果能直接改写基类对象自身的 vptr,让它指向子类的虚表,那么这个对象在后续调用虚函数时,就会走进子类的实现——这就是“动态声明子类”的核心思路。
关键步骤
- 用
static_assert保证基类与子类不新增成员变量(sizeof相等),否则改写 vptr 后对象内存会被越界使用。 - 通过
LPVOID*取出基类对象首地址处的vptr与临时子类实例的vptr。 - 把基类对象的
vptr覆盖为子类的vptr。
cpp
#include <iostream>
#include <cstdlib>
using namespace std;
class BaseClass
{
public:
BaseClass()
{
cout<<"BaseClass Construct"<<endl;
}
virtual ~BaseClass()
{
cout<<"BaseClass Destruct"<<endl;
}
virtual void TestFunction()
{
cout<<"BaseClass TestFunction"<<endl;
}
protected:
int Test;
};
class SonClass : public BaseClass
{
public:
SonClass()
{
cout<<"SonClass Construct"<<endl;
}
virtual ~SonClass()
{
cout<<"SonClass Destruct"<<endl;
}
virtual void TestFunction()
{
cout<<"SonClass TestFunction"<<endl;
}
};
typedef void *LPVOID;
template<class _baseClass, class _derivedClass>
bool DoDynamicSubclassing(_baseClass *pBaseObj)
{
if(pBaseObj)
{
static_assert(sizeof(_baseClass) == sizeof(_derivedClass), "DynamicSubclassing to _derivedClass must not have its own members");
_derivedClass _derivedClassinstance;
LPVOID *pBaseVirtPtr = (LPVOID*)pBaseObj, *pDerivedVirtPtr = (LPVOID*)&_derivedClassinstance;
//assign the virtual ptr of the derived class
*pBaseVirtPtr = *pDerivedVirtPtr;
return true;
}
return false;
}
int main()
{
BaseClass *Test = new BaseClass;
cout << *(int*)Test << endl;
DoDynamicSubclassing<BaseClass, SonClass>(Test);
Test->TestFunction();
cout << *(int*)Test << endl;
cout << " ————————- " <<endl;
BaseClass aaaa;
cout << *(int*)&aaaa << endl;
DoDynamicSubclassing<BaseClass, SonClass>(&aaaa);
aaaa.TestFunction();
cout << *(int*)&aaaa << endl;
return 0;
}输出与分析
运行后会看到:
text
BaseClass Construct
<基类 vptr 地址>
SonClass TestFunction ← 明明是 BaseClass*,却调用了子类实现
<子类 vptr 地址>
BaseClass Construct
BaseClass Destruct ← 栈上对象销毁时仍走基类析构
<子类 vptr 地址>
SonClass TestFunction
...
Test->TestFunction()在改写 vptr 后输出SonClass TestFunction,说明多态已生效。
注意事项 / 风险
- 危险操作:直接篡改 vptr 属于未定义行为(UB),不同编译器、不同对象布局下行为可能不同,生产代码中应避免。
- 只能用于无新增成员的子类,否则越过
static_assert会导致内存越界。 - 析构函数不会被正确替换:栈上对象销毁时仍调用基类析构,堆对象若
delete基类指针(基类析构为 virtual)则仍走基类析构,不会执行子类析构逻辑。 - 更安全的替代方案:工厂模式、虚函数、
std::variant、策略模式等,能够在不破坏封装的前提下实现运行时多态。
实际业务中几乎不会直接采用这种 hack 方式,更多是作为一种理解 vptr / vtable 布局的学习示例。