• c++入门学习⑦——继承和多态(超级详细版)


    目录

    前言

    继承

    继承是什么?

    为什么会存在继承?

    语法:

    一些基本的定义:

    三种继承方式:

    对象模型

    对于构造和析构的顺序

    同名函数的处理方式

    总结:

    静态成员:

    定义:

    性质:

    共享数据 

    编译阶段分配内存

    类内声明类外初始化

    静态成员函数

    静态成员函数与普通成员函数的区别:

    静态成员访问:

    多继承

    菱形继承

    菱形继承会遇到的问题:

    如何解决?

    原理:

    多态

    多态分类:

    静态多态和动态多态区分:

    静态多态:(静态绑定)

    动态多态:(动态绑定)

    动态多态实现:

    引入:

    那什么是虚函数?

    虚函数的重写?

    多态实现代码示例:

    虚函数的动态绑定机制(面试经常问的)

     重载、重定义、重写的区别

    纯虚函数

    抽象类:

    那什么是接口继承呢?

    那正常的继承叫做什么?

    多态原理(工具观察):

    虚析构

    语法:

    原理:通过父类指针来释放子类空间

    纯虚析构

    特点:

    语法:

    纯虚析构和虚析构的共性与区别:

    虚析构和纯虚析构共性:

    虚析构和纯虚析构区别:

    总结:

    总结:


    前言

    这篇博客介绍相关c++继承以及多态的内容,在c++中属于很重要的一个部分,请认真学习(ง •_•)ง

    继承

    继承是什么?

    简单从字面意思上来说就是继承上一个事物的基本特点

    准确定义是:继承是面向对象三大特征之一,可以使得子类具有父类的属性和方法 , 还可以在子类中重新定义 ,以及追加属性和方法。

    为什么会存在继承?

    假如要构造两个类——斑点猫和白猫,其中包含的行为有类对象的行为习惯等,而这两个同属于猫类,他们有很多相似的地方,如果没有继承,则需要重复性写这一相似的内容,一个两个还好,如果多个类都有相似之处,继承就是比较好的方式了。

    语法:

    class 子类名:继承方式(public/private/protected)  父类名

    一些基本的定义:

    基类:被继承的类,又称为“父类”

    派生类:继承其他类的类,又称为“子类”

    三种继承方式:

    公共继承public:继承父类的公共权限,则相应的私有权限和保护权限也继承过去,也是相应的私有和保护权限

    保护继承protected:继承父类的保护权限,则父类的公共权限到子类中变为保护权限,而私有权限仍然是私有权限

    私有继承private:继承父类的私有权限,父类中所有权限到子类中都是私有权限。

    对象模型

    这里创造两个类,一个基类,一个派生类,B类继承A类的公共权限

    class A

    class B

    1. #include
    2. using namespace std
    3. class A
    4. {
    5. public:
    6. int a;
    7. };
    8. class B:public A
    9. {
    10. public:
    11. int b;
    12. };

    则它们在内存中的分布是:

    也就是说,派生类会继承一份基类的成员,然后在旁边创建自己的成员

    对于构造和析构的顺序

    当创建一个子类对象后,如果要初始化它,则哪一个类先构造,哪一个类后构造,程序结束时,谁先析构?

    谁的构造函数先调用,谁的析构函数先调用

    代码示例:

    1. #include
    2. using namespace std;
    3. class A
    4. {
    5. public:
    6. A()
    7. {
    8. cout<<"A的构造函数"<
    9. }
    10. ~A()
    11. {
    12. cout<<"A的析构函数"<
    13. }
    14. int m_a;
    15. };
    16. class B:public A
    17. {
    18. public:
    19. B()
    20. {
    21. cout<<"B的构造函数"<
    22. }
    23. ~B()
    24. {
    25. cout<<"B的析构函数"<
    26. }
    27. int m_b;
    28. };
    29. test1(){
    30. B b;
    31. }
    32. int main()
    33. {
    34. test1();
    35. system("pause");
    36. return 0;
    37. }

     输出结果为:

    由此可见刚才问题的答案是:父类先构造然后子类构造,子类析构再父类析构

    这个可以用鸡生蛋来记——鸡比它生的蛋要早出生,先有这只鸡才有这个蛋,然后吃的时候,先吃蛋,再吃鸡

    同名函数的处理方式

    对于基类和派生类的同名函数如何处理呢?换句话说当调用这个同名函数时,真正会起作用的是哪一个函数呢?

    我们来通过代码看一看:

    1. #include
    2. using namespace std;
    3. class A
    4. {
    5. public:
    6. void speak()
    7. {
    8. cout<<"A会说话了"<
    9. }
    10. int m_a;
    11. };
    12. class B:public A
    13. {
    14. public:
    15. void speak()
    16. {
    17. cout<<"B会说话了"<
    18. }
    19. int m_b;
    20. };
    21. test1(){
    22. B b;
    23. b.speak();
    24. }
    25. int main()
    26. {
    27. test1();
    28. system("pause");
    29. return 0;
    30. }

    通过代码发现派生类调用同名函数,则调用派生类的同名函数

    得出结论:

    如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数

    那么如何通过派生类调用基类的同名函数呢?

    通过添加作用域      类名::

    总结:

    • 当子类与父类拥有同名的成员函数,子类会隐意父类中同名成员函教,加作用域可以访问到父类中同名函数
    • 子类对象可以直接访问到子类中同名成员
    • 子类对象加作用域可以访问到父类同名成员

    作用域是👉类名::

    静态成员:

    定义:

    简单来说:普通的成员前加上一个static关键字,就被称为静态成员。

    当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。

    性质:

    共享数据 

    静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。

    编译阶段分配内存

    在编译阶段就已经分配好内存了

    类内声明类外初始化

    静态成员不可以在类内初始化,要在类外初始化,初始化时在成员名加上作用域即可

    静态成员函数

    在成员函数前加上关键字static,这样就把类的特定对象和该函数独立开

    静态函数只要使用类名加范围解析运算符 :: 就可以访问

    静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数

    静态成员函数与普通成员函数的区别:
    • 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)
    • 普通成员函数有 this 指针,可以访问类中的任意成员

    静态成员访问:

    1. 通过类名访问:类名::父类作用域::成员
    2. 通过对象访问
    3. 同名静态函数或者是变量,父类的所有同名函数包括函数重载会被隐藏,除非加上作用域才可以成功

    多继承

    子类是否只可以继承一个类,可以进行多个类吗?如果可以继承多个类,多继承的语法是什么?

    c++允许子类可以继承多个类

    语法为:class 子类  :继承权限 父类1 ,继承权限 父类2,继承权限 父类3……

    但是注意:在实际开发过程中,不建议使用多继承语法

    菱形继承

    什么是菱形继承?

    如下图:

    这样一种几个类相互有一定的继承关系的看起来像菱形一样的,被称为菱形继承

    定义:两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石继承

    菱形继承会遇到的问题:

    由于类4同时继承类2和类3,而类2和类3都继承了1,那么相当于类4继承了两份类1.

    如何解决?

    使用虚继承的方式,用关键字virtual,可以使派生类不重复继承

    在派生类继承的时候在继承权限前加上这个关键字virtual即可

    示例:

    原理:

    虚继承会产生虚基类指针vbptr

    该指针指向虚基表,虚基表记录的是通过指针访问公共祖先的数据的偏移量

    多态

    多态是c++面向对象三大特征之一

    多态按字面的意思就是多种形态,具体解释是指:不同对象去完成某一个行为是而产生的不同状态

    当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态

    多态性提供接口与具体实现之间的隔离,将what和how这两个板块分离开

    多态好处:

    1. 组织结构清晰
    2. 可读性强
    3. 对于前期和后期扩展以及维护性高

    多态分类:

    分为静态多态动态多态(其实静态多态之前就已经涉及了)

    静态多态和动态多态区分:

    静态多态的函数地址早绑定-编译阶段确定函数地址
    动态多态的函数地址晚绑定-运行阶段确定函数地址

    静态多态:(静态绑定)

    静态多态:函数重载,重定义,运算符重载属于静态多态

    静态多态的函数地址早绑定-编译阶段确定函数地址

    动态多态:(动态绑定)

    动态多态:派生类和虚函数实现运行时的多态

    动态多态的函数地址晚绑定-运行阶段确定函数地址

    动态多态满足条件:

    1.有继承关系

    2.子类重写父类虚函数,函数返回值类型,名称,参数列表完全相同

    3.使用->父类指针或者引用执行于类对象

    下面仅介绍重点——动态多态

    动态多态实现:

    如何实现动态多态呢?有什么作用?使用场景又是什么?

    多态是当不同继承关系的类对象去调用一个函数而产生的不同的行为

    实现动态多态的条件

    • 类对象要调用虚函数,且派生类中必须要包含基类虚函数的重写
    • 通过基类的引用/指针去调用虚函数⭐ 

    引入:

    继承会使子类都含有父类的数据,而每一个子类都对这份数据进行重写,而如果想创建一个函数,使其可以操纵父类所有派生的子类?(即使以后还有派生的子类,也可以操纵)

    该怎么做呢?

    其实最重要的是函数的参数

    参数设定其实需要找到这几个派生类的共性——》它们都有父类的数据

    因此如果要实现这样的一个函数,那么就需要其参数为父类的指针或是引用,而要操纵所有子类,这就需要用父类指针来保存子类的空间地址⭐⭐⭐

    而用父类指针保存子类地址会造成问题:

    代码示例:

    1. #include
    2. using namespace std;
    3. class A
    4. {
    5. public:
    6. void say()
    7. {
    8. cout << "A会说话了" << endl;
    9. }
    10. };
    11. class B : public A
    12. {
    13. public:
    14. void say()
    15. {
    16. cout << "A类里的B会说话了" << endl;
    17. }
    18. };
    19. class C :public A
    20. {
    21. public:
    22. void say()
    23. {
    24. cout << "A类里的C会说话了" << endl;
    25. }
    26. };
    27. void test1(A*a)
    28. {
    29. a->say();//输出的都是 A会说话了
    30. }
    31. int main()
    32. {
    33. A* a=new B;
    34. test1(a);
    35. A* b = new C;
    36. test1(b);
    37. return 0;
    38. }

    我们发现本身要通过父类的指针来完成子类的重定义函数,但是实际上调用的都是父类的函数,为什么呢?

    因为指针指向的地址是由指针指向类型决定的,A*a,这个式子就决定了它要指向父类,而子类中继承了父类的数据,因此a指向子类中的父类,实现的也是父类的函数

    如何解决这一个问题呢? 

    用虚函数

    那什么是虚函数?

    被关键字virtual修饰的类成员函数,而且子类中要重写虚函数(可加virtual也可不加)

    虚函数的重写?

    虚函数的重写(又可以叫做覆盖)👉

    派生类中有一个跟基类完全相同的虚函数(这里指它们的返回值类型、函数名字、参数列表完全相同),则称子类的虚函数重写了基类的虚函数,而其中的重写内容可以做适当改变,来实现多态

    多态实现代码示例:

    1. #include
    2. using namespace std;
    3. class A
    4. {
    5. public:
    6. virtual void say()
    7. {
    8. cout << "A会说话了" << endl;
    9. }
    10. };
    11. class B : public A
    12. {
    13. public:
    14. void say()
    15. {
    16. cout << "A类里的B会说话了" << endl;
    17. }
    18. };
    19. void test1(A*a)
    20. {
    21. a->say();//输出的是A类里的B会说话了
    22. }
    23. int main()
    24. {
    25. A* a=new B;
    26. test1(a);
    27. return 0;
    28. }

    这样父类指针就可以调用子类的函数了,解决了父类指针指向子类地址的一个问题 

    那为什么变成虚函数后,父类指针可以调用子类中的子类函数而非父类呢?

    虚函数的动态绑定机制(面试经常问的)

    当一个类中的函数变为虚函数之后,会产生虚函数指针(vfptr),虚函数指向虚函数表(vftable),而如果这个类没有被继承的话——》虚函数表保存的是这个虚函数的入口地址

    如果被继承了,子类会把父类中的虚函数指针给继承过来,但是这时候这个虚函数表里的内容就发生变化了,它里面的是子类重写的地址

    如图本质上A*a指针还是指向父类地址,但是由于去调用时发现它是一个虚函数指针,而这个指针指向的虚函数表是重写的say函数地址,因此调用的是子类函数。

     重载、重定义、重写的区别

    重载:同一作用城,同名函教,参数的顺序,个数,类型不同都可以重载。函数的返回值类型不能作为重载条件(函数重载,运算行重载)
    重定义:有继承,子类重定义父亲的同名函数(非虚函数),参数顺序,个数,类型可以不同,子类的同名函数会屏蔽父类的所有同名函数(可以通过作用域解决)
    重与(覆盖):有继承,子类重写父类的虚函数。返回值类型,函数名,参数顺序,个数,类型都必须一致。 

    而当这样写后,代码只进行子类函数调用,此时的父类虚函数就没有什么用了,那么我们可以不可以找到一种方式去省略掉父类虚函数的内容——可以,用纯虚函数,下面就来介绍什么是纯虚函数?

    纯虚函数

    在多态中,通常父类中虚函数的实现是毫无意义的主要都是调用子类重写的内容
    因此可以将虚函数改为纯虚函数

    纯虚函数是指在虚函数上加上=0则称为虚函数

    例如:

    virtual void say ()=0;

    而当一个类中由纯虚函数后这个类就成了——抽象类👇 

    抽象类:

    抽象类是指类中含有纯虚函数的类,抽象类又称为接口类,抽象类不能实例化对象(因为它没有函数体,怎么调用?)

    抽象类特点:

    无法实例化对象

    子类必须重写抽象类中的所有纯虚函数,否则也属于抽象类⭐

    其实抽象类主要目的是设计类的接口

    也就是说只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,其实这里的纯虚函数存在接口继承

    那什么是接口继承呢?

    简单来说虚函数的继承——接口继承,派生类并没有继承基类函数,而是基类虚函数的接口,这是一个实现多态的方式,可以达到重写的目的,如果不要求多态实现,函数尽量不要定义为虚函数

    那正常的继承叫做什么?

    普通函数的继承为——实现继承,派生类它继承了基类函数,继承的是函数的实现,而非接口,派生类可以使用这个接口

    多态原理(工具观察):

    前面已经将结果多态如何通过虚函数实现的了,现在来使用工具观察多态实现的虚函数表

    ⭐满足多态以后的函数调用,并不是在编译时确定的,而是运行起来以后到对象中去找的

    不满足多态的函数调用是在编译时确认好的。

    这里借助vs下的一个工具

    输入dir空格后出现目录,然后直接输入以下内容,即可看代码中的类

    • cl(空格)/d1(空格)reportSingleClassLayout(类名)(空格)(文件名)(回车)     

    例如在test.cpp文件中,A类的结构:

    • cl /d1 reportSingleClassLayoutA test.cpp

    最后就可以看见这个类的内部情况。

    ⭐回顾:我们要达到多态,有两个条件,一个是虚函数覆盖,一个是基类对象的指针或引用调用虚函数

    哎?有人发现没,其实刚才多态实现里的代码有个问题——new出来的空间一直没有释放,堆区空间没释放会发生内存泄漏,其实这里就涉及到另一个知识了——虚析构

    这时可能会问,为什么非要用虚析构呢?正常的析构为什么不可以释放掉代码呢?这时假如你写一个代码,就会发现——父类的析构函数正常调用,而子类的析构函数无法调用,也就是我们没办法在子类析构函数中去写代码,释放掉子类的堆区数据。

    做法就是在父类的析构前加上一个很熟悉的关键字——virtual

    这也就是下面要介绍的虚析构

    虚析构

    多态使用时,若子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,父类在析构的时候不会调用子类析构,如果子类有堆区属性,则会出现内存泄漏
    则需要将父类中的析构函数改为虚析构

    语法:

    虚析构语法:
    virtua1 ~类名()

    原理:通过父类指针来释放子类空间

    由于刚开始实现多态的时候,子类的空间是由父类指针储存的,因此只能通过父类指针来释放子类空间。

    已知的是

    构造是:父类——》成员——》子类

    析构是:子类——》成员——父类

    析构函数本身也是个成员函数,虚析构,产生虚函数指针,指向虚函数表:包含这个类的析构函数 

    纯虚析构

    特点:

    纯虚析构的本质:是析构函数,完成各个类的回收工作。
    必须为纯虚析构函数提供一个函数体
    而且纯虚析构函数必须在类外实现

    含有纯虚析构的类也是抽象类

    语法:

    virtual ~类名()=0;
    在类外:类名::~类名() 

    纯虚析构和虚析构的共性与区别:

    虚析构和纯虚析构共性:

    可以解决父类指针释放子类对象
    都需要有具体的函数实现


    虚析构和纯虚析构区别:

    如果是纯虚析构,该类属于抽象类,且无法实例化对象,需要在类外实现

    总结:

    • 1.虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
    • 2.如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
    • 3.拥有纯虚析构函数的类也属于抽象类

    总结:

    c++的入门核心内容基本介绍完毕,整理的有关c++的文件操作以及内存分区放在下面了,可以按需观看

    c++文件操作-CSDN博客

    c++内存的四大分区详解-CSDN博客

    下一阶段是对于c++的模板,欢迎点赞收藏关注主页专栏o(* ̄▽ ̄*)ブ

  • 相关阅读:
    Java基础-字符串
    4.2、Linux进程(1)
    通过Git GUI上传本地代码至Github
    梦开始的地方—— C语言动态内存管理(malloc+calloc+realloc+free)
    A-Level补考注意事项
    深入解析Kafka消息传递的可靠性保证机制
    Ajax简介
    LiDAR点云转换到大地坐标系——简单粗标定
    安装NodeJS并使用yarn下载前端依赖
    ARM Cortex-M 系列 MCU 芯片选型
  • 原文地址:https://blog.csdn.net/2301_79328557/article/details/136167870