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


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

    1.1概述

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

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

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

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

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

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

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

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

    2继承关系的分类

    2.1单继承的方式:

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

    2.2继承方式:

    继承方式有三种:

    public:公有继承

    protected:受保护继承

    private:私有继承

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

    2.3简单的例子 

    1. #include
    2. using namespace std;
    3. class Car{
    4. private:
    5. int weight;
    6. public:
    7. Car()
    8. {
    9. cout<<"Car的构造"<
    10. }
    11. ~Car()
    12. {
    13. cout<<"Car的析构"<
    14. }
    15. void run()
    16. {
    17. cout<<"Car正在行驶过程中"<
    18. }
    19. int setweight(int weight)
    20. {
    21. this->weight=weight;
    22. return this->weight;
    23. }
    24. };
    25. class Bwm:public Car
    26. {
    27. private:
    28. string logo;
    29. public:
    30. Bwm(string logo,int weigth)
    31. {
    32. this->logo=logo;
    33. this->setweight(weigth);
    34. cout<<"宝马的构造"<
    35. }
    36. ~Bwm()
    37. {
    38. cout<<"宝马的析构"<
    39. }
    40. };
    41. int main()
    42. {
    43. return 0;
    44. }

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

     

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

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

    1. #include
    2. using namespace std;
    3. class Car{
    4. private:
    5. int weight;
    6. public:
    7. Car(int weigth)
    8. {
    9. this->weight=weight;
    10. cout<<"Car的构造"<
    11. }
    12. ~Car()
    13. {
    14. cout<<"Car的析构"<
    15. }
    16. void run()
    17. {
    18. cout<<"Car正在行驶过程中"<
    19. }
    20. int setweight(int weight)
    21. {
    22. this->weight=weight;
    23. return weight;
    24. }
    25. };
    26. class Bwm:public Car
    27. {
    28. private:
    29. string logo;
    30. public:
    31. Bwm(string logo,int weigth):Car(1)
    32. {
    33. this->logo=logo;
    34. this->setweight(weigth);
    35. cout<<"宝马的构造"<
    36. }
    37. ~Bwm()
    38. {
    39. cout<<"宝马的析构"<
    40. }
    41. };
    42. int main()
    43. {
    44. return 0;
    45. }

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

    1. #include
    2. using namespace std;
    3. class A{
    4. public:
    5. int a=10;
    6. };
    7. class B:public A
    8. {
    9. public:
    10. int a=20;
    11. };
    12. int main()
    13. {
    14. B b;
    15. cout<
    16. cout<
    17. return 0;
    18. }

    结果图:

     

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

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

     2.7.1证明起始地址是一样的

    1. #include
    2. using namespace std;
    3. class A{
    4. public:
    5. int one=10;
    6. A()
    7. {
    8. cout<<"父类的起始地址"<<this<
    9. }
    10. };
    11. class B:public A
    12. {
    13. public:
    14. int two=20;
    15. B()
    16. {
    17. cout<<"子类的起始地址"<<this<
    18. }
    19. };
    20. int main()
    21. {
    22. B b;
    23. return 0;
    24. }

    结果图:

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

    1. #include
    2. using namespace std;
    3. class A{
    4. public:
    5. int one=10;
    6. };
    7. class B:public A
    8. {
    9. public:
    10. int two=20;
    11. };
    12. int main()
    13. {
    14. A* a=new B;
    15. cout<one<
    16. cout<<(a+1)->one<
    17. cout<<static_cast(a)->two<
    18. return 0;
    19. }

    结果图:

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

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

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

    3.1多继承的语法:

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

    3.2多继承的实例

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

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

    1. using namespace std;
    2. class Phone{
    3. public:
    4. int power;
    5. int a;
    6. int b;
    7. };
    8. class Competer
    9. {
    10. public:
    11. int c;
    12. int d;
    13. int f;
    14. };
    15. class Notebook:public Phone,public Competer
    16. {
    17. Notebook(int power)
    18. {
    19. this->power=power;
    20. }
    21. };
    22. int main()
    23. {
    24. return 0;
    25. }

    3.3解决方案:

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

    1. #include
    2. using namespace std;
    3. class Phone{
    4. public:
    5. int power;
    6. };
    7. class Competer
    8. {
    9. public:
    10. int power;
    11. };
    12. class Notebook:public Phone,public Competer
    13. {
    14. Notebook(int power)
    15. {
    16. this->Competer::power=power;
    17. }
    18. };
    19. int main()
    20. {
    21. return 0;
    22. }

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

    4.1棱形继承图:

     4.2菱形的缺点

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

    1. #include
    2. using namespace std;
    3. class A{
    4. public:
    5. int a;
    6. };
    7. class B:public A
    8. {
    9. public:
    10. };
    11. class C:public A
    12. {
    13. public:
    14. };
    15. class D:public B,public C
    16. {
    17. public:
    18. };
    19. int main()
    20. {
    21. D d;
    22. cout<<sizeof (d)<
    23. return 0;
    24. }

    结果图:

     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的时候我们再来看看,代码如下

    1. #include
    2. using namespace std;
    3. class A{
    4. public:
    5. int a;
    6. };
    7. class B:virtual public A
    8. {
    9. public:
    10. };
    11. class C:virtual public A
    12. {
    13. public:
    14. };
    15. class D:public B,public C
    16. {
    17. public:
    18. };
    19. int main()
    20. {
    21. cout<<sizeof (B)<
    22. cout<<sizeof (C)<
    23. cout<<sizeof (D)<
    24. return 0;
    25. }

    结果图:

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

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

    4.3.3总结:

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

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

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

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

  • 相关阅读:
    RocketMQ整体架构及NameServer源码分析
    用Python订正数据
    207.课程表
    Java:实现动态数组类算法(附完整源码)
    TDengine3.0 基础操作
    漏洞复现 - - - WebLogic反序列化远程命令执行漏洞(二)
    GDB调试技巧汇总
    [Python进阶] 目录相关库:os、pathlib、shutil
    .Net Core中使用DiagnosticSource进行日志记录
    MySQL进阶实战1,数据类型与三范式
  • 原文地址:https://blog.csdn.net/a2998658795/article/details/126020445