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 }
现在我们直接看内存布局: