【C++】继承详解,菱形继承问题

概念

是面向对象程序设计是代码可以复用的最重要的手段,它允许程序员在保持原有的类的特性的基础下进行拓展,增加功能。 这样产生的类被称为派生类;

class Person
{
    
    
public:
void Print()
{
    
    
cout << "name:" << _name << endl; cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓 名
int _age = 18;	// 年 龄
};

// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。调    用Print可以看到成员函数的复用。
class Student : public Person
{
    
    
protected:
int _stuid; // 学 号
};

class Teacher : public Person
{
    
    
protected:
int _jobid; // 工 号
};

int main()
{
    
    
Student s; Teacher t; s.Print();
t.Print();

return 0;}

继承方式

在这里插入图片描述
注意:
1.在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中 扩展维护性不强。

三种继承方式比较:
在这里插入图片描述

赋值兼容规则

  • 基类的指针或引用可以直接指向子类的对象,反之不行(可以进行强转,但是不安全)
  • 子类的对象可以直接赋值给基类,反之不行

同名隐藏

  • 在继承体系下,子类中包含和基类中名字相同的成员函数或者成员变量,那么就构成了同名隐藏。
  • 如果含有同名隐藏,那么在使用子类对象调用此成员函数或者成员变量时,只会调用自己的,不会访问基类中的。
  • 注意:与成员函数的原形无关,与成员变量的类型无关。
#include<iostream>
using namespace std;

class A
{
    
    
    public:
        void print2(){
    
    cout << "A print2!" << endl;}
};

class B : public A
{
    
    
    public:
        void print2(int x)
        {
    
    
            cout << "B print2 !" << x << endl;
        }
};

int main()
{
    
    
    B b;
    b.print2();
    return 0;
}

编译失败!
由结果可知,已经不能从B的对象中直接用函数名访问print2()了。

子类对象的构造过程

  1. 基类中没有显示给出任何构造函数,那么子类中也可以不定义构造函数
  2. 如果基类显示定义了无参的构造函数 || 全缺省的构造函数,则子类可以自己选择是否定义构造函数。如果没用显示给出,那么编辑器就会生成一个默认的构造函数,使得子类从基类中继承下来的成员构造完整。
  3. 如果基类显示定义了构造函数,并且不是无参的构造函数 || 全缺省的构造函数,则子类必须要显示定义自己的构造函数,然后在其构造函数初始化列表的位置显示调用基类的构造函数来完成基类成员的初始化,否则编辑失败!
  4. 子类对象的构造过程:在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

菱形继承

单继承+多继承
单继承:
在这里插入图片描述
多继承:
在这里插入图片描述
菱形继承:
在这里插入图片描述
菱形继承的问题:从上面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在D的对象中B成员会有两份。

//公共基类
class N
{
    
    
public:
    N(int data1, int data2, int data3) : 
        m_data1(data1), 
        m_data2(data2), 
        m_data3(data3)
    {
    
    
        std::cout << "call common constructor" << std::endl;
    }
    virtual ~N(){
    
    }

    void    display()
    {
    
    
        std::cout << m_data1 << std::endl;
    }

public :
    int     m_data1;
    int     m_data2;
    int     m_data3;
};


class A : /*virtual*/ public N
{
    
    
public:
    A() :N(11, 12, 13), m_a(1)
    {
    
    
        std::cout << "call class A constructor" << std::endl;
    }
    ~A(){
    
    }

public :
    int m_a;
};

class B :  /*virtual*/ public N
{
    
    
public:
    B() :N(21, 22, 23),m_b(2)
    {
    
    
        std::cout << "call class B constructor" << std::endl;
    }
    ~B(){
    
    }

public :
    int m_b;
};


class C : public A ,  public B
{
    
    
public:
    //负责对基类的初始化
    C() : A(), B(),
        m_c(3)
    {
    
    
        std::cout << "call class C constructor" << std::endl;
    }
    void show()
    {
    
    
        std::cout << "m_c=" << m_c << std::endl;
    }

 public :
    int m_c;
};

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

虚拟继承和普通继承的区别:

  1. 对象中多了4个字节
  2. 为子类中的构造函数中填充一个指针
  3. 对象模型和普通的对象模型不一样,是颠倒过来的。

如何解决:

  • 在对象的前面添加4字节,用来存放虚基表指针,这个指针指向虚基表,表中存放的是偏移量,分别是子类对像对于自己的偏移量,和派生类对于基类的偏移量。
  • 通过偏移量即可访问到数据,所以只用保存一份数据即可,解决了二义性的问题。

猜你喜欢

转载自blog.csdn.net/Vcrossover/article/details/114373228
今日推荐