C++ Primer 学习笔记_36_面向对象编程(3)--继承(三):多重继承、虚继承与虚基类

C++ Primer学习笔记36_面向对象编程(3)--继承(三):多重继承、虚继承与虚基类

一、多重继承

1、单重继承——一个派生类最多只能有一个基类
2、多重继承——一个派生类可以有多个基类

class <派生类名> : <继承方式1>  <基类名1>,<继承方式2>  <基类名2>,...
{
           <派生类新定义成员>
};

(1)示例
沙发床和沙发的例子
#include <iostream>
using namespace std;

class Bed  //床类
{
public:
    Bed(int weight) : weight_(weight)
    {

    }
    void Sleep()  //睡觉
    {
        cout << "Sleep ..." << endl;
    }
    int weight_;  //重量
};

class Sofa  //沙发类
{
public:
    Sofa(int weight) : weight_(weight)
    {

    }
    void WatchTV()  //看电视
    {
        cout << "Watch TV ..." << endl;
    }
    int weight_;
};

class SofaBed : public Bed, public Sofa  //沙发床类
{
public:
    SofaBed() : Bed(0), Sofa(0)
    {
        FoldIn();
    }
    void FoldOut()
    {
        cout << "FoldOut ..." << endl;
    }
    void FoldIn()  //折叠
    {
        cout << "FoldIn ..." << endl;
    }
};

int main(void)
{
    SofaBed sofaBed;
    //sofaBed.weight_ = 10; error
    //sofaBed.weight_ = 20; error

    sofaBed.Bed::weight_ = 10;
    sofaBed.Sofa::weight_ = 20;

    sofaBed.WatchTV();
    sofaBed.FoldOut();
    sofaBed.Sleep();

    return 0;
}

运行结果:

FoldIn ...

Watch TV ...

FoldOut ...

Sleep ...

解释:不能直接写 sofaBed.weight_ = 10; 因为sofaBed 继承了Sofa 和 Bed ,实际上有weigh_的两份拷贝,这样指向不明。只能通过sofaBed.Bed::weight_ = 10; 访问,但实际上一个sofaBed逻辑上理应只有一个weight_,下面通过虚基类和虚继承可以解决这个问题


(2)派生类同时继承多个基类的成员,更好的软件重用
(3)可能会有大量的二义性,多个基类中可能包含同名变量或函数

(4)多重继承中解决访问歧义的方法:

    基类名::数据成员名(或成员函数(参数表))

    明确指明要访问定义于哪个基类中的成员

(5)如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最底层的派生类中会保留这个简介的共同基类数据成员的多份同名成员。为了解决这个问题,提出了虚继承的概念。



二、虚继承与虚基类

1、当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类中的成员(weigth_)时,将产生二义性,可以采用虚基类来解决


2、虚基类的引入

用于有共同基类的场合


3、声明

(1)以virtual修饰说明基类 

例:class B1 : virtual public BB

或者class B1 : public virtual BB


4、作用

(1)主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题.

(2)为最远的派生类提供唯一的基类成员,而不重复产生多次拷贝


5、虚基类及其派生类构造函数

(1)虚基类的成员最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
(2)在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中给出对虚基类的构造函数的调用如果未列出,则表示调用该虚基类的默认构造函数(必须有)
(3)在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用被忽略


6、类图

(1)引入Furniture类,但如果不加上virtual,则类图如下

C++ Primer 学习笔记_36_面向对象编程(3)--继承(三):多重继承、虚继承与虚基类_第1张图片

(2)如果加上virtual,则类图如下(也称菱形继承或钻石继承

C++ Primer 学习笔记_36_面向对象编程(3)--继承(三):多重继承、虚继承与虚基类_第2张图片


7、示例

#include <iostream>
using namespace std;

class Furniture  //家具类
{
public:
    Furniture(int weight) : weight_(weight)
    {
        cout << "Furniture ..." << endl;
    }
    ~Furniture()
    {
        cout << "~Furniture ..." << endl;
    }
    int weight_;  //只有一份重量
};

class Bed : virtual public Furniture  //如果不加上virtual仍然存在二义性
{
public:
    Bed(int weight) : Furniture(weight)  //每个类的构造函数,必须给出Furniture类的weight成员的构造
    {
        cout << "Bed ..." << endl;
    }
    ~Bed()
    {
        cout << "~Bed ..." << endl;
    }
    void Sleep()
    {
        cout << "Sleep ..." << endl;
    }
};

class Sofa : virtual public Furniture  //如果不加上virtual仍然存在二义性
{
public:
    Sofa(int weight) : Furniture(weight)  //每个类的构造函数,必须给出Furniture类的weight成员的构造
    {
        cout << "Sofa ..." << endl;
    }
    ~Sofa()
    {
        cout << "~Sofa ..." << endl;
    }
    void WatchTV()
    {
        cout << "Watch TV ..." << endl;
    }
};

class SofaBed : public Bed, public Sofa
{
public:
    SofaBed(int weight) : Bed(weight), Sofa(weight), Furniture(weight)  //每个类的构造函数,必须给出Furniture类的weight成员的构造
    {
        cout << "SofaBed ..." << endl;
        FoldIn();
    }
    ~SofaBed()
    {
        cout << "~SofaBed ..." << endl;
    }
    void FoldOut()
    {
        cout << "FoldOut ..." << endl;
    }
    void FoldIn()
    {
        cout << "FoldIn ..." << endl;
    }
};

int main(void)
{
    SofaBed sofaBed(5);
    sofaBed.weight_ = 10;  //没有歧义了,因为weight_只有一份
    sofaBed.WatchTV();
    sofaBed.FoldOut();
    sofaBed.Sleep();
    return 0;
}

运行结果:

Furniture ...

Bed ...

Sofa ...

SofaBed ...

FoldIn ...

Watch TV ...

FoldOut ...

Sleep ...

~SofaBed ...

~Sofa ...

~Bed ...

~Furniture ...

解释:

如果不加上virtual仍然存在二义性,下面加详细讲述二义性

加上了virtual,此时只有一份weigh_,不存在访问歧义的问题。

Bed和Sofa的顺序,和继承的顺序有关,如class SofaBed : public Bed, public Sofa 



三、继承时导致的二义性

1、this指针

    无论是通过创建成员对象还是通过继承的方式,当我们把一个类的子对象嵌入一个新类中时,编译器会把每一个子对象置于新对象中,当然,每一个子对象都有自己的this指针,在处理成员对象时可以万事俱简。但是只要引入了多重继承,一个有趣的现象就会出现:由于对象在往上转换期间出现多个类,因而对象存在多个this指针。

【例】下面代码的输出结果是什么?

#include <iostream>
using namespace std;

class base1
{
 char c[16];
public:
 void printthis1()
 {
  cout << "base1 this = " << this << endl;
 }
};

class base2
{
 char c[16];
public:
 void printthis2()
 {
  cout << "base2 this = " << this << endl;
 }
};

class number1
{
 char c[16];
public:
 void printthism1()
 {
  cout << "number1 this = " << this << endl;
 }
};

class number2
{
 char c[16];
public:
 void printthism2()
 {
  cout << "number2 this = " << this << endl;
 }
};

class mi : public base1, public base2
{
 number1 m1;
 number2 m2;
public:
 void printthis()
 {
  cout << "m1 this = " << this << endl;
  printthis1();
  printthis2();
  m1.printthism1();
  m2.printthism2();
 }
};

int main()
{
 mi MI;
 cout << "sizeof(mi) = " << sizeof(mi) << endl;
 MI.printthis();
 base1* b1 = &MI;
 base2* b2 = &MI;
 cout << "base 1 pointer = " << b1 << endl;
 cout << "base 2 pointer = " << b2 << endl;
 return 0;
}

运行结果:

sizeof(mi) = 64

m1 this = 0x7fff5f3c2890

base1 this = 0x7fff5f3c2890

base2 this = 0x7fff5f3c28a0

number1 this = 0x7fff5f3c28b0

number2 this = 0x7fff5f3c28c0

base 1 pointer = 0x7fff5f3c2890

base 2 pointer = 0x7fff5f3c28a0

解释:每个类都有打印一个this指针的函数,这些类通过多继承和组合而被装配成mi,它打印自己和其他所有子对象的地址。

    派生对象MI的起始地址和它的基类列表中的第一个类base1的地址是一致的,第二个基类base2的地址随后,接着根据声明的次序安排成员对象(number1、number2)的地址。当向base1和base2进行上行转换时(语句 base1* b1 = &MI;和 base2* b2 = &MI;),产生的指针表面上是指向同一个对象,而实际上有不同this指针,b1指向base1类的子对象,b2指向base2类的自独享。


    我们将上述代码的mian函数加上几句话

int main()
{
 mi MI;
 cout << "sizeof(mi) = " << sizeof(mi) << endl;
 MI.printthis();
 base1* b1 = &MI;
 base2* b2 = &MI;
 cout << "base 1 pointer = " << b1 << endl;
 cout << "base 2 pointer = " << b2 << endl;
 mi* b3 = &MI;
 if(b1 == b3)
 {
  cout << "b1 == b3" << endl;
 }
 if(b2 == b3)
 {
  cout << "b2 == b3" << endl;
 }
 return 0;
}

运行结果:

sizeof(mi) = 64

m1 this = 0x7ffda67683c0

base1 this = 0x7ffda67683c0

base2 this = 0x7ffda67683d0

number1 this = 0x7ffda67683e0

number2 this = 0x7ffda67683f0

base 1 pointer = 0x7ffda67683c0

base 2 pointer = 0x7ffda67683d0

b1 == b3

b2 == b3

解释:按照刚才的分析,b1与b3相等,b2与b3不等。但实际上为什么都相等呢?实际上,b1与b3的比较过程中,由于两者类型不同,会发生隐式类型转换b3(mi*类型)会隐式转换为base1*(这是b1能与b3比较的基础,反过来转换不成立),然后与b1进行比较。同理,b2与b3比较,b3会转换为base2*,故都相等。


2、菱形继承

    现在考虑如果d1和d2都是从相同的基类派生,该基类称为base。在下面的图中,d1和d2都包含base的子对象,所以mi包含基的两个子对象。

    从继承图形状上看,有时该继承层次结构称为“菱形”。没有菱形情况时,多重继承相当简单,但是只要菱形一出现,由于新类中存在重叠的子对象,麻烦就开始了。重叠的子对象增加了存储空间,这种额外开销是否成为一个问题取决于我们的设计,但它同时又引入了二义性。

C++ Primer 学习笔记_36_面向对象编程(3)--继承(三):多重继承、虚继承与虚基类_第3张图片

【例】

下列公共基类导致的二定义性如何解决?

#include <iostream>
using namespace std;

class A
{
public:
    void print()
    {
        cout << "this is x in A: " << endl;
    }
};
class B: public A {};
class C: public A {};
class D: public B, public C {};

int main()
{
    D d;
    A* pa = (A*)&d; //上行转换产生二定义
    d.print();
}

    注意:把子类的指针或引用转换成基类指针或引用是上行转换,把基类指针或引用转换成子类指针或引用是下行转换。

解答:

(1)上述main()函数中语句d.print();编译错误,可改为以下的一种:

d.B::print();
d.C::print();

若改为“d.A::print()"又会如何呢?

由于d对象中有两个A类对象,故编译会报“基类A不明确“。

(2)语句A* pa = (A*)&d;产生的二定性是由于d中含有两个基类对象A,隐式转换时不知道让pa指向哪个对象,从而出错。可改为以下的一种:

A* pa = (A*)(B*)&d;
A* pa = (A*)(C*)&d;

实施上,使用关键字virtual将共同基类A声明为虚基类,可有效解决上述问题。





参考:

C++ primer 第四版

C++ primer 第五版

你可能感兴趣的:(C++,C++,继承,面向对象编程,Primer,类与数据抽象)