理解虚继承

本篇博客主要对继承的对象模型以及虚拟继承进行一个整理和总结。

单继承

  • 方式一:单级派生

理解虚继承

//单继承
class A
{
public:
     int _a;
};

class B :public  A
{
public:
     int _b;
};
int main()
{
     B b;
     b._a = 1;
     b._b = 2;
     return 0;
}

在上面的代码中,类B公有继承了类A,通过监视窗口和内存,可以看到b对象的内存模型:首先是基类的成员_a,其次是子类B自己的成员_b; sizeof(b)=8.
理解虚继承

  • 方式二:多级派生

理解虚继承

class A
{
public:
     int _a;
};

class B :public  A
{
public:
     int _b;
};

class C :public B
{
public:
     int _c;
};
int main()
{
     C c;
     c._a = 1;
     c._b = 2;
     c._b = 3;
     return 0;
}

在这种情况下:A为基类,B公有继承A,C公有继承B。按照方式一的对象模型,我们可以推测,对于方式二,c的成员有三个,在内存中的排布方式应该为:最上层基类的成员、中间是B的成员_b,最下层是C类自己的成员_c。sizeof©=12。打开调试窗口,观察到内存布局与我们猜测的一样
理解虚继承

多继承

  • 普通

理解虚继承

//多继承

class B1
{
public:
     int _b1;
};

class B2
{
public:
     int _b2;
};

class C :public  B1,public B2
{
public:
     int _c;
};
int main()
{
     C c;
     c._b1 = 1;
     c._b2 = 2;
     c._c = 3;
     return 0;
}

同样,有了前面对单继承分析的基础,对于多重继承,也不难理解。子类C继承了两个基类,分别是B1,B2,那么对于C类创建的对象c,它的对象模型一个包括三部分:按照继承的顺序,最上面一个是B1类的成员,中间是B2类的成员,最下面是C类自己的成员_c,sizeof©=12;
理解虚继承

  • 菱形继承

理解虚继承
在引出“虚继承”这个概念之前,首先来看下面一段代码

//菱形继承
class B
{
public:
     int _b;
};

class C1:public B
{
public:
     int _c1;
};
class C2:public B
{
public:
     int _c2;
};

class D :public  C1,public C2
{
public:
     int _d;
};

int main()
{
      D d;
     d._b = 1;//错误,对_b的访问不明确
     d.C1::_b = 1;//明确指定是哪个作用域中的_b
     d.C2::_b = 2;
     return 0;
}

当用对象d直接对_b成员进行赋值时,出现错误。那么为什么会出现这样的问题呢?
理解虚继承
这种菱形继承有一个很大的问题存在:二义性。 在这种继承关系中,D不仅继承了来自C1中的_b,还继承了C2中的_b,那么当定义D类的对象d时,直接对d中的_b进行赋值,究竟是C1中的_b呢?还是C2中的_b?

虚拟继承是多重继承中特有的概念,那么为什么要有虚拟继承?
  • 首先要明白菱形继承的本质:
    一个派生类有多个直接基类,这些直接基类又有一个共同基类。
  • 这种方式的共同问题:
    最终的派生类中会保留间接共同基类数据成员的多份同名成员,即“数据冗余”。且直接访问会产生二义性。
  • 解决办法:

(1) 访问时加上类的作用域限定符
(2)只保留一份间接共同基类的同名成员
对于办法(1),虽然解决了“二义性”问题,但是“数据冗余”问题还存在,因为还是保存着多份基类的同名成员。
对于办法(2),从根本上解决了二义性问题和“数据冗余”问题

对于上面的菱形继承,由于类D继承自C1,C2,而C1和C2都继承自基类B,所以D中会保存两份变量_b,分别是C1与C2中的。sizeof(d)=20,也证明了我们的判断。实际上_b变量只需要保存一份。为了节约内存空间,可以将C1,C2对B的继承定义为虚拟继承,那么B就成了虚基类。
虚拟继承具体实现方式:在C1,C2对B的继承方式前添加关键字virtual

class B
{
public:
	int _b;
};
class C1:virtual public B
{
public:
	int _c1;
};
class C2:virtual public B
{
public:
	int _c2;
};
class D :public  C1,public C2
{
public:
	int _d;
};

int main()
{
   D d;
   d._b = 1;
   d._c1 = 2;
   d._c2 = 3;
   d._d = 4;
   return 0;
}

首先来看,对于普通的多重继承,它的对象模型是怎样的?
理解虚继承
可以看出,d的对象模型是:先是C1类中的成员,再是C2类中的成员,最后是D类自己的成员,sizeof(d)=20。如果换成虚继承,对象模型又有什么区别呢?
理解虚继承

可以看到,_b成员只保留了一份,理论上我们推测sizeof(d)的大小为16(_b+_c1+_c2+_d),但实际上并非如此。虚继承的对象模型中,C1和C2都维护了一个指针,基类成员_b反而在最下层。思考这样一个问题:C1与C2中都有成员_b,但是在对象模型中_b不在其各自所占的内存区域,而在最下层,那C1和C2又是如何找到成员_b呢?

这里就不难理解虚基表指针的作用了,各自的虚基表指针指向一个偏移量表格,记录了相对于_b的偏移量

所以sizeof(d)的大小=24


为了更便于查看虚继承的对象模型,我们举一个单继承的例子(虽然虚拟继承是用在多重继承)

class A
{
public:
     int _a;
};

class B:virtual public A
{
public:
     int _b;
};
int main()
{
    B b;
    b._a = 1;
    b._b = 2;
    cout << "b:" << sizeof(b) << endl;
    return 0;
}

在上面的代码中,B与A是单继承的关系,B虚拟继承A,按照我们最先看的单级派生对象模型:是基类成员在最上层,然后才是子类成员。如果变为虚继承,则变化就比较明显了–>子类在上,基类在下
理解虚继承

总结

  • 普通单继承于虚拟继承区别
  1. 虚拟继承的对象模型倒立
  2. 虚拟继承对象中多了4个字节
  3. 编译器为派生类生成默认的构造函数有两个参数:空间首地址和1,其中1是虚拟继承标志
    理解虚继承
  • 对于虚基表指针和虚基表的理解
  1. 取对象前4个字节中的内容
  2. 以该内容为地址addr,取addr+4空间中的内容offset
  3. 给对象起始地址向后偏移offset字节位置赋值