看过关于C++的书籍,知道了如果类中有虚函数,那么对象的前四个字节就是一个指向称为虚函数表的指针。
使用了多继承之后的情况是怎样呢?我以前并没有认真考虑过这个问题,只是想当然地认为多个基类的虚函数表会合成一张表。
但是后来看过一些关于C++内存对象模型的文章之后我知道我错了。这些文章我大体看懂了,但是有些细节不很明白,于是决定自己写代码来进行实验。
我用VC7写了一个动态库,在某些程度上模仿了COM:
interf.h:
#define IID_X 1
#define IID_Y 2
class IBase
{
public:
virtual int __stdcall Query(int iid,void ** ppv)=0;
virtual int __stdcall AddRef()=0;
virtual int __stdcall Release()=0;
};
class IX:public IBase
{
public:
virtual unsigned long __stdcall FuncX1()=0;
};
class IY:public IBase
{
public:
virtual unsigned long __stdcall FuncY1()=0;
};
class XY:public IX,IY
{
public:
XY();
protected:
int i;
int j;
int m_ref;
virtual ~XY();
public:
virtual int __stdcall Query(int iid,void ** ppv);
virtual int __stdcall AddRef();
virtual int __stdcall Release();
virtual unsigned long __stdcall FuncX1();
virtual unsigned long __stdcall FuncY1();
};
extern "C"
{
int _declspec(dllexport) NewObject(IX ** ppv);
}
xxyy.cpp:
#include "stdafx.h"
#include "interf.h"
int XY::Query(int iid,void ** ppv)
{
if(IID_X==iid)
{
*ppv=(IX *)this;
((IX*)(*ppv))->AddRef();
}
else if(IID_Y==iid)
{
*ppv=(IY *)this;
((IY*)(*ppv))->AddRef();
}
else
{
*ppv=NULL;
return 0;
}
return 1;
}
int XY::AddRef()
{
return ++m_ref;
}
int XY::Release()
{
if(0==--m_ref)
delete this;
return m_ref;
}
XY::XY()
{
m_ref=1;
}
XY::~XY(){}
unsigned long XY::FuncX1()
{
return (unsigned long)this;
}
unsigned long XY::FuncY1()
{
return (unsigned long)this;
}
MultiExt.cpp:
// MultiExt.cpp : 定义 DLL 应用程序的入口点。
//
#include "stdafx.h"
#include "interf.h"
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
return TRUE;
}
int _declspec(dllexport) NewObject(IX ** ppv)
{
*ppv=(IX *)new XY;
if(NULL==*ppv)
return 0;
return 1;
}
这是一个动态库。编译好之后,又用PureBasic写了一段程序来调用这个动态库:
ProtoType ProtoNewObject(ppv)
Interface IBase
Query(iid,ppv)
AddRef()
Release()
EndInterface
Interface IX Extends IBase
FuncX1()
EndInterface
Interface IY Extends IBase
FuncY1()
EndInterface
OpenLibrary(0,"MultiExt.dll")
NewObject.ProtoNewObject=GetFunction(0,"NewObject")
x.IX
y.IY
NewObject(@x)
x\Query(2,@y)
Debug x
Debug y
y\Release()
x\Release()
CloseLibrary(0)
运行这个程序,就会发现一些让人惊讶的内容,2个Debug输出的结果分别为:4136880、4136884。原来x指针和y指针指向了不同的地方。更明确地说,是y比x靠后4字节。x是由动态库函数NewObject获取的,y是由x调用Query得来的。Query做了什么呢?看看上面的xxyy.cpp,原来Query将对象的指针强制转换成为(IY*)并通过参数输出。我原以为对指针进行类型转换只是对指针算术产生一些影响,没想到此时的类型转换竟然改变了指针的值。那么为什么和x相比y偏移了4个字节呢?
看了一些文章知道了原来在多继承的时候,对象中已经不止一个虚函数表指针了。像上面的示例程序,XY的对象中包含两个虚函数表指针,一个来自IX,一个来自IY。由于IX和IY均没有数据成员,所以属于IY的虚函数表指针就放在了属于IX的虚函数表指针的后面,属于IX的虚函数表指针放在了对象的最前面。基于这种原理,x和y的值相差4。
但是x与y的不同带来了一个问题,那就是y没有指向对象的首地址,可是通过一个接口指针调用虚成员函数时,首先通过查找虚函数表来确定函数指针,这一点没有问题,因为y就指向 指向IY虚函数表的指针(有点拗口)。问题是调用函数时系统会把接口指针作为this参数传递给函数,然而y已经不能代表对象的首地址,也就是说传递给函数的this参数不是对象的首地址。我想了想,应该是编译器在函数里“偷偷摸摸”加了一句:“this-=4;”,大家会说this是const,的确,不过那仅仅针对程序员,编译器可以为所欲为。
当然,这只是一个想法,要证明这个想法得进行实验,得到能够说明问题的数据,我在上面的PureBasic程序中加了一些语句,修改后的程序如下(如果对下文的内容不很明白可以参看《再谈PureBasic的Interface》):
ProtoType ProtoNewObject(ppv)
Interface IBase
Query(iid,ppv)
AddRef()
Release()
EndInterface
Interface IX Extends IBase
FuncX1()
EndInterface
Interface IY Extends IBase
FuncY1()
EndInterface
ProtoType ProtoQuery(this,iid,ppv)
ProtoType ProtoAddRef(this)
ProtoType ProtoRelease(this)
ProtoType ProtoFuncY1(this)
Structure IYVTable
Query.ProtoQuery
AddRef.ProtoAddRef
Release.ProtoRelease
FuncY1.ProtoFuncY1
EndStructure
OpenLibrary(0,"MultiExt.dll")
NewObject.ProtoNewObject=GetFunction(0,"NewObject")
x.IX
y.IY
NewObject(@x)
x\Query(2,@y)
*object.LONG=y
*iyvt.IYVTable=*object\l
Debug *iyvt\FuncY1(11)
y\Release()
x\Release()
CloseLibrary(0)
我们给FuncY1传入11,Debug显示结果7。这足够证明在XY::FuncY1()中编译器加入了“this-=4;”。当然不一定总是减4,减几得根据虚函数表指针出现在对象中的位置来定,例如,我们给class IX添加一个数据成员long x,再用上面的PureBasic程序实验,传入11,得到Debug输出“3”,原来在属于IX的虚函数表指针和属于IY的虚函数表之间夹了一个long x,所以只有将this自减8才能指向对象的首地址。
C++的编译程序将这些复杂的东西都隐藏了起来,所以我们写起程序来轻松了很多。