题记:
今天在公司看到一个很奇怪的code,从它的函数名DynamicSubClassing 貌似是动态子类化,于是研究了一番收获颇多,特作如下总结:
首先上code:
#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;
DoDynamicSubclassing<BaseClass, SonClass>(Test);
Test->TestFunction();
getchar();
return 0;
}
运行这段code后你会发现Test调用的TestFunction不再是BaseClass的了,而是SonClass的TestFunction。其实这个原理就是修改了Test对象的虚函数表指针。
下面让我来一一解析其具体原理:
1、首先说说虚函数表指针
在C++中如果在类中有虚函数的话,那么就会生成一个虚函数表,并且生成的对象在其头部有一个指针专门指向虚函数表首地址,在对象以后调用修函数的时候就会从这个表中去查询具体调用的函数地址并执行。说了这么多你可能说这个都是我自己的话,那么下面上code证明我所说的。
#include <iostream>
using namespace std;
class ClassA
{
public:
virtual void a(){ cout << "ClassA::function a" <<endl; };
virtual void b(){ cout << "ClassA::function b" <<endl; };
virtual void c(){ cout << "ClassA::function c" <<endl; };
};
class ClassB : public ClassA
{
public:
virtual void a(){ cout << "ClassB::function a" <<endl; };
virtual void b(){ cout << "ClassB::function b" <<endl; };
virtual void c(){ cout << "ClassB::function c" <<endl; };
};
int main()
{
ClassA a;
typedef void (*pFun)();
pFun pf = NULL;
//1、取对象b的地址
//2、将取地址后转换成 int* 再取内容,这样获得前四个字节的内容(标记为 p1 ),其实这就是虚拟表的指针的内容
//3、将 p1 在转换成 int* 再取内容,这样获取的就是虚拟表的第一个元素的内容,及第一个虚函数的地址
int *pVirtualTable = (int*)(*(int*)&a);
for(int i = 0; i < 3; i++)
{
pf = (pFun)pVirtualTable[1];
pf();
}
cout<<"———————————"<<endl;
ClassB b;
pVirtualTable = (int*)(*(int*)&b);
for(int i = 0; i < 3; i++)
{
pf = (pFun)pVirtualTable[1];
pf();
}
cout<<"———————————"<<endl;
//这里将输出 classB 的内容,因为指向的虚函数表classB的
ClassA *pb = new ClassB;
pVirtualTable = (int*)(*(int*)pb);
for(int i = 0; i < 3; i++)
{
pf = (pFun)pVirtualTable[1];
pf();
}
//看了这个code后是不是对多态的原理一下子豁然开朗了,
// 1、基类类定义虚函数,同时就声明了虚函数表,将虚函数的地址填入表中 (这个表示类静态成员,下面将用code证明)
// 2、子类继承基类,并且子类也有自己的静态虚函数表成员,如果重写基类虚函数将导致子类虚函数表中函数地址修改
// 3、用基类指针 指向子类的对象内存,其实在c/C++ 指针其实都是差不多的意思,都是指向一段内存的起始地址,
// 只是不同类型的指针,这个指针能够操作的内存宽度不同而已
// 4、由于基类指针指向的是 new 子类的内存,在new的时候内存中存的内容是子类的,当然这样就虚函数表也是子类的,这样导致调用虚函数时
// 在查表的时候就是子类重写虚函数了,虽然我们用的是基类的指针。
//证明 虚函数表是 静态成员,
// 思路, 如果每个对象的虚函数表指向同一个地址,那就是说明这个虚函数表是静态的了
ClassA ta, tb;
pVirtualTable = (int*)(*(int*)&ta);
cout << "size of ClassA "<<sizeof(ClassA)<<endl;
cout << "Obj ta start address: "<< &ta <<endl;
cout << "virtual table address: "<<pVirtualTable <<endl;
pVirtualTable = (int*)(*(int*)&tb);
cout << "Obj tb address: "<< &tb <<endl;
cout << "virtual table address: "<< pVirtualTable <<endl;
//运行你会看到上面两个虚函数表的内容是一样的
}
看了上面的code后你是不是对这个动态子类化豁然开朗了? 它就是先声明一个子类对象获取子类对应的虚函数表地址,然后复制给基类对象的虚函数表,这样就成功动态子类化了。
当然这个方法也是有局限性的,个人总结如下:
1、小心子类重载的虚函数不能有操作只有子类才有的数据,因为父类对象不可能有这个数据的。所以第一个code做了一个子类size要等于父类的size的限制,就是为了这方面的安全考虑
2、如果子类是多重继承,那么你可能要修改的虚函数表就不止一个了,要根据父类个数去修改,修改顺序按你继承的顺序
3、如果不是 new 出来的对象那么就算成功修改了虚函数表也没用,至于原因只能各种猜测了(可能编译只对指针做了特殊处理)。至于证明修改了虚函数表也没有用可以用如下code证明
#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;
}
差不多到这里了,求各位看官给看法,特别是对第三点限制(非new对象,修改虚函数表也不能多态的原因)。