C++:中的继承关系:单继承,多继承,菱形继承的详细介绍

1.继承关系(is a关系)

1.1概述

简单来说:就是父亲的东西,都会被儿子继承下来。

注意:构造函数,析构函数,拷贝构造函数,拷贝赋值函数不能被子类继承

1.2.继承关系的用意和目的是什么?

老子的特有属性都被儿子继承了下来,所以儿子类中是不是就可以不用再写父类中的属性与方法了。这样就提高了代码的复用性。

同时儿子类中我们还可以添加一些儿子类独有的新特性。这样就提高了代码的拓展性。

所以说继承关系的用意是:代码复用性与高拓展性。就如同这个缤纷复杂的世界,我们可以找到很多的现实的例子。

使用继承关系,需要同学们看待这个世界的事物要上升一个维度,要具有一些抽象的思维。

首先要把这一类的事件进行抽象出来一些共有的属性与特征,把它们定义为父类。然后,再用这个父类,对具有不同特点的子类进行派生。这样就可以派生出各种不同的子类。子类不仅拥有父类的共有的特性,与具备子类独用的特性。这样的代码的复用性与拓展性就会非常灵

2继承关系的分类

2.1单继承的方式:

class + 子类 : 继承方式 + 父类
{
    //单继承的方式
};

2.2继承方式:

继承方式有三种:

public:公有继承

protected:受保护继承

private:私有继承

当继承方式与类中访问权限的结合时,类内属性到子类之中的访问权限的改变如图所示:

2.3简单的例子 

#include <iostream>

using namespace std;

class Car{
private:
    int weight;
public:
    Car()
    {
        cout<<"Car的构造"<<endl;
    }

    ~Car()
    {
        cout<<"Car的析构"<<endl;
    }

    void run()
    {
        cout<<"Car正在行驶过程中"<<endl;
    }

    int setweight(int weight)
    {
        this->weight=weight;
        return this->weight;
    }
};

class Bwm:public Car
{
private:
    string logo;
public:
    Bwm(string logo,int weigth)
    {
        this->logo=logo;
        this->setweight(weigth);
        cout<<"宝马的构造"<<endl;
    }

    ~Bwm()
    {
        cout<<"宝马的析构"<<endl;
    }
};

int main()
{

    return 0;
}

2.4单继承关系的内存布局:

子类在定义对象时,先创建子类的空间,然后构造顺序是:先调用父类的构造对父类中的属性完成初始化,然后再调用子类的构造完成对子类属性的初始化。当子类对象被销毁时,析构的顺序是:首先调用子类的析构,然后再调用父类的析构,最后资源就被回收

2.5当使用继承时,如果父类中没有默认构造,需要在子类的初始化列表指定编译器所应该调用父类构造。

#include <iostream>

using namespace std;

class Car{
private:
    int weight;
public:
    Car(int weigth)
    {
        this->weight=weight;
        cout<<"Car的构造"<<endl;
    }

    ~Car()
    {
        cout<<"Car的析构"<<endl;
    }

    void run()
    {
        cout<<"Car正在行驶过程中"<<endl;
    }

    int setweight(int weight)
    {
        this->weight=weight;
        return weight;
    }
};

class Bwm:public Car
{
private:
    string logo;
public:
    Bwm(string logo,int weigth):Car(1)
    {
        this->logo=logo;
        this->setweight(weigth);
        cout<<"宝马的构造"<<endl;
    }

    ~Bwm()
    {
        cout<<"宝马的析构"<<endl;
    }
};

int main()
{

    return 0;
}

2.6当父类中有与子类中的属性或方法同名时,父类中的同名属性或方法,将被自动隐藏在父类的类域之中。

#include <iostream>

using namespace std;

class A{
public:
    int a=10;
};

class B:public A
{
public:
    int a=20;
};

int main()
{

    B b;

    cout<<b.a<<endl;
    cout<<b.A::a<<endl;

    return 0;
}

结果图:

2.7C++中继承关系下的内存布局与类型兼容规则:

is a关系是一种特殊的has a关系:也和包含关系一样,起始地址是一样的,可以通过父类访问到子类。

 2.7.1证明起始地址是一样的

#include <iostream>

using namespace std;

class A{
public:
    int one=10;

    A()
    {
        cout<<"父类的起始地址"<<this<<endl;
    }
};

class B:public A
{
public:
    int two=20;
    B()
    {
        cout<<"子类的起始地址"<<this<<endl;
    }
};

int main()
{
    B b;


    return 0;
}

结果图:

2.7.3证明是包含关系的代码(可以通过父类的直接访问到子类的数值) 

#include <iostream>

using namespace std;

class A{
public:
    int one=10;
};

class B:public A
{
public:
    int two=20;
};

int main()
{
    A* a=new B;

    cout<<a->one<<endl;
    cout<<(a+1)->one<<endl;
    
    cout<<static_cast<B*>(a)->two<<endl;
    
    return 0;
}

结果图:

所以在单继承情况下,父类指针与子类指针保持一致,父类指针可以天然且安全指向父类对象。

这就是单继承情况下的类型兼容规则,反之则不可以。

 3多继承及棱形继承的相关问题及解决方案

3.1多继承的语法:

class + 子类 : 继承方式 + 父类1,继承方式 + 父类2,继承方式 + 父类3,...
{
    //多继承的方式
};

3.2多继承的实例

3.2.1当我们这样写的时候多继承就会出现二义性,如图:

 3.2.2而且如果像这样我们只想要power和e的时候我们会继承很多我们不需要的东西,造成代码膨胀的问题

using namespace std;

class Phone{
public:
    int power;
    int a;
    int b;
};

class Competer
{
public:
    int c;
    int d;
    int f;
};

class Notebook:public Phone,public Competer
{
    Notebook(int power)
    {
        this->power=power;
    }
};
int main()
{
    
    return 0;
}

3.3解决方案:

一般在使用多继承时,使用多继承多个抽象类,而且实体体。这样就可以避免以上的问题。如果,一定要继承多个实体时,在访问属性或方法时,一定要加上具体的父类的类域,这样也可以避免同名属性或方的二义性的问题,如下。

#include <iostream>

using namespace std;

class Phone{
public:
    int power;
   
};

class Competer
{
public:
   int power; 
};

class Notebook:public Phone,public Competer
{
    Notebook(int power)
    {
        this->Competer::power=power;
    }
};
int main()
{
    
    return 0;
}

4棱形继承及相关问题及解决方案:

4.1棱形继承图:

 4.2菱形的缺点

4.2.1如下代码所示,我们用代码来说明问题,总结在结果图处。

#include <iostream>

using namespace std;

class A{
public:
    int a;
};

class B:public A
{
public:

};

class C:public A
{
public:
};

class D:public B,public C
{
public:

};


int main()
{
    D d;
    cout<<sizeof (d)<<endl;
    return 0;
}

结果图:

 1.我们在代码中只定义了一个代码为int a的内存,内存大小为4,而到了最远的D时,内存大小为了8,这就导致不管父类有多少内存,最远的那个类型接到的内存大小永远是父类的两倍,导致了最远处的类被多次构造。

这两个和多次继承一样。

2.同名属性与方法的二义性的问题。

3.代码膨胀的问题。

4.3解决方法

4.3.1首先我们说一下内部机制

当使用virtual修饰继承权后,继承类中,编译器就会默默安插了一根虚指针,这个虚指针。这两个直接继承类中各有一根虚基表指针,指向一张共有的虚基表。这张虚基表中存在偏移量,通过偏移量就可以找到共有的那个属性。也就是说B 与 C 是共享了一分虚基类。所以A只需要构造一分,B与C就可以虚基表中的偏移找到A中的属性。

4.3.2代码说明

        当我们没有用virtual修饰的时候,内存大小在4.2.1中的代码中有说过,也就是最后D的内存大小为8,当我们加上virtual的时候我们再来看看,代码如下

#include <iostream>

using namespace std;

class A{
public:
    int a;
};

class B:virtual public A
{
public:

};

class C:virtual public A
{
public:
};

class D:public B,public C
{
public:

};


int main()
{

    cout<<sizeof (B)<<endl;
    cout<<sizeof (C)<<endl;
    cout<<sizeof (D)<<endl;
    return 0;
}

结果图:

 由结果图来说,我们可以看到B和C的内存大小为16,他们当中各有一个虚指针,大小为8,还有一个共同使用的int a,内存大小为4,因为涉及结构体补齐的问题,所以大小为16,所以D就是继承了两个虚指针,以及B和C共同使用的int a 所以结构体大小为24。

注意:结构体补齐的问题。

4.3.3总结:

在实际工作中,如果使用继承与使用包含关系都可以解决,首选包含关系。

如果单继承与多继承都可以解决,首选单继承。

如果不可避免要要使用多继承,则要多继承多个接口类。

如果不可避免会发发生棱形继承,则要使用虚继承

猜你喜欢

转载自blog.csdn.net/a2998658795/article/details/126020445