• 面向对象的三大特质之继承


    目录

    一、概念

    二、深层刨析

    1.子类继承了父类哪些东西?

    2.继承的规则

    继承与组合

    3.继承三步骤

    4.数据成员构造析构顺序

    三、使用

    1.如何在子类中对从基类中继承来的数据成员进行初始化?

    2.无参和有参构造函数调用的差别?

    3.赋值运算符的使用

    4.继承下的静态数据成员

    四、隐藏规则


    • 面向对象三大特征:
      • 封装继承多态

    一、概念

    定义:一个类自动拥有了来自另外一个类的属性和方法,我们管这种属性叫做继承。

    层次关系:上层派生下层,下层继承上层

    书写格式

    1. class 子类:继承权限 基类
    2. {};

    如果A是基类,B是派生类,那么B将继承A的数据和函数。

    1. class A
    2. {
    3. public:
    4. void Fun1(void);
    5. void Fun2(void);
    6. };
    7. class B:public A //继承是共有的,如果没有写权限,默认是私有继承
    8. {
    9. public:
    10. void Fun3();
    11. void Fun4();
    12. };
    13. main()
    14. {
    15. B b;
    16. b.Fun1();//B从A继承了函数Fun1
    17. b.Fun2();//B从A继承了函数Fun2
    18. b.Fun3();
    19. b.Fun4();
    20. }

    二、深层刨析

    1.子类继承了父类哪些东西?

    • 除了构造和析构函数之外,无论几代继承,其余的全盘被继承
    • 被继承不代表能被访问访问的两种形式:1.外界访问(主函数..)2.成员函数访问

    1.子类能够继承所有数据成员

    • 下图为例,B继承了A(没有写权限,默认是私有继承),因为A中的数据类型大小为12,而子类B继承了所有的数据成员(m_i,m_j,m_k),因此,下面的代码输出结果为12。

    2.子类能够继承所有成员函数

    • 如下代码所示,A中有成员函数print(),让B继承A,外部成员函数main内可见,B的对象b可以访问A的成员函数print。

    3.公有和保护的可以被访问,私有的可以被继承,不能被访问。

    • 如下代码中,原A类中的数据成员m_i :公有,m_j:保护,m_k:私有,而被B共有继承后,B中的数据成员m_i 仍未:公有,m_j:保护,m_k:私有。原本基类的数据成员类型被继承下来后类型不变。

    4.保护继承和私有继承中:公有和保护可以被继承,但所有的数据成员外界都不能访问。但是自身的成员函数,内部无论是公有、保护还是私有都可以访问

    • 如下代码所示,C保护继承A,把所有public和protected的成员函数和数据成员都私有化了,因为在主函数内无法访问m_i,m_j,m_k。

     总结:

    子类权限(右👉)

    基类的权限(下👇)

    publicprotectedprivate
    publicpublicprotectedprivate
    protectedprotectedprotectedprivate
    private不可访问不可访问不可访问

    2.继承的规则

    1. 如果类A和类B毫不相干,不可以为了使B的功能更多些而让B继承A的功能和属性。不要觉得“不吃白不吃”,让一个好端端的健壮青年无缘无故吃人参补身体。
    2. 若在逻辑上B是A的“一种(a kind of)”,则允许B继承A的功能和属性。例如男人man是人human的一种,男孩boy是男人man的一种。那么类男人man可以从类人human派生,类男孩boy可以从类男人man派生。
    3. 继承的概念在程序世界和现实世界并不相同。例如从生物界角度讲,鸵鸟是鸟的一种,按理说鸵鸟类应该可以从鸟类中派生,但是鸵鸟不能飞,那鸟类中的fly函数就不对。

            所以,更严格的继承规则应该是:若在逻辑上B是A的“一种”,并且A的所有功能和属性对B而言都有意义,则允许B继承A的功能和属性

    继承与组合

            组合:若在逻辑上A是B的“一部分”,则不允许B从A派生,而是要用A和其他东西组合出B。例如眼eye、鼻mose、口mouse、耳ear是头Head的一部分,所以类Head由类眼eye、鼻mose、口mouse、耳ear组合而成,不是派生而成。

    3.继承三步骤

    1. 全盘接收——基类中的除了构造和析构函数之后,其余的数据成员、函数全盘被继承。
    2. 改写
    3. 添加子类特有的
      1. class A
      2. {
      3. public:
      4. private:
      5. int m_i;
      6. };
      7. class B:public A
      8. {
      9. public:
      10. B(){cout<<"B"<
      11. private:
      12. int m_j;
      13. };
    • 1.全盘接收
    • 2.改写(public->不用改写)
    • 3.B的数据成员(放在A的数据成员下面)

             要给m_i开辟空间:需要调用基类的构造函数对从基类中继承的数据成员m_i开辟空间。

    那这样的话为什么不把构造函数继承下来呢?因为子类当前没有对基类数据成员的构造权

    4.数据成员构造析构顺序

    • 构造的顺序: 
      • 1.先按照继承顺序调用基类的构造

        2按照继承顺序调用组合的构造

        3.调用自己的构造

      • 基类->子类

      • 构造函数的调用顺序是某类在类继承表中出现的顺序,而不是它们在成员初始化表中的顺序。如class D : public A, public B, public C  ,即为DABC的顺序

      • 如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。

    • 析构函数的调用顺序:子类->基类

    三、使用

    1.如何在子类中对从基类中继承来的数据成员进行初始化?

    子类的数据成员是从父类中继承来的,子类没有对基类数据成员的构造权,因此,需要在基类中进行初始化。

    以下面student和person类的代码为例。在主函数内构造了Student类的对象s1并对其进行初始化,Student类是Person类的子类,继承了m_num,m_name,m_sex三个数据成员以及Show()的成员函数。想要在主函数内输出对象s1的初始化信息,需要调用构造函数对对象s1进行初始化,而默认构造函数是无参的,因此必须显示的调基类构造函数进行传参。利用冒号语法(初始化列表方式)进行初始化调用。

    1. class Person
    2. {
    3. public:
    4. Person(int num, const char* name, char sex)
    5. :m_num(num),m_sex(sex)
    6. {
    7. m_name = new char[strlen(name) + 1];
    8. strcpy_s(m_name, strlen(name) + 1, name);
    9. }
    10. void Show()
    11. {
    12. cout << m_num << " " << m_name << " " << m_sex << " ";
    13. }
    14. private:
    15. int m_num;
    16. char* m_name;
    17. char m_sex;
    18. };
    19. class Student :public Person
    20. {
    21. public://冒号语法进行显示调用传参
    22. Student(int num, const char* name, char sex, float score)
    23. :Person(num,name,sex),m_score(score)
    24. {
    25. }
    26. void print()
    27. {
    28. Show();
    29. cout << m_score << endl;
    30. }
    31. private:
    32. float m_score;//学生特征
    33. };
    34. void main()
    35. {
    36. Student s1(1001, "wangpangpang", 'f', 89);
    37. s1.print();
    38. }

    2.无参和有参构造函数调用的差别?

    1.定义无参对象时,基类的构造函数必须也是默认无参的

    • 如下代码所示:当定义无参对象s时,要调用构造函数对对象初始化开辟空间,先调用基类构造函数,然后调用子类默认的无参构造函数。但是基类中声明的三个数据成员继承给子类,基类构造函数是没有对其传参的,s就会报错。
    • 修改方法就是把基类person的构造函数修改为无参的。无参时要调用无参的构造函数;有参时对象就直接调用用冒泡语法进行初始化的有参构造函数。

     2.无参对象调用无参构造函数时,有指针作为数据成员,需要给指针开辟空间

    • 如下代码所示,对指针m_name赋初值为占位符‘\0’,就可以避免指针对非合法空间的开辟。
      • 开辟空间new后要析构delete

    •  没有对指针char* name开辟合法内存单元,就像如下C代码所示的错误,对非法空间进行了访问。

    3.赋值运算符的使用

    例:

    • 如下代码中s=s2;旧对象给新对象赋值,调用赋值运算符。
    • 需要在子类和基类中补充对赋值运算符的操作:
      • 基类将自己的数据成员m_name,m_sex,m_age赋值操作:
      • 子类调用基类赋值运算符,对自身m_num,m_score进行赋值运算操作:

    4.继承下的静态数据成员

    基类和子类都对static的数据成员进行共享

    • 如下代码中,定义person类,派生出子类teacher、worker。静态全局变量m_count(静态全局变量在类内声明,类外初始化)。此时,对count输出结果为2 。
    •  
    •  

    四、隐藏规则

    规则内容:

    1. 如果派生类的函数与基类的函数同名,但参数不同。此时,无论有无virtual关键字,基类的函数都将被隐藏(注意别和重载混淆)。
    2. 如果派生类的函数和基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别和覆盖混淆,覆盖是对其进行改写)
    • 同名不同参,被隐藏
    • 同名同参,基类无virtual函数

            如下代码中,A的成员函数print(),B是A的子类,成员函数名也为print(),此时B虽然继承了A::print(),也有自身的B::print(),但是如果此时主函数内对b对象输出b.print()输出的结果为"B::print",因为同名,B内对A::print隐藏了

            对sizeof(B)进行输出的结果为8,因为虽然A的成员函数m_i与B的成员函数m_i同名,但是两者实质并不同,就像B自己有printf但还是继承了A::print,只是因为同名,对其隐藏起来了,但是还是继承下来,可以通过b.A::print()进行访问。因此对于成员函数,B内有A的m_i和B的m_i。输出结果就是4+4=8。不加前缀的话,用的是B自己的m_i。A::m_i->用A的m_i。

    • 总结:
      • 满足隐藏规则时,对基类函数进行隐藏,调用访问时访问的是子类的。
      • 子类仍然对基类函数进行基类,只是隐藏起来。

  • 相关阅读:
    爱上开源之golang入门至实战第三章-性能分析PPROF
    Redis之五大基础数据结构及简单操作
    AndroidStudio - Logcat显示乱码,都是编码格式惹得祸
    一篇文章带你弄懂Kerberos的设计思路
    [NLP] LLM---<训练中文LLama2(一)>训练一个中文LLama2的步骤
    bm20 2
    python采集小破站视频弹幕
    【shell】初识shell脚本语言
    解决appium 提示:UiAutomationService android.accessibilityservice....registered!
    Python-算法编程100例-滑动窗口(入门级)
  • 原文地址:https://blog.csdn.net/qq_53830608/article/details/127775854