当前位置: 首页 > java >正文

【C++进阶十】多态深度剖析

【C++进阶十】多态深度剖析

  • 1.多态的概念及条件
  • 2.虚函数的重写
  • 3.重写、重定义、重载区别
  • 4.C++11新增的override 和final
  • 5.抽象类
  • 6.虚表指针和虚表
    • 6.1什么是虚表指针
    • 6.2指向谁调用谁,传父类调用父类,传子类调用子类
  • 7.多态的原理
  • 8.单继承的虚表状态
  • 9.多继承的虚表状态
  • 10.菱形继承
  • 11.菱形虚继承

1.多态的概念及条件

继承是实现多态的前提
通俗来说,多态就是多种状态,父子对象完成相同任务会产生不同的结果
例如:成年人买火车票是全价票,学生买票是折扣票

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

  1. 必须通过父类的指针引用去调用
  2. 被调用的函数必须是虚函数,且完成虚函数的重写

在这里插入图片描述

2.虚函数的重写

什么是虚函数:

被virtual修饰的类成员函数称为虚函数

什么是虚函数的重写(覆盖):

三同:父子虚函数的函数名返回值类型参数相同
(参数的缺省值可以不同)

传递不同的对象调用不同的函数
传父类调用的是父类的虚函数
传子类调用的是子类的虚函数

虚函数重写的例外:

  1. 子类的虚函数可以不加virtual
    重写体现了接口继承:子类把函数的声明继承下来,重写的是函数的实现,所以不写virtual也可以满足多态的条件
    在这里插入图片描述

  2. 协变:子类与父类的虚函数返回值类型不同,但必须满足父类虚函数返回父类对象的指针或引用,子类虚函数返回子类对象的指针或引用
    在这里插入图片描述
    这个父子类指针也可以是自己的父子类,也可以是指其他类型的父子类:
    在这里插入图片描述

子类的虚函数可以不加virtual可以防止析构函数出错:
正确使用析构函数:在这里插入图片描述
父类和子类的虚函数的析构函数函数名并不相同却依然构成虚函数的重写,因为析构函数在多态中会被编译器修改成同一个名字
为什么会变成同一个名字:析构函数会因为父子类关系,在子类调用析构后会自动调用父类的析构,如果父子的析构函数不是虚函数,调用析构时就不会调用子类的析构,即使我们指向了子类
在这里插入图片描述
为什么没有调用到子类:因为delete的内部构成是:析构函数和operator delete(),而operator delete 有一个特点就是调用delete的指针是什么类型的,就会调用什么类型的析构函数,上图的两个指针p1和p2都是父类person类型,所以就都调用了父类person的析构函数,没有调动子类的析构函数
实际使用结果如下:
在这里插入图片描述
所以想要指向父类调用父类析构,指向子类调用子类析构,就需要编译器把析构函数的名字进行了统一,满足了虚函数重写的三同:父子虚函数的函数名返回值类型参数相同
子类可以不写virtual,只要父类加上了virtual就可以进行虚函数的重写,但是不太建议
在这里插入图片描述

3.重写、重定义、重载区别

在这里插入图片描述

4.C++11新增的override 和final

overrride:检查子类虚函数是否重写了父类的某个虚函数,如果没有重写编译报错
在这里插入图片描述

final:修饰虚函数,表示该虚函数不能被重写
在这里插入图片描述

5.抽象类

在虚函数后面写上=0,这个虚函数被称为纯虚函数,包含纯虚函数的类叫做抽象类,抽象类不能实例化对象
在这里插入图片描述
若创建一个子类继承抽象类,则该子类也包含纯虚函数,子类也会变成抽象类,所以子类创建对象也会报错
在这里插入图片描述

6.虚表指针和虚表

6.1什么是虚表指针

sizeof(Base) 大小是多少?
在这里插入图片描述
以结构体的内存对齐考虑,在32位机器下,大小应为8字节,但是实际上为12字节

在这里插入图片描述
_vfptr代表虚函数表指针
加上虚表指针,内存对齐后字节大小为12

6.2指向谁调用谁,传父类调用父类,传子类调用子类

  1. 父类对象的虚表与子类对象的虚表没有任何关系,这是两个不同的对象
  2. 通过虚表指针和虚函数表就可以实现多态性,即可以在运行时确定应该调用哪个类的虚函数
  3. 虚表指针是类级别的,而不是函数级别的
  4. 每个类只有一个虚表指针,指向其对应的虚函数表,但是多继承的时候,就会可能有多张虚表
  5. 每一个类中的虚函数在虚表中都有地址

在这里插入图片描述
如图所示, 图中父子类各自的虚表指针指向的虚表以及虚表内部的地址并不相同,这证明了父类的虚表指针和子类的虚表指针指向的虚表并不是同一个

7.多态的原理

class Person
{
public:virtual void BuyTicket(){cout << "成人:全价票" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket() {cout << "学生:半价票" << endl;}
};void func(Person* p)
{p->BuyTicket();//父类指针指向父类对象A(或子类对象B)
}int main()
{Person A;func(&A);//传父类对象Student B;func(&B);//传子类对象return 0;
}

父类对象内的虚函数放进了父类的虚表,子类对象继承了父类的同时也继承了父类的虚表,如果子类有虚函数的重写,那么父类的虚表内有父类的虚函数,子类的虚表内有子类的虚函数(重写后的虚函数)
指向谁调用谁,父类指针指向父类对象,则从父类的虚表中找到父类的虚函数;父类指针指向子类对象,则从子类的虚表中找到子类的虚函数,因此产生了指向父类调用父类,指向子类调用子类的现象

不是多态则是在编译时就是已经指向了某个地方,而不是因为指向谁调用了谁
多态的本质在底层看来就是在虚表内寻找虚函数

虚函数和普通函数一样都是存在于代码段中的,而不是存在虚表内部的,虚表内部仅仅储存了虚函数的地址,而虚表储存在常量区

虚函数表 本质是一个虚函数指针数组
子类的虚表是由父类的虚表拷贝过来的,再向其中填入新的地址,所以造成覆盖
为什么父类对象不可以实现多态,必须是父类的指针或引用?
若为父类对象,就会把子类中属于父类的那一部分拷贝给父类,有可能把子类的虚表也拷贝给父类,若拷贝成功,则父类对象的虚表就不知道是父类的虚表还是子类的虚表了
若为指针或者引用,将子类中属于父类那一部分切出来,使指针指向属于父类那一部分,或者作为属于父类那一部分的别名,子类的虚表还是子类的

8.单继承的虚表状态

class A
{
public:virtual void func1(){cout << "A::func1" << endl;}virtual void func2(){cout << "A::func2" << endl;}
};class B : public A
{
public:virtual void func1(){cout << "B::func1" << endl;}virtual void func3(){cout << "B::func3" << endl;}virtual void func4(){cout << "B::func4" << endl;}
};int main()
{A a;B b;return 0;
}

在这里插入图片描述
可以看到子类的虚表不正常,因为每个类中的虚函数在虚表中都要出现,而子类虚表里少了两个虚函数的地址,func1是重写的,func2是继承的(没有重写),而func3和func4不见了,子类自己的虚函数消失了
这里可以解释为一种bug:是Visual Studio监视窗口的bug
也可也理解为:子类的虚表实际上是拷贝了父类的虚表,重写的部分进行覆盖,没有重写的部分原模原样地拷贝,可以说是子类的虚表被隐藏了

9.多继承的虚表状态

class A
{
public:virtual void func1(){cout << "A::func1" << endl;}virtual void func2(){cout << "A::func2" << endl;}
private:int a;
};class B
{
public:virtual void func1(){cout << "B::func1" << endl;}virtual void func2(){cout << "B::func2" << endl;}
private:int b;
};class C : public A, public B
{
public:virtual void func1(){cout << "C::func1" << endl;}virtual void func3(){cout << "C::func3" << endl;}
private:int c;
};int main()
{C temp;return 0;
}

在这里插入图片描述
在这里插入图片描述
C由两个部分构成,第一个是继承了A, 一个类中只有一个虚表指针,所以A内部有一个虚表指针和一个int类型的对象a,第二个是继承了B, 一个类中只有一个虚表指针,所以B内部有一个虚表指针和一个int类型的对象b
C类的虚函数func3放到了继承的第一个类A的虚表内部
所以C中有两个虚表指针,同时这里的两个虚表指针不能合二为一,这关系于切片问题
所以多继承内部可能会有多个虚表指针

10.菱形继承

class A
{
public:virtual void func1(){cout << "A::func1" << endl;}
private:int a;
};class B : public A
{
public:virtual void func2(){cout << "B::func2" << endl;}
private:int b;
};class C : public A
{
public:virtual void func3(){cout << "C::func3" << endl;}
private:int c;
};class D : public B,public C
{
public:virtual void func4(){cout << "D::func4" << endl;}
private:int a;
};int main()
{D temp;return 0;
}

菱形继承和多继承没区别,同时func4放入了B的虚表内部
D类对象temp有两张虚表,分别是B和C的虚表

11.菱形虚继承

class A
{
public:virtual void func1(){cout << "A::func1" << endl;}int _a = 1;
};class B : virtual public A
{
public:virtual void func1(){cout << "B::func1" << endl;}int _b = 2;
};class C : virtual public A
{
public:virtual void func1(){cout << "C::func1" << endl;}int _c = 3;
};class D : public B,public C
{
public:virtual void func1(){cout << "D::func1" << endl;}virtual void func2(){cout << "D::func2" << endl;}int _d = 4;
};int main()
{D temp;return 0;
}

在这里插入图片描述

A的虚表由B、C共享
D自己新增的func2需要虚表,但D对象中的B没有的虚表
虚基表存储偏移量,帮助B、C找到A

注意:上述的B、C没有新增额外虚函数,如果有新增,则D的虚表消失,B和C各有一张虚表,D的虚函数放入B的虚表内,共计三张虚表

class A
{
public:virtual void func1(){cout << "A::func1" << endl;}int _a = 1;
};class B : virtual public A
{
public:virtual void func1(){cout << "B::func1" << endl;}virtual void func3(){cout << "B::func3" << endl;}int _b = 2;
};class C : virtual public A
{
public:virtual void func1(){cout << "C::func1" << endl;}virtual void func4(){cout << "C::func4" << endl;}int _c = 3;
};class D : public B,public C
{
public:virtual void func1(){cout << "D::func1" << endl;}virtual void func2(){cout << "D::func2" << endl;}int _d = 4;
};int main()
{D temp;return 0;
}

在这里插入图片描述

http://www.xdnf.cn/news/4098.html

相关文章:

  • Paramiko源码深入解析
  • 2025年PMP 学习四
  • Monster Hunter Rise 怪物猎人 崛起 [DLC 解锁] [Steam] [Windows SteamOS]
  • MySQL基础关键_008_DDL 和 DML(一)
  • linux、window安装部署nacos
  • STC单片机与淘晶驰串口屏通讯例程之02【HDMI数据处理】
  • LangChain构建大模型应用之Chain
  • APP 设计中的色彩心理学:如何用色彩提升用户体验
  • 模型训练实用之梯度检查点
  • 二重指针和二维数组
  • 深入理解 Cortex-M3 的内核寄存器组
  • 学习笔记msp430f5529lp
  • AI向量检索
  • 【前缀和】连续数组
  • 支持图文混排的Gemini Next Chat
  • Linux 系统下VS Code python环境配置!
  • GPU性能加速的隐藏魔法:Dual-Issue Warp Schedule全解析
  • 国内短剧 vs. 海外短剧系统:如何选择?2025年深度对比与SEO优化指南
  • 高并发内存池------threadcache
  • WebService的学习
  • 电子邮件相关协议介绍
  • NetSuite 2025.1 学习笔记
  • Java基础学完,继续深耕(0505)Linux 常用命令
  • TS 类class修饰符
  • 接口测试过程中常见的缺陷详解
  • Go小技巧易错点100例(三十)
  • 算法刷题篇
  • 基于Redis实现优惠券秒杀——第3期(分布式锁-Redisson)
  • UniGetUI 使用指南:轻松管理 Windows 软件(包括CUDA)
  • 【Springboot知识】Springboot计划任务Schedule详解