• 【C++进阶(九)】C++多态深度剖析


    💓博主CSDN主页:杭电码农-NEO💓

    ⏩专栏分类:C++从入门到精通

    🚚代码仓库:NEO的学习日记🚚

    🌹关注我🫵带你学习C++
      🔝🔝


    在这里插入图片描述


    1. 前言

    继承和多态这两兄弟常常一起出现
    继承是实现多态的前提!

    本章重点:

    本篇文章着重讲解多态的概念以及
    定义,多态的底层原理和析构函数重写
    以及函数重写的两个例外条件
    多继承中的虚函数表关系.其中,简单介绍
    的部分有抽象类的概念以及定义和
    继承与多态中的两个新增关键字

    注:如果你不知道什么是继承,或继承
    的知识掌握不牢固,请先阅读下面文章:

    C++继承深度剖析


    2. 多态的概念以及定义

    概念: 通俗来说,多态就是多种状态
    父子对象完成相同任务会产生不同的结果

    比如:
    学生和普通人都去买门票
    学生是半价,而普通人是全价

    在继承中构成多态要有两个条件:

    1. 必须通过基类的指针或引用调用虚函数
    2. 被调用的函数必须是虚函数
      并且子类的虚函数要被重写

    现在的你可能有一万个问号
    什么是虚函数?什么是重写?
    没关系,我们一步一步讲!

    关键字virtual加在成员函数前
    这个成员函数就是虚函数!

    在这里插入图片描述
    虚函数的重写(也叫覆盖):

    派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的,返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数

    class Person {
    public:
     	virtual void BuyTicket() { cout << "买票-全价" << endl; }
    };
    class Student : public Person {
    public:
     	virtual void BuyTicket() { cout << "买票-半价" << endl; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面的代码中,BuyTicket函数就被重写了!

    概念讲完,下一步进行实战!


    3. 多态的实例调用情况

    构成多态的条件就两个,一定要熟记!
    一定要熟记!一定要熟记!重要的事情说三遍

    下面是多态的实例:

    class Person {
    public:
     	virtual void BuyTicket() { cout << "买票-全价" << endl; }
    };
    class Student : public Person {
    public:
     	virtual void BuyTicket() { cout << "买票-半价" << endl; }
    };
    
    int main()
    {
    	Person* p1 = new Person;
    	Person* p2 = new Student;
    	p1->BuyTicket();
    	p2->BuyTicket();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们知道一个事实:
    基类的指针或引用可以指向/引用
    子类的对象,我们称为切片

    p1和p2是基类指针,它们调用的
    函数恰好还被重写了,所以这里符合
    多态,p1指针指向的内容是Person
    所以它调用Person中的函数,然而p2
    指针指向的内容是Student,所以它
    调用的是Student中的函数!

    依次打印:"买票-全家","买票-半价"


    4. 构成多态的两个特例

    1. 特例一: 子类的虚函数不写virtual
      依旧构成多态
    class Person {
    public:
     	virtual void BuyTicket() { cout << "买票-全价" << endl; }
    };
    class Student : public Person {
    public:
     	void BuyTicket() { cout << "买票-半价" << endl; }
    };
    Person* p1 = new Person;
    Person* p2 = new Student;
    p1->BuyTicket();
    p2->BuyTicket();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这样写也是构成多态的!

    1. 特例二:基类与派生类虚函数返回值类型不同
      也可以构成多态(返回值必须满足某种条件)
    class A{};
    class B : public A {};
    
    class Person {
    public:
     	virtual A* f() {return new A;}
    };
    class Student : public Person {
    public:
     	virtual B* f() {return new B;}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    父类的返回值要返回父类
    子类的返回值要返回子类

    1. 注意事项1:父类不写virtual,而子类的同名
      函数写了virtual,这是不构成多态的!
    class Person {
    public:
     	void BuyTicket() { cout << "买票-全价" << endl; }
    };
    class Student : public Person {
    public:
     	virtual void BuyTicket() { cout << "买票-半价" << endl; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    不构成多态!

    1. 注意事项2:在继承体系中,父子类的同名
      函数不构成重写就构成隐藏,不可能构成重载!

    5. 多态的底层原理分析(一)

    在这里插入图片描述

    如果你单纯的认为Base类只有一个
    整型变量占用空间的话,那你就上当啦!
    事实上在32位机器下,这里的结果是8
    在64位机器下,这里的结果是16!

    这是因为它除了有一个变量外,还有
    一个指针,此指针指向一个虚函数表

    我们通过以下的代码来观察内存:

    class A
    {
    public:
    	virtual void func1()
    	{
    		cout << "父类func1";
    	}
    private:
    	int _a;
    };
    class B : public A
    {
    public:
    	virtual void func1()
    	{
    		cout << "子类func1";
    	}
    private:
    	int _b;
    };
    
    int main()
    {
    	A a;
    	B b;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在这里插入图片描述

    此指针叫虚表指针:vfptr,也就是
    virtual function ptr

    这个指针并不是直接指向虚函数的地址
    而是指向一个虚函数表,可以理解位一个
    数组,此数组中存放着此对象中所有的虚
    函数的地址,它们的关系可以用下图表示:

    在这里插入图片描述

    注:不管有没有继承体系或多态
    只要有虚函数就有虚表!


    6. 多态底层原理分析(二)

    现在得出一个结论:有虚函数的
    类对象中还存放了一个虚表指针!

    那么父类和子类的虚表指针和指向
    的内容有什么不同或相同处吗?
    形成多态现象的原理又是什么?
    我来一一解答这些问题:

    1. 通过下面的代码来观察内存情况
      得出父子类虚表的关联:
    class A
    {
    public:
    	virtual void func1()
    		cout << "父类func1";
    	virtual void func2()
    		cout << "父类func2";
    private:
    	int _a;
    };
    class B : public A
    {
    public:
    	virtual void func1()
    		cout << "子类func1";
    private:
    	int _b;
    };
    int main()
    {
    	A a;
    	B b;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    请看下图观察情况:

    在这里插入图片描述

    结论:

    父类和子类的虚表指针是不同的
    证明父子类各有一张虚函数表!
    函数func1在子类中被重写了,所以
    父子类虚表中的func1函数地址是不同的
    函数func2没有被子类重写,所以
    父子类虚表中的func2函数地址是相同的

    拓展结论:同一个类的不同对象共用一个虚表

    1. 多态的原理深度剖析:

    当一个函数A被重写时,它的父类虚表存放
    父类函数A的地址,子类虚表存放的是子类
    函数A的地址!

    当父类的指针或引用指向子类空间时
    调用虚函数时,会到指向对象的虚表中
    中找到对应的虚函数地址,进行调用!

    拓展结论: 父子类都只有A函数或无函数时

    1. 若父类写了虚函数A,而子类
      甚至没有写函数A,此时子类对象中
      存储的虚函数地址与父类相同

    2. 若父类甚至没有写函数A,而子类
      直接写了虚函数A,则父类对象中没有
      虚表,而子类对象中有虚表(存放A)


    7. 多态中的两个关键字

    1. final:修饰虚函数,表示该虚函数不能被重写

    在这里插入图片描述

    1. override:检查子类类虚函数是否重写了
      基类虚函数如果没有重写编译报错

    在这里插入图片描述


    8. 抽象类以及虚函数的几个结论

    抽象类概念:

    在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写

    抽象类的只需了解概念,实际中
    使用到的场景很少

    关于虚函数的几个小结论:

    1. 析构函数最好定义为虚函数
    2. 构造函数不能定义为虚函数
    3. 静态成员函数不能是虚函数
    4. 内联函数(inline)不能是虚函数

    为什么说析构函数最好定义为虚函数?
    请看下面的例子:

    class Person {
    public:
     	virtual ~Person() {cout << "~Person()" << endl;}
    };
    class Student : public Person {
    public:
     	virtual ~Student() { cout << "~Student()" << endl; }
    };
    // 只有派生类Student的析构函数重写了Person的析构函数
    //下面的delete对象调用析构函数,才能构成多态
    //才能保证p1和p2指向的对象正确的调用析构函数。
    int main()
    {
     	Person* p1 = new Person;
     	Person* p2 = new Student;
     	delete p1;
     	delete p2;
     	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    若析构函数不是虚函数,delete ptr2时
    不符合多态,ptr2是Person类型指针
    就只会调用Person类的析构,会有问题

    若析构函数是虚函数,delete ptr2时
    构成多态的条件,指针指向父类的对象
    就调用父类的析构,指向子类的对象
    就调用子类的析构,这样才是正确的!


    9. 总结以及拓展

    多态在校招的笔试面试中考察的
    非常之多,很多面试官都喜欢在这
    上面考察学生的掌握C++语法的程度
    所以同学们请耐心学习!

    拓展阅读:

    多继承场景下的多态


    🔎 下期预告:二叉搜索树 🔍
  • 相关阅读:
    网安学习-应急响应3
    javaee springMVC的简单使用 jsp页面在webapp和web-inf目录下的区别
    【中间件篇-Redis缓存数据库08】Redis设计、实现、redisobject对象设计、多线程、缓存淘汰算法
    Xilinx SDK编译完成自动生成SREC文件(适用于ISE、Vivado、Vitis)
    【面试题】封装jQuery源码以及实现jQuery的扩展功能
    Power Automate-创建审批流
    windows系统git使用ssh方式和gitee/github进行同步
    Linux
    wpf Grid布局详解 `Auto` 和 `*` 是两种常见的设置方式 行或列占多个单元格,有点像excel里的合并单元格。使其余的列平均分配剩余的空间
    升级一下电脑,CPU换I5-14600K,主板换华硕B760M
  • 原文地址:https://blog.csdn.net/m0_61982936/article/details/133935580