C++多态详解

文章目录

    • 多态
      • 概念
      • 定义及实现
        • 构成条件
        • 虚函数
        • 虚函数的重写
        • override 和 final
        • 重载、覆盖、隐藏
      • 抽象类
        • 纯虚函数
        • 接口继承与实现继承
      • 多态的原理
        • 虚函数表
        • 原理
        • 动态绑定与静态绑定
      • 多继承的虚函数表
        • 多继承中的虚函数表

多态

概念

多态是面向对象三大特性中相对复杂的一个,他从直观上理解就是,不同的人(对象)做同一件事情(调用函数),会产生不同的结果(状态)

多态又分为静态多态和动态多态,静态多态其实就是函数重载,在本篇文章中主要介绍动态多态,在讲到多态的原理时,我们会细讲其中的区别

定义及实现

多态是在不同的为继承关系的类对象,调用同一个函数(同名函数),产生的不同的行为

例如,Student是Person的子类,同样的买票动作下,Person是全价,而Student是全价

构成条件

构成多态必须有两个条件

  1. 必须通过父类的指针或引用调用虚函数
  2. 被调用的函数必须是虚函数,子类必须重写父类的虚函数

例如

#include<iostream>
using namespace std;
class Person
{
public:
    virtual void Buyticket()
    {
        cout << "Person:全价" << endl;
    }
};
class Student : public Person
{
public:
    virtual void Buyticket()
    {
        cout << "Student:半价" << endl;
    }
};

void func(Person & people)
{
    people.Buyticket();
}

int main()
{
    Person P;
    Student S;

    func(P);
    func(S);

    P.Buyticket();
    S.Buyticket();
}

屏幕截图 2024-03-05 184126.png

简单说就是,谁调用就用谁的同名函数,要注意这两个条件缺一不可

虚函数

被virtual修饰的成员函数称为虚函数,刚刚我们也用到了

class Person
{
public:
    virtual void Buyticket()
    {
        cout << "Person:全价" << endl;
    }
};
虚函数的重写

虚函数的重写也叫做覆盖,要和函数重载,继承中的隐藏相区分

重写是子类和父类中有一个完全相同的虚函数(返回值、函数名、参数列表),称之为子类虚函数重写父类的虚函数

在实际中,子类不加virtual关键字时仍然可以构成多态,是因为继承的基类虚函数在子类中依然保持虚函数属性,但是不建议省略

虚函数重写中有两个例外

  1. 协变
    基类与派生类的虚函数返回值类型不同,这里的不同指的是,基类虚函数返回基类指针或引用,派生类虚函数返回派生类指针或引用,构成协变,依然属于重写

    class A{};
    class B : public A {};
    
    class C
    {
    public:
        virtual A* f()
        {
            return new A;
        }
    }
    
    class D : public C
    {
    public:
        virtual B* f()
        {
            return new B;
        }
    }
    
  2. 析构函数重写
    父类的析构函数为虚函数,只要子类的析构函数定义,由于继承了父类的虚函数特性,无论是否加virtual关键字,都构成重写,因为编译器对析构函数的名称统一处理为destructor,也满足多态的条件

override 和 final

这两个关键字是C++11提供的,用于确保重写

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

class A
{
public:
    virtual void func() final {};
};

override会检查该函数是否重写了某个虚函数,如果没有重写则编译器报错

重载、覆盖、隐藏

函数重载的条件

  1. 两个函数处于相同作用域
  2. 函数名相同
  3. 参数列表不相同

重写(覆盖)的条件

  1. 两个函数分别在父类和子类
  2. 函数名,参数,返回值必须相同(除协变)
  3. 必须是两个虚函数

重定义(隐藏)的条件

  1. 两个函数分别在父类和子类
  2. 函数名相同

这里我们发现,重写比隐藏的条件要苛刻,实际上在父类和子类的派生类,只要函数名相同,不是重写就是隐藏

抽象类

纯虚函数

在虚函数的后面写上 = 0 就是纯虚函数

包含纯虚函数的类叫做抽象类,也叫做接口类

抽象类不能实例化出对象,派生类继承后也不能实例化,抽非重写纯虚函数,才能实例化

纯虚函数更体现了接口继承的特性

例如

class Base
{
public:
    virtual void Func() = 0;
};

class A : public Base
{
public:
    virtual void Func()
    {
        cout << "A::Func()" << endl;
    }
};

class B : public Base
{
public:
    virtual void Func()
    {
        cout << "B::Func()" << endl;
    }
};

void test()
{
    Base* pA = new A;
    Base* pB = new B;
    pA->Func();
    pB->Func();
}
接口继承与实现继承

普通函数的继承是一种实现继承,派生类继承基类的函数,可以使用函数,继承的是函数的实现,或者说函数体

虚函数的继承是一种接口继承,派生类继承基类虚函数的接口,是为了重写,实现多态,继承的是接口,因此只需要在实现多态的时候定义虚函数

多态的原理

虚函数表

有这样一个类

class Base
{
public:
    virtual void Func1()
    {
        cout << "Base::Func1" << endl;
    }
protected:
    int _base;
};

当我们输出sizeof Base时会发现,他的大小是8,查看监视窗口

屏幕截图 2024-03-05 192153.png

我们发现还有一个_vfptr的指针(x86,32位平台)

这个指针我们叫做虚函数表指针,一个包含虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址都要放到虚函数表中

我们对Base类继承,如下

class Base
{
public:
    virtual void Func1()
    {
        cout << "Base::Func1" << endl;
    }
    virtual void Func2()
    {
        cout << "Base::Func2" << endl; // 添加虚函数Func2
    }
    void Func3()
    {
        cout << "Base::Func3" << endl; // 添加普通函数Func3
    }
protected:
    int _base;
};

class Derive : public Base
{
public:
    virtual void Func1()
    {
        cout << "Derive::Func1" << endl; // 重写虚函数Func1
    }
protected:
    int _derive;
};
int main()
{
    Base b;
    b._base = 1;
    Derive d;
    d._derive = 1;
    d._base = 2;
    return 0;
}

这里因为VS2022的限制,由监视窗口就不能看出来具体的构造了,需要通过内存分析

我们先看Base类的

屏幕截图 2024-03-05 195613.png

通过这里我们可以发现,首先虚函数表实际上是一个函数指针数组,其次不论有无被重写,都会存在于虚函数表中,普通函数不会存在虚表之中

然后再看Derive类的

屏幕截图 2024-03-05 200753.png

这里我们发现,虚表地址不同,说明在继承之后虚表是重新创建了一份,重写函数地址不同,虚表的第一行是重写的Func()地址不同,也就是被覆盖,因此重写也称为覆盖

其次未重写的虚函数,由于继承,也在虚表中出现

最后,虚表的最后一项为空指针,经过验证,Linux g++没有这个空指针

因为虚表实际上是一个函数指针数组,我们可以通过指针分别打印出他们的地址,并判断出他是存在于内存的哪个区域

经过验证,虚函数和虚函数表是存在于代码段的,虚函数表中存的是虚函数指针,对象中存的是虚函数表指针

原理

通过上面的虚函数表分析,我们可以大致可以了解原理应该是这样的

当调用函数时,编译器直接访问第一个虚函数表指针,找到虚表,在调用对应的虚函数即可,这样就实现了多态的功能

动态绑定与静态绑定

静态绑定又称为前期绑定、早绑定,程序在编译时就确定了程序的行为,也称为静态多态

动态绑定又称为后期绑定、晚绑定,在程序运行期间,需要根据类型确定程序的行为和具体调用的函数,称为动态多态

我们可以通过对应的汇编代码来观察这一过程

多继承的虚函数表

单继承的虚函数表我们已经在上面的虚函数表原理中介绍过了,这里不再赘述

多继承中的虚函数表
class Base1
{
public:
    virtual void func1()
    {
        cout<<"Bas1::func1"<<endl;
    }
    virtual void func2()
    {
        cout<<"Base1::func2"<<endl;
    }
    int b1;
};

class Base2
{
public:
    virtual void func1()
    {
        cout<<"Base2::func1"<<endl;
    }
    virtual void func3()
    {
        cout<<"Base2::func2"<<endl;
    }
	int b2;    
};
class Derive : public Base1, public Base2
{
public:
    virtual void func1()
    {
        cout<<"Derive::func1"<<endl;
    }
    virtual void func3()
    {
        cout<<"Derive::func3"<<endl;
    }
    int d1;
};
int main()
{
    Derive d;
    
    return 0;
}

这里的内存分布就类似于之前的了,只是子类的虚函数和第一个父类公用一张虚函数表

屏幕截图 2024-03-05 212030.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/431833.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

c语言网络编程学习整理 网络编程结构框架 一些常见协议的介绍

1.网络分层&#xff1a;osi体系结构 重点&#xff1a;网络层&#xff0c;传输层。 口诀&#xff1a;物数网传会表应。 可是osi体系过于理想&#xff0c;不过其为原型依旧通用&#xff1a; TCP/IP协议 是Internet事实上的工业标准 2.TCP/IP 4层模型 1&#xff09;网络接口与…

Java生成 word报告

Java生成 word报告 一、方案比较二、Apache POI 生成三、FreeMarker 生成 在网上找了好多天将数据库信息导出到 word 中的解决方案&#xff0c;现在将这几天的总结分享一下。总的来说&#xff0c;Java 导出 word 大致有 5 种。 一、方案比较 1. Jacob Jacob 是 Java-COM Bri…

7款炫酷的前端动画特效分享(三)(附效果图及在线演示)

分享7款好玩的前端动画特效 其中有CSS动画、SVG动画、js小游戏等等 下方效果图可能不是特别的生动 那么你可以点击在线预览进行查看相应的动画特效 同时也是可以下载该资源的 CSS3模仿四季交替动画 基于HTML5CSS3实现的卡通风格一年四季交替动画特效 以下效果图只能体现框架的…

ThreadPoolExecutor 学习

ThreadPoolExecutor 是开发中最常用的线程池&#xff0c;今天来简单学习一下它的用法以及内部构造。 1、线程池存在的意义&#xff1f; 一般在jvm上&#xff0c;用户线程和操作系统内核线程是1&#xff1a;1的关系&#xff0c;也就是说&#xff0c;每次创建、销毁线程的时候&am…

10.WEB渗透测试-Linux基础知识-Linux用户权限管理(下)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;9.WEB渗透测试-Linux基础知识-Linux用户权限管理&#xff08;上&#xff09;-CSDN博客 ch…

Linux进程详细介绍

文章目录 Linux进程1、计算机体系结构和操作系统管理1.1、计算机体系结构 -- 硬件1.2、操作系统&#xff08;Operator System&#xff09; -- 软件 2、进程2.1、进程基本概念2.2、进程标识符2.2.1、获取当前进程标识符和当前进程的父进程标识符2.2.2、通过系统调用创建进程 -- …

微信小程序开发学习笔记《19》uni-app框架-配置小程序分包与轮播图跳转

微信小程序开发学习笔记《19》uni-app框架-配置小程序分包与轮播图跳转 博主正在学习微信小程序开发&#xff0c;希望记录自己学习过程同时与广大网友共同学习讨论。建议仔细阅读uni-app对应官方文档 一、配置小程序分包 分包可以减少小程序首次启动时的加载时间 为此&#…

Google Play上架:自查封号政策解析(高风险行为之不允许破坏Google Play生态系统中用户信任度的应用或应用内容)

本文章提供给近期被封号的开发者们&#xff0c;希望能带来帮助&#xff0c;有其他的自查方向后续也会发布出来。 ——————————————————————————————————————— 用户数据设备和网络滥用 用户数据 设备和网络滥用

前端学习之HTML(第二天)--多媒体标签和表格标签

注&#xff1a;里面的注释是对各个标签的解释 多媒体标签 <!DOCTYPE html> <html> <head><meta charset"utf-8"><title></title> </head> <body> <!-- audio是音频可以填写绝对路径也可填写相对路径 --> &l…

解决微软活动目录管理工作中常见问题

微软活动目录&#xff08;AD域&#xff09;是一种由微软的用于管理网络中用户、计算机、资源等的目录服务。活动目录被广泛应用于企业内部的网络管理中&#xff0c;尤其是对于使用微软产品的企业来说&#xff0c;活动目录是至关重要的基础设施之一。 因此&#xff0c;以微软为…

索引下推 INDEX CONDITION PUSHDOWN

索引下推 (INDEX CONDITION PUSHDOWN&#xff0c;简称ICP)是在 MySQL5.6 针对扫描索引下推二级索引的一项优化改进。 用来在范围查询时减少回表的次数。ICP适用于 MYISAM和INNODB.

ref和reactive用哪个?

ref和reactive用哪个? 1.&#x1f916;GPT&#x1f916;:ref和reactive用哪个根据数据类型而定 ref 用于将基本类型的数据&#xff08;如字符串、数字&#xff0c;布尔值等&#xff09;转换为响应式数据。使用 ref 定义的数据可以通过 .value 属性访问和修改。 reactive 用于…

JavaScript 学习笔记(7)

一模板字符串 1.用途 允许在字符串中嵌入表达式和变量&#xff0c;是一种方便的字符串语法 2.用法 模板字符串使用反引号 作为字符串的定界符分隔的字面量&#xff1b;模板字面量是用反引号&#xff08;&#xff09;分隔的字面量&#xff0c;允许多行字符串、带嵌入表达式…

ElasticSearch之分布式查询过程分析

写在前面 本文一起看下es分布式查询的过程。 1&#xff1a;分布式搜索过程 分布式搜索分为两个阶段&#xff0c;query和fetch,即query-then-fetch。 假定primary shard3,replica shard1&#xff0c;即3个主分片&#xff0c;1个副本分片。 1.1&#xff1a;query阶段 某data …

二叉树——700. 二叉搜索树中的搜索、98. 验证二叉搜索树

二叉搜索树中的搜索 给定二叉搜索树&#xff08;BST&#xff09;的根节点 root 和一个整数值 val。 你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在&#xff0c;则返回 null 。 示例 1: 输入&#xff1a;root [4,2,7,1,3], val 2 …

【论文精读】基于知识图谱关系路径的多跳智能问答模型研究

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

96道前端面试题,前端开发工作内容

HTML、CSS、JS三大部分都起什么作用&#xff1f; HTML内容层&#xff0c;它的作用是表示一个HTML标签在页面里是个什么角色&#xff1b;CSS样式层&#xff0c;它的作用是表示一块内容以什么样的样式&#xff08;字体、大小、颜色、宽高等&#xff09;显示&#xff1b;JS行为层…

js形参传递特殊字符

在前端我们给其他页面传值或者传数据到后台的时候&#xff0c;字符串经常将一些特殊符号识别成字符集。这种情况下会将数据打断或者打乱&#xff0c;比如字符串里面包含*/&这些符号的时候就会错误。 我们可以通过将字符中的特殊字符替换成十六进制的字符&#xff0c;一些特…

【详识JAVA语言】String类2

常用方法 字符串的不可变性 String是一种不可变对象. 字符串中的内容是不可改变。字符串不可被修改&#xff0c;是因为&#xff1a; 1. String类在设计时就是不可改变的&#xff0c;String类实现描述中已经说明了 以下来自JDK1.8中String类的部分实现&#xff1a; String类…

马士超:符合国际标准的沉浸式音频HOLOSOUND的发展与未来 | 演讲嘉宾公布

一、3D音频 3D 音频分论坛将于3月27日同期举办&#xff01; 3D音频技术不仅能够提供更加真实、沉浸的虚拟世界体验&#xff0c;跨越时空的限制&#xff0c;探索未知的世界。同时&#xff0c;提供更加丰富、立体的情感表达和交流方式&#xff0c;让人类能够更加深入地理解彼此&a…