1、普通的多继承情况
需要用类名加两个冒号:: 来说明成员所属的基类。
代码如下:
classA
{
public:
voidfun()
{
a= 2;
}
inta;
intx;
};
classB
{
public:
voidfun()
{
b= 5;
}
intb;
intx;
};
classC : public A, public B
{
public:
intc;
};
int_tmain(int argc, _TCHAR* argv[])
{
C*pc = new C();
pc->A::fun();
pc->B::fun();
return0;
}
我们从成员变量和成员方法两方面进行观察,
进入调试模式:查看C 对象的内容:
可以看到一个C对象实际上包含一个A对象,一个B对象,然后再加上自身特有的属性c,从A继承来的x 和从B继承来的x 并不在同一块空间中。但是由于他们的标识符(变量名)一样,所以要通过所在的基类名来说明。
注意:关于上图中的内容只是对象的逻辑模型,而不是真实的内存模型
pc->A::fun();
009A2DF3 mov ecx,dword ptr [pc]
009A2DF6 call A::fun (09A1523h)
pc->B::fun();
009A2DFB mov ecx,dword ptr [pc]
009A2DFE add ecx,8
009A2E01 call B::fun (09A152Dh)
关于方法的调用也是类似。 关于这种编译器的识别方式,我们稍作了解即可。重点在于跟后面的程序的实现原理。
2、使用虚基类的多重继承
在看虚基类的例子之前,我们看一下普通的有共同基类的情况
代码如下:
classA
{
public:
voidfunc()
{
a= 3;
}
inta;
};
classB1 : public A
{
public:
intb1;
};
classB2 : public A
{
public:
intb2;
};
classC : public B1, public B2
{
public:
intc;
};
int_tmain(int argc, _TCHAR* argv[])
{
C*pc = new C();
pc->B1::a= 5;
pc->B2::a= 6;
pc->b1= 1;
pc->b2= 2;
pc->B1::func();
pc->B1::func();
return0;
}
先看一下对象的内容,可以发现,它首先是由包含B1和B2的对象,然后再B1 和B2 中分别包含A对象。C占20个字节。
然后再看一下方法的调用情况:
pc->B1::func();
000B68F2 mov ecx,dword ptr [pc]
000B68F5 call A::func (0B1537h)
pc->B2::func();
000B68FA mov ecx,dword ptr [pc]
000B68FD add ecx,8
000B6900 call A::func (0B1537h)
调用的方法都是A中的方法,但是在找到A的过程需要明确说明:是通过B1对象还是通过B2对象。
我们再看一下使用虚基类的情况:
classA
{
public:
voidfunc()
{
a= 3;
}
inta;
};
classB1 : virtual public A
{
public:
intb1;
};
classB2 : virtual public A
{
public:
intb2;
};
classC : public B1, public B2
{
public:
intc;
};
int_tmain(int argc, _TCHAR* argv[])
{
C*pc = new C();
cout<< sizeof(C) << endl;
pc->B1::a= 5;
pc->a= 6;
pc->b1= 1;
pc->b2= 2;
pc->B1::func();
pc->func();
return0;
}
首先还是先看一下C对象的内容
发现它在原来的基础上又多了一个公共的基类对象A,此时C对象占用的空间为24个字节。需要注意的是,图中所示的成员变量只是逻辑意义上的。我们再来看一下内存中的实际数据。
B1、B2 虚继承之后会在对象开始添加两个虚指针。真正从A继承来的部分在最后存放。
然后再看方法的调用,
pc->B1::func();
00E76950 mov eax,dword ptr [pc]
00E76953 mov ecx,dword ptr [eax]
00E76955 mov edx,dword ptr [pc]
00E76958 add edx,dword ptr [ecx+4]
00E7695B mov ecx,edx
00E7695D call A::func (0E71537h)
pc->func();
00E76962 mov eax,dword ptr[pc] //eax保存对象的首地址
00E76965 mov ecx,dword ptr[eax] // 把eax前4个字节内容复制到ecx
00E76967 mov edx,dword ptr[pc] / / edx指向对象首地址
00E7696A add edx,dword ptr [ecx+4] // [ecx]处的内容是00000000, [ecx+4]处的内容是00000014(普通成员所占用的字节数),相加之后,edx指向虚基类的首地址。此时,[edx] = 6
00E7696D mov ecx,edx
00E7696F call A::func (0E71537h)
虚继承时,成员函数的调用方式不发生改变。
综上所述,在虚继承时,生成的对象的前四个字节保存一个地址,我们称它为虚指针,它指向一段保存当前对象结构数据的内存空间。我们可以称它为虚表,从而找到真正要读写和写入的变量。在后面看了多态的 内存结构之后,会更加清楚。
思考题:
classA
{
public:
voidfunc()
{
a= 3;
}
inta;
};
classB1 : public A
{
public:
intb1;
};
classB2 : public A
{
public:
intb2;
};
classC : virtual public B1, virtual public B2
{
public:
intc;
};
int_tmain(int argc, _TCHAR* argv[])
{
C*pc = new C();
pc->B1::a= 5;
pc->B2::a= 6;
pc->b1= 1;
pc->b2= 2;
pc->B1::func();
pc->B2::func();
return0;
}
这是我测试时,想到的一种情况。现在我们用之前分析的内容来分析一下C对象是如何存放的,以及虚表中会有哪些内容。
首先,这里B1和B2 都是普通的继承。而C则虚继承于B1、B2,那么B1、B2是C的虚基类。C的对象的前四个字节是一个虚指针,它指向一个虚表,虚表中应该存放两个虚基类对象相对于C对象首地址的偏移地址。
C对象内容如下:虚指针cccccccc(c 的空间,我们没有初始化) 05 00 00 00 01 00 00 00(B1继承来的内容) 06 00 00 00 02 00 00 00 (B2继承来的内容)他们的偏移地址是8和16(10h)
所以虚表中的内容应该是 00 00 00 00(前4个字节为0,可能是做验证用的,具体的以后再分析)08 00 00 00 0a 00 00 00
我们现在来验证一下