我们知道C++支持的虚函数是通过为每个类提供一个虚函数来实现的,它的内存状态究竟是怎样的呢?
首先我们实现两个拥有虚函数的类:ClassA和ClassB,其中ClassB是ClassA的派生类,具体实现如下:
//ClassA
#include <iostream.h>
#include <stdio.h>
class ClassA
{
public:
ClassA()
{
x=0;
y=0;
}
virtual void Func1(void)
{
printf("This is ClassA::Func1( )/n");
return;
}
virtual void Func2(void)
{
printf("This is ClassA::Fun2( )/n");
return;
}
int x;
int y;
};
//ClassB
#include <iostream.h>
#include <stdio.h>
class ClassB:public ClassA
{
public:
virtual void Func2(void)
{
printf("This is ClassB::Func2()/n");
return;
}
virtual void Func3(void)
{
printf("This is ClassB::Func3()/n");
return;
}
};
容易看出ClassA的成员包括
虚函数: ClassA::Func1( ); ClassA::Func2( );
成员变量: int x; int y;
ClassB的成员包括
虚函数: ClassB::Func2( ); ClassB::Func3( );
成员变量: int x; int y; (由ClassA继承的)
而且ClassB重载了ClassA的第二个虚函数,自己实现了一个虚函数Func3( )。
因而我们知道ClassA的对象的内存结构可如下表示:
&ClassA::Func1( )
|
&ClassA::Func2( )
|
ClassA:: Func1( ) {……}
|
ClassA:
VTable
|
X
|
Y
|
ClassA:: Func2( ) {……}
|
ClassB对象的结构是类似的,但是要注意ClassB::Func2()是继承ClassA的。
下面先考虑怎么样才能得到VTbale 呢?
首先在程序中声明一个ClassA的对象
ClassA caa; //事实上我们已经可以得到caa的地址了
int *pInta=NULL;
int *pCaa=(int *)(&caa); //指向对象caa的指针
int * const &pVtable=(int *)(*pCaa); //声明一个指向int型指针的引
//用,并用caa对象的前4个字
//节的内容初始化;
void *pVoid;
void (*pFun1)(void);
void (*pFun2)(void);
void (*pFun3)(void);
好了,我们知道在对象caa内存空间的前面4个字节存放的就是ClassA类的虚函
数表的指针,而我们已经得到了,就是pVtable了。
可以用下面的语句来验证一下:
pInta=(int *)(&caa);
printf("The memory Content of Class caa:/n");
printf("/t vtable : %x/n",*(pInta));
printf("/t x : %d/n",*(pInta+1));
printf("/t y : %d/n",*(pInta+2));
恩,运行的结果如下:
The memory Content of Class caa:
vtable : 415394
x : 0
y : 0
成功的取得了caa对象的Vtable之后,我们的工作就是通过Vtable来得到
虚函数表了。
这已经是很简单了:
pVoid=(int *) (*pVtable);
pFun1=(void (*)(void))pVoid; //这就是指向第一个虚函数函数
//代码段的指针了
pVoid=(int *) (*(pVtable+1));
pFun2=(void)(*)(void))pVoid; //这就是指向第二个虚函数函数
//代码段的指针了
然后我们执行通过这两个指针来执行一下函数以便验证我们确实取得了想要
虚函数的指针。
pFun1();
pFun2();
可以得到输出为:
This is ClassA::Func1( )
This is ClassA::Func2( )
当然,我们也可以打印出这几个虚函数的地址,这样就可以和ClassB的VTable
相比较了(J) 。
同样的道理可以得到ClassB的虚函数表,如果我们打印出ClassA和ClassB
的各个虚函数的函数地址的话,很容易发现ClassB的Vtable的第一个指针——
指向ClassB的第一个虚函数的代码段,是和ClassA的Vtable的第一个指针的内
容是相同的。哈哈,因为ClassB是继承ClassA的,而且在ClassB中我们并没有
重载函数 virtual ClassA:Finc1( );事实上它们指向的是同一个函数体的。
注释:程序的运行环境Intel 80X86/WinXP/Visual C++6.0,在程序中,我们使用了
int * pInt; void (*)(void) ; void *pVoid ;int;等指针和类型对内存进行了频
繁的转换,因为在我们的环境中
sizeof(int)==sizeof(int *)=sizeof(void (*)(void))==4;
也就是这些类型都占用了4个字节的内存单位,因而在他们之间的转换并没有引
起内存泄露等等错误,正如我们确实作到的,呵呵!
其次就是我们使用了指针引用
int * const &pVtable=(int *)(*pCaa);
而没有简单使用
int *pInt ; 指向相同的内容呢?
这主要是因为在后面我们要用到
pVoid=(int *) (*(pVtable+1));
来取得第i个虚函数的指针,如果简单的应用
pVoid=(int *)(*(pInt+1));
的话就达不到目标了,看到其中的差别了吗?
对了,就是因为指针变量pInt本身在内存中的位置已经和VTable没有什么关系了
(当然,这么说多少有点武断J,因为毕竟二者都是在main 函数的运行栈中),
虽然pInt和VTable中的内容是一样的,但是pInt++返回的将是pInt在内存中的
位置的下一个位置,是得不到下一个虚函数的指针的。那(*pInt)++得到的是什么
呢?不要认为是指向第二个虚函数的指针啊,这条语句所做的不过是将由pInt存
储的第一个虚函数的代码地址再自加而已,如此一来,我们不但得不到第二个虚
函数的地址,连第一个已得到的虚函数的地址都被破坏了!!
再次,
int * const &pVtable=(int *)(*pCaa);
为什么要用const 修饰呢?注意我们取得的(*pCaa)返回的已经不是一个变量了,
而是一个右值,在C++ 中,允许定义一个常量的引用,过程是先使用常量构造一
个临时变量(该临时变量的生命周期和将要定义的引用变量一致),再用该临时变
量来初始化我们定义的引用变量,前提是必须要使用const 修饰,这样就防止了
临时变量被用户修改了,而这通常是不适合的。