C++虚基类的实现机制:笔记 && C++学习笔记(10)——虚基类的作用

在《深度探索C++对象模型》里,有一个问题,也是去公司面试的时候那些技术人员常问的问题:在C++中,obj是一个类的对象,p是指向obj的指针,该类里面有个数据成员mem,请问obj.mem和p->mem在实现和效率上有什么不同。


答案是:只有一种情况下才有重大差异,该情况必须满足以下3个条件:

(1)、obj 是一个虚拟继承的派生类的对象
(2)、mem是从虚拟基类派生下来的成员
(3)、p是基类类型的指针

当这种情况下,p->mem会比obj.mem多了两个中间层。(也就是说在这种情况下,p->mem比obj.mem要明显的慢,呵呵)

WHY?

如果好奇心比较重的话,请往下看 :)


1、虚基类的使用,和为多态而实现的虚函数不同,是为了解决多重继承的二义性问题。

举例如下:

class A
{
public:
    int a;
};

class B : virtual public A
{
public:
   int b;
};

class C :virtual public A
{
public:
   int c;
};

class D : public B, public C
{
public:
   int d;
};

上面这种菱形的继承体系中,如果没有virtual继承,那么D中就有两个A的成员int a;继承下来,使用的时候,就会有很多二义性。而加了virtual继承,在D中就只有A的成员int a;的一份拷贝,该拷贝不是来自B,也不是来自C,而是一份单独的拷贝,那么,编译器是怎么实现的呢??

在回答这个问题之前,先想一下,sizeof(A),sizeof(B),sizeof(C),sizeof(D)是多少?(在32位x86的linux2.6下面,或者在vc2005下面)

在linux2.6下面,结果如下:sizeof(A) = 4; sizeof(B) = 12; sizeof(C) = 12; sizeof(D) = 24

sizeof(B)为什么是12呢,那是因为多了一个指针(这一点和虚函数的实现一样),那个指针是干嘛的呢?

那么sizeof(D)为什么是24呢?那是因为除了继承B中的b,C中的c,A中的a,和D自己的成员d之外,还继承了B,C多出来的2个指针(B和C分别有一个)。再强调一遍,D中的int a不是来自B也不是来自C,而是另外的一份从A直接靠过来的成员。

如果声明了D的对象d: D d;
那么d的内存布局如下:

vb_ptr: 继承自B的指针
 
int b: 继承自B公有成员
 
vc_ptr:继承自C的指针
 
int c: 继承自C的共有成员
 
int d: D自己的公有成员
 
int a: 继承自A的公有成员
 


那么以下的用法会发生什么事呢?

D dD;
B *pb = &dD;
pb->a;

上面说过,dD中的int a不是继承自B的,也不是继承自C的,那么这个B中的pb->a又会怎么知道指向的是dD内存中的第六项呢?

那就是指针vb_ptr的妙用了。原理如下:(其实g++3.4.3的实现更加复杂,我不知道是出于什么考虑,而我这里只说原理,所以把过程和内容简单化了)

首先,vb_ptr指向一个整数的地址,里面放的整数是那个int a的距离dD开始处的位移(在这里vb_ptr指向的地址里面放的是20,以字节为单位)。编译器是这样做的:

首先,找到vb_ptr(这个不用找,因为在g++中,vb_ptr就是B*中的第一项,呵呵),然后取得vb_ptr指向的地址的内容(这个例子是20),最后把这个内容与指针pb相加,就得到pb->a的地址了。

所以说这种时候,用指针转换多了两个中间层才能找到基类的成员,而且是运行期间。

由此也可以推知dD中的vb_ptr和vc_ptr的内容都是一样的,都是指向同一个地址,该地址就放20(在本例中)

如下的语句呢:

A *pa = &dD;
pa->a = 4;

这个语句不用转换了,因为编译器在编译期间就知道他把A中的成员插在dD中的那个地方了(在本例中是末尾),所以这个语句中的运行效率和dD.a是一样的(至少也是差不多的)

这就是虚基类实现的基本原理。

注意的是:那些指针的位置和基类成员在派生类成员中的内存布局是不确定的,也就是说标准里面没有规定int a必须要放在最后,只不过g++编译器的实现而已。c++标准大概只规定了这套机制的原理,至于具体的实现,比如各成员的排放顺序和优化,由各个编译器厂商自己定~

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/jiangnanyouzi/archive/2009/01/06/3721091.aspx

 

 

 

 

---------------------------------------------------------------------------------------------------------------

虚基类的作用
    
当一个基类被声明为虚基类后,即使它成为了多继承链路上的公共基类,最后的派生类中也只有它的一个备份。例如:
class CBase { };
class CDerive1:virtual public CBase{ };
class CDerive2:virtual public CBase{ };
class CDerive12:public CDerive1,CDerive2{ };
则在类CDerive12的对象中,仅有类CBase的一个对象数据

虚基类的特点:

       虚基类构造函数的参数必须由最新派生出来的类负责初始化(即使不是直接继承);
       虚基类的构造函数先于非虚基类的构造函数执行。
      
   
重写“C++学习笔记(9)——使用范围运算符解决继承中的二义性问题 ”中的程序,观察虚基类的作用
代码如下:
      
/**//************************************************************************
* 混合继承:多基类继承与多重继承
************************************************************************/
#include <IOSTREAM.H>
//基类
class CBase
...{
protected:
    int a;
public:
    CBase(int na)
    ...{
        a=na;
        cout<<"CBase constructor! ";
    }

    ~CBase()...{cout<<"CBase deconstructor! ";}
};

//派生类1(声明CBase为虚基类)
class CDerive1:virtual public CBase
...{
public:
    CDerive1(int na):CBase(na)
    ...{
        cout<<"CDerive1 constructor! ";
    }
   
    ~CDerive1()...{cout<<"CDerive1 deconstructor! ";}

    int GetA()...{return a;}
};

//派生类2(声明CBase为虚基类)
class CDerive2:virtual public CBase
...{
public:
    CDerive2(int na):CBase(na)
    ...{
        cout<<"CDerive2 constructor! ";
    }
    ~CDerive2()...{cout<<"CDerive2 deconstructor! ";}
    int GetA()...{return a;}
};

//子派生类
class CDerive12:public CDerive1,public CDerive2
...{
public:
    CDerive12(int na1,int na2,int na3):CDerive1(na1),CDerive2(na2),CBase(na3)
    ...{
        cout<<"CDerive12 constructor! ";
    }
    ~CDerive12()...{cout<<"CDerive12 deconstructor! ";}
};
void main()
...{
    CDerive12 obj(100,200,300);
    //得到从CDerive1继承的值
    cout<<" from CDerive1 : a = "<<obj.CDerive1::GetA();
    //得到从CDerive2继承的值
    cout<<" from CDerive2 : a = "<<obj.CDerive2::GetA()<<endl<<endl;
}
          
       
1. 子派生类对象的值:
      

             
     从上例可以看出,在类CDerived12的构造函数初始化表中,调用了间接基类CBase的构造函数,这对于非虚基类是非法的,但对于虚基类则是合法且必要的。
  对于派生类CDerived1和CDerived2,不论是其内部实现,还是实例化的对象,基类CBase是否是它们的虚基类是没有影响的。受到影响的是它们的派生类CDerived12,因为它从两条路径都能到达CBase。
        
2. 运行结果:
            

              
    由此可知,其公共基类的构造函数只调用了一次,并且优先于非基类的构造函数调用;并且发现,子派生类的对象obj的成员变量的值只有一个,所以,当公共基类CBase被声明为虚基类后,虽然它成为CDerive1和CDerive2的公共基类,但子派生类CDerive12中也只有它的一个备份。可以仔细比较与例2的运行结果有什么不同。 


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/livelylittlefish/archive/2008/03/11/2171267.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章