• C++中的类继承(4)继承种类之单继承&多继承&菱形继承


     1、单继承与多继承

      单继承是一般的单一继承,一个子类只 有一个直接父类时称这个继承关系为单继承。这种关系比较简单是一对一的关系:

      多继承是指 一个子类有两个或以上直接父类时称这个继承关系为多继承。这种继承方式使一个子类可以继承多个父类的特性。多继承可以看作是单继承的扩展。派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个单继承。多继承下派生类的构造函数与单继承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。同时,派生类的参数个数必须包含完成所有基类初始化所需的参数个数。在子类的内存中它们是按照声明定义的顺序存放的,下面的截图将清晰看到。

    菱形继承也叫钻石继承

    但是多继承存在一个问题,要想研究这个问题,我们先从单继承讲起。来看内存空间:

     1 class Base
     2 {
     3 public:
     4 Base() {
     5 cout << "B()" << endl;
     6 }
     7 int b1;
     8 };
     9 class Derive : public Base
    10 {
    11 public:
    12 Derive() {
    13 cout << "D()" << endl;
    14 }
    15 int d1; 
    16 };
    17 int main()
    18 {
    19 Test();
    20 getchar();
    21 return 0;
    22 }

    多继承的内存空间:

     1 class Base
     2 {
     3 public:
     4 Base() {
     5 cout << "B()" << endl;
     6 }
     7 int b1;
     8 };
     9 class C
    10 {
    11 public:
    12 C() {
    13 cout << "C()" << endl;
    14 }
    15 int c;
    16 };
    17 class Derive : public Base, public C
    18 {
    19 public:
    20 Derive() {
    21 cout << "D()" << endl;
    22 }
    23 int d1; 
    24 };

    菱形继承内存中数据分布:

     1 class A
     2 {
     3 public:
     4 A() {
     5 cout << "A()" << endl;
     6 }
     7 int a;
     8 };
     9 class Base:public A
    10 {
    11 public:
    12 Base() {
    13 cout << "B()" << endl;
    14 }
    15 int b1;
    16 };
    17 class C: public A
    18 {
    19 public:
    20 C() {
    21 cout << "C()" << endl;
    22 }
    23 int c;
    24 };
    25 class Derive : public Base, public C
    26 {
    27 public:
    28 Derive() {
    29 cout << "D()" << endl;
    30 }
    31 int d1; 
    32 };

    在A类中初始化int a=4则可清楚的看到菱形继承中内存分布

    所以子类Derive中有两份A类中的数据成员,这造成了访问二义性和数据冗余的问题

    这就是我前面说的多继承存在的问题。可以这样访问

    1 tmp.C::a=4;
    2 tmp.Base::a=5;

    什么是对象模型

    有两个概念可以解释C++对象模型:

    1、语言中直接支持面向对象程序设计的部分。
    2、对于各种支持的底层实现机制。

    菱形继承对象模型如下:

    2、虚继承

    还有另外一个方法解决这个问题,我们要用到一种新的继承方法:虚继承 是面向对象编程中的一种技术,是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类,它可共享的特性,避免了拷贝多份相同的数据,从而解决菱形继承的二义性和数据冗余的问题。看下面这段代码:

     1 class Base
     2 {
     3 public:
     4     Base() {
     5         cout << "B()" << endl;
     6     }
     7     int b1;
     8 };
     9 class Derive : virtual public Base
    10 {
    11 public:
    12     Derive() {
    13         cout << "D()" << endl;
    14     }
    15     int d1; 
    16 };
    17 void Test()
    18 {
    19     Derive tmp;
    20     tmp.d1 = 1;
    21     tmp.b1 = 2;
    23 }
    24 int main()
    25 {
    26     Test();
    27     getchar();
    28     return 0;
    29 }

    虚拟继承的关键字---virtual

    下图为单继承的内存分布:

    图中的偏移量地址其实为一个指向基类偏移量表的指针。

    虚拟继承是虽然不是多重继承中特有的概念。但虚拟基类是为解决多重继承而出现的。
    下图可以看出虚基类和非虚基类在多重继承中的区别

      虚继承的提出就是为了解决多重继承时可能会保存两份副本的问题,也就是说用了虚继承就只保留了一份副本,但是这个副本是被多重继承的基类所共享的,该怎么实现这个机制呢?待我慢慢道来

    1.类中无其它数据成员时

     1 class B //基类
     2 {
     3 public:
     4     B()
     5     {
     6         cout << "B" << endl;
     7     }
     8     ~B()
     9     {
    10         cout << "~B()" << endl;
    11     }
    12 };
    13 class C1 :virtual public B
    14 {
    15 public:
    16     C1()
    17     {
    18         cout << "C1()" << endl;
    19     }
    20     ~C1()
    21     {
    22         cout << "~C1()" << endl;
    23     }
    24 };
    25 class C2 :virtual public B
    26 {
    27 public:
    28     C2()
    29     {
    30         cout << "C2()" << endl;
    31     }
    32     ~C2()
    33     {
    34         cout << "~C2()" << endl;
    35     }
    36 };
    37 class D :public C1, public C2
    38 {
    39 public:
    40     D()
    41     {
    42         cout << "D()" << endl;
    43     }
    44     ~D()
    45     {
    46         cout << "~D()" << endl;
    47     }
    48 };
    49 
    50 int main()
    51 {
    52     cout << sizeof(B) << endl;
    53     cout << sizeof(C1) << endl;
    54     cout << sizeof(C2) << endl;
    55     cout << sizeof(D) << endl;
    56     return 0;
    57 }

    输出结果为:

    结果分析:首先,基类中除了构造函数和析构函数没有其他成员了,所以 sizeof(B) = 1;这里再提一个问题有的初学者可能会问为什么为1呢?首先类在内存中的存储是这样的:

     如果有一个类Base定义如下例:

     1 class Base
     2 {
     3     public:
     4     void fun();
     5     int b;
     6 };
     7 int Test()
     8 {
     9     Base b1,b2,b3;
    10 }

    那么在内存中的对象模型如下图:

     

    成员函数是单独存储的,并且所有为类对象公用。
      类的实例化要求每个实例对象在有独立无二的地址空间,而空类也可以实例化。编译器要区分开所有的类对象,就要给对象一个地址,只是一个占位符,表示这个对象存在,并且让编译器给这个对象分配地址。至于占多少位,由编译器决定,这里空类的大小为1,是在VS2015中,其他编译器可能不同。

       由于C1与C2都是虚拟继承,故会在C1,C2内存起始处存放一个vbptr,为指向偏移量表的指针。所以C1和C2大小为4,这就是指针的大小了。D的大小就是继承的两个指针的大小了。这里再详细解释一下偏移量表,是什么的偏移量呐?

    我们在main函数中生成一个C1类对象c1:

    1 int main()
    2 {
    3     C1 c1;
    4     return 0;
    5 }

     内存布局如下:

      由图可以看出,c1占了四个字节,存了一个指针变量,指针变量的内容就是 c1 的 vbptr 指向的偏移量表的地址。偏移量表有八个字节,分别存的为0和4。 那么0和4代表的都是什么呢? 虚基类表存放的为两个偏移地址,分别为0和4。其中0表示c1对象地址相对于存放vbptr指针的地址的偏移量。(因为vbptr指针是属于c1对象的,c1对象地址相对于vbptr指针的地址偏移量为0。这里我把它这个表叫做偏移量表,避免与后面多态中的虚表混淆。)

    而4表示c1对象中基类对象部分相对于存放vbptr指针的地址的偏移量,可以用 &c1(B)-&vbpt 表示,其中&c1(B)表示对象c1中基类B部分的地址。

     c2的内存布局与c1一样,因为C1,C2都是虚继承自B基类,且C1,C2都没有独自的数据成员。

      总结:C1,C2是虚继承自基类B,所以编译器会给C1,C2中生成一个指针vbptr指向一个偏移量表,即指针vbptr的值是偏移量表的地址。表中存放对象相对于偏移量表指针的偏移量。表中分两部分,第一部分存储的是对象相对于存放vptr指针的偏移量,可以用&(对象名)->vbptr_(对象名)来表示。对c1对象来说,可以用&c1->vbprt_c1来表示。表的第二部分存储的是对象中基类对象部分相对于存放vbptr指针的地址的偏移量,我们知道在本例中基类对象与指针偏移量就是指针的大小。

    下面再来看看D的内存结构:

    如图所示,d中存放了两个虚基类指针,每个虚基类表中存储了偏移量。形象的内存布局如下图:

     

    2.类中加数据成员

    下面看一下拥有独立数据成员的类的虚继承,可以更清晰的理解内存布局:

     1 #include <iostream>
     2 using namespace std;
     3 
     4 class B
     5 {
     6 public:
     7     B()
     8     {
     9         cout << "B" << endl;
    10     }
    11     ~B()
    12     {
    13         cout << "~B()" << endl;
    14     }
    15     int b;
    16 };
    17 class C1 :virtual public B
    18 {
    19 public:
    20     C1()
    21     {
    22         cout << "C1()" << endl;
    23     }
    24     ~C1()
    25     {
    26         cout << "~C1()" << endl;
    27     }
    28     int c1;
    29 };
    30 class C2 :virtual public B
    31 {
    32 public:
    33     C2()
    34     {
    35         cout << "C2()" << endl;
    36     }
    37     ~C2()
    38     {
    39         cout << "~C2()" << endl;
    40     }
    41     int c2;
    42 };
    43 class D :public C1, public C2
    44 {
    45 public:
    46     D()
    47     {
    48         cout << "D()" << endl;
    49     }
    50     ~D()
    51     {
    52         cout << "~D()" << endl;
    53     }
    54     void fun()
    55     {
    56         b = 0;
    57         c1 = 1;
    58         c2 = 2;
    59         d = 3;
    60     }
    61     int d;
    62 };
    63 
    64 int main()
    65 {
    66     cout << sizeof(B) << endl;
    67     cout << sizeof(C1) << endl;
    68     cout << sizeof(C2) << endl;
    69     cout << sizeof(D) << endl;
    70     D d;
    71     d.fun();
    72     return 0;
    73 }

    输出结果为:

    B占四个字节没有问题,因为B类中有int b数据成员,所以B类占四个字节。 C1,C2是虚继承自B类的,所以C1,C2的内存布局是相似的,在这里我只分析一下C1。 我在C1类中加一个Fun成员函数,为了更清楚的看到内存布局:

     1 class C1 :virtual public B
     2 {
     3 public:
     4     C1()
     5     {
     6         cout << "C1()" << endl;
     7     }
     8     ~C1()
     9     {
    10         cout << "~C1()" << endl;
    11     }
    12     void Fun()
    13     {
    14         b = 5;
    15         c1 = 6;
    16     }
    17     int c1;
    18 };
    19 int main()
    20 {
    21     C1 c1;
    22     c1.Fun();
    23     return 0;
    24 }

    在main函数中生成对象c1,C1=int+int+指针=4+4+4=12,再来看一看内存布局:

    现在来看看D类的内存布局:

     1 class D :public C1, public C2
     2 {
     3 public:
     4     D()
     5     {
     6         cout << "D()" << endl;
     7     }
     8     ~D()
     9     {
    10         cout << "~D()" << endl;
    11     }
    12     void fun()//fun()函数主要帮助我们看D类的内存布局
    13     {
    14         b = 0;//基类数据成员
    15         c1 = 1;//C1类数据成员
    16         c2 = 2;//C2类数据成员
    17         d = 3;//D类自己的数据成员
    18     }
    19     int d;
    20 };

    内存布局如下:

    下面再看看多重虚拟继承

     1 class A
     2 {
     3 public:
     4     A() {
     5         cout << "A()" << endl;
     6     }
     7     int a ;
     8 };
     9 class Base : virtual public A
    10 {
    11 public:
    12     Base() {
    13         cout << "B()" << endl;
    14     }
    15     int b1;
    16 };
    17 class C:virtual public A
    18 { 
    19 public:
    20     C() {
    21         cout << "C()" << endl;
    22     }
    23     int c;
    24 };
    25 class Derive : virtual public Base, virtual public C
    26 {
    27 public:
    28     Derive() {
    29         cout << "D()" << endl;
    30     }
    31     int d1; 
    32 };
    33 void Test()
    34 {
    35     Derive tmp;
    36     tmp.d1 = 1;
    37     tmp.b1 = 2;
    38     tmp.c = 3;
    39     tmp.a = 4;
    40 }
    41 int main()
    42 {
    43     Test();
    44     getchar();
    45     return 0;
    46 }

    现在我们直接看内存布局:

     

  • 相关阅读:
    MyBatis(六)、事务控制
    MyBatis(五)、CRUD操作与重要参数及标签
    Java学习笔记-2020学习目录
    Java学习笔记(十四) 字符串
    Java学习笔记(十三) 面向对象基础
    Java学习笔记(十二) 基础知识练习
    Java学习笔记(十一) Debug
    Java学习笔记(十) 方法
    Java学习笔记(九) 数组
    Java学习笔记(八) IDEA
  • 原文地址:https://www.cnblogs.com/33debug/p/6718672.html
Copyright © 2020-2023  润新知