C++进阶之路---多态(二)

顾得泉:个人主页

个人专栏:《Linux操作系统》 《C++从入门到精通》  《LeedCode刷题》

键盘敲烂,年薪百万!


一、多态的原理

1.虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
     virtual void Func1()
     {
         cout << "Func1()" << endl;
     }
private:
     int _b = 1;
};

  

        通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

       那么派生类中这个表放了些什么呢?我们接着往下分析:

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
     virtual void Func1()
     {
         cout << "Base::Func1()" << endl;
     }
     virtual void Func2()
     {
         cout << "Base::Func2()" << endl;
     }
     void Func3()
     {
         cout << "Base::Func3()" << endl;
     }
private:
     int _b = 1;
};
class Derive : public Base
{
public:
     virtual void Func1()
     {
          cout << "Derive::Func1()" << endl;
     }
private:
     int _d = 2;
};
int main()
{
     Base b;
     Derive d;
     return 0;
}

       通过观察和测试,我们发现了以下几点问题:

       1.派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。

       2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

       3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

       4.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

       5.总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

       这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的?

       答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?

       实际我们去验证一下会发现vs下是存在代码段的,Linux g++下大家可以自己去验证。

2.多态的原理

       上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket

class Person 
{
public:
     virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:
     virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
    p.BuyTicket();
}
int main()
{
     Person Mike;
     Func(Mike);
     Student Johnson;
     Func(Johnson);
     return 0;
}

       1.观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。

       2.观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。

       3.这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

       4.反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。反思一下为什么?

       5.再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

3.动态绑定与静态绑定

       1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。

       2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。


二、单继承和多继承关系的虚函数表

       需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的。

1.单继承中的虚函数表

class Base 
{ 
public :
     virtual void func1() { cout<<"Base::func1" <<endl;}
     virtual void func2() {cout<<"Base::func2" <<endl;}
private :
     int a;
};
class Derive :public Base 
{ 
public :
     virtual void func1() {cout<<"Derive::func1" <<endl;}
     virtual void func3() {cout<<"Derive::func3" <<endl;}
     virtual void func4() {cout<<"Derive::func4" <<endl;}
private :
     int b;
};

       观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢下面我们使用代码打印出虚表中的函数。

class Base 
{
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
private:
    int a;
};
class Derive :public Base 
{
public:
    virtual void func1() { cout << "Derive::func1" << endl; }
    virtual void func3() { cout << "Derive::func3" << endl; }
    virtual void func4() { cout << "Derive::func4" << endl; }
private:
    int b;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
    // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
    cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
        VFPTR f = vTable[i];
        f();
    }
    cout << endl;
}
int main()
{
    Base b;
    Derive d;
    
    VFPTR * a = (VFPTR*)(*(int*)&b);
    PrintVTable(a);
    VFPTR* c = (VFPTR*)(*(int*)&d);
    PrintVTable(c);
    return 0;
}

2.多继承中的虚函数表

class Base1 
{
public:
    virtual void func1() { cout << "Base1::func1" << endl; }
    virtual void func2() { cout << "Base1::func2" << endl; }
private:
    int b1;
};
class Base2 
{
public:
    virtual void func1() { cout << "Base2::func1" << endl; }
    virtual void func2() { cout << "Base2::func2" << endl; }
private:
        int b2;
};
class Derive : public Base1, public Base2 
{
public:
    virtual void func1() { cout << "Derive::func1" << endl; }
    virtual void func3() { cout << "Derive::func3" << endl; }
private:
    int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
    cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
        VFPTR f = vTable[i];
        f();
    }
    cout << endl;
}
int main()
{
    Derive d;
    VFPTR* ptr1 = (VFPTR*)(*(int*)&d);
    PrintVTable(ptr1);
    VFPTR* ptr2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
    PrintVTable(ptr2);
    return 0;
}

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中:

3.菱形继承、菱形虚拟继承

       实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。如果好奇心比较强的友友们,可以去看下面的两篇链接文章。

       1.C++ 虚函数表解析

       2.C++ 对象的内存布局


三、多态问答总结

1.什么是多态?        答:参考专栏上篇文章:多态(一)

⒉.什么是重载、重写(覆盖)、重定义(隐藏)?        答:参考专栏上篇文章 :多态(一)

3.多态的实现原理?        答:参考本文上述内容

4. inline函数可以是虚函数吗?

       答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。

5.静态成员可以是虚函数吗?

       答:不能,因为静态成员函数没有this指针,使用类型:成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6.构造函数可以是虚函数吗?

       答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

7.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

       答:可以,并且最好把基类的析构函数定义成虚函数。参考本文内容。

8.对象访问普通函数快还是虚函数更快?

       答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

9.虚函数表是在什么阶段生成的,存在哪的?

       答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。


结语:C++关于多态的分享到这里就结束了,希望本篇文章的分享会对大家的学习带来些许帮助,如果大家有什么问题,欢迎大家在评论区留言~~~ 

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

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

相关文章

Vue首屏优化方案

在Vue项目中&#xff0c;引入到工程中的所有js、css文件&#xff0c;编译时都会被打包进vendor.js&#xff0c;浏览器在加载该文件之后才能开始显示首屏。若是引入的库众多&#xff0c;那么vendor.js文件体积将会相当的大&#xff0c;影响首屏的体验。可以看个例子&#xff1a;…

后端八股笔记------微服务篇

注册中心的主要作用&#xff1a;根据服务进行负载均衡&#xff0c;服务的健康监控。 服务雪崩&#xff0c;因为一个服务D的宕机&#xff0c;导致很多服务崩掉。 达到失败阈值----Closed_to_Open 服务降级------某一个接口 服务熔断------整个服务 限流常见的算法可以是令牌…

Upload 上传(图片/文件),回显(图片),下载(文件)

1.前端技术&#xff1a;V3 Ant Design Vue 2.后端技术&#xff1a;Java 图片上传/回显&#xff1a; 文件上传回显&#xff1a; 表结构&#xff1a;单文件/图片上传为A表对文件C表 &#xff08;A表field字段 对应 C表id字段&#xff09; 如图&#xff1a;A表中的 vehicle_d…

【测试工具系列】压测用Jmeter还是LoadRunner?还是其他?

说起JMeter&#xff0c;估计很多测试人员都耳熟能详。它小巧、开源&#xff0c;还能支持多种协议的接口和性能测试&#xff0c;所以在测试圈儿里很受欢迎&#xff0c;也是测试人员常用的工具&#xff0c;但是在企业级性能场景下可能会有性能瓶颈&#xff0c;更适合测试自己使用…

基于单片机的视觉导航小车设计

目 录 摘 要 I Abstract II 引 言 1 1 总体方案设计 3 1.1 方案论证 3 1.2 项目总体设计 3 2 项目硬件设计 4 2.1 主控模块设计 4 2.1.1单片机选型 4 2.1.2 STM32F103RCT6芯片 4 2.2单片机最小系统电路 5 2.3电机驱动模块设计 7 2.4红外模块设计 8 2.5红外遥控模块设计 9 2.6超…

一条 SQL 更新语句如何执行的

Server 层 存储引擎层 总流程 查询语句 连接器 查询缓存 分析器 优化器 执行器 更新语句 redo log&#xff08;节省的是随机写磁盘的 IO 消耗&#xff08;转成顺序写&#x…

动态规划刷题总结(入门)

目录 什么是动态规划算法 如何判断题目中将使用动态规划算法&#xff1f; 动态规划题目做题步骤 动态规划题目解析 泰波那契数模型 第 N 个泰波那契数 三步问题 使用最小花费爬楼梯 路径问题 不同路径 不同路径 Ⅱ 珠宝的最高价值 下降最短路径和 地下城游…

【Numpy】练习题100道(1-25题)

#学习笔记# 在学习神经网络的过程中发现对numpy的操作不是非常熟悉&#xff0c;遂找到了Numpy 100题。 Git-hub链接 目录 1 题目列表&#xff1a; 2 题解&#xff1a; 1 题目列表&#xff1a; 导入numpy包&#xff0c;并将其命名为np&#xff08;★☆☆&#xff09; 打印…

设计模式学习笔记 - 规范与重构 - 5.如何通过封装、抽象、模块化、中间层解耦代码?

前言 《规范与重构 - 1.什么情况下要重构&#xff1f;重构什么&#xff1f;又该如何重构&#xff1f;》讲过&#xff0c;重构可以分为大规模高层重构&#xff08;简称 “大型重构”&#xff09;和小规模低层次重构&#xff08;简称 “小型重构”&#xff09;。大型重构是对系统…

3.6研究代码(2)

指的是微电网运行参数。 在MATLAB中&#xff0c;randi([0,1],1,48) 会生成一个包含1*48个0或1的随机整数数组。这意味着数组中的每个元素都将是0或1。 MATLAB帮助中心&#xff1a;均匀分布的伪随机整数 - MATLAB randi - MathWorks 中国https://ww2.mathworks.cn/help/matlab/r…

Java零基础入门到精通_Day 1

01 Java 语言发展史 Java语言是美国Sun公司(StanfordUniversity Network)在1995年推出的 计算机语言Java之父:詹姆斯高斯林(ames Gosling) 重要的版本过度&#xff1a; 2004年 Java 5.0 2014年 Java 8.0 2018年 9月 Java 11.0 &#xff08;目前所使用的&#xff09; 02 J…

llama factory学习笔记

模型 模型名模型大小默认模块TemplateBaichuan27B/13BW_packbaichuan2BLOOM560M/1.1B/1.7B/3B/7.1B/176Bquery_key_value-BLOOMZ560M/1.1B/1.7B/3B/7.1B/176Bquery_key_value-ChatGLM36Bquery_key_valuechatglm3DeepSeek (MoE)7B/16B/67Bq_proj,v_projdeepseekFalcon7B/40B/18…

#LT8711V适用于Type-C/DP1.2/EDP转VGA应用方案,分辨率高达1080P。

1. 概述 LT8711V是一款高性能 Type-C/DP1.2 转 VGA 转换器&#xff0c;设计用于将 USB Type-C 源或 DP1.2 源连接到 VGA 接收器。 该LT8711V集成了一个符合DP1.2标准的接收器和一个高速三通道视频DAC。此外&#xff0c;还包括两个用于 CC 通信的 CC 控制器&#xff0c;以实现 …

文件服务器

文件服务器 # 构建NFS远程共享存储## 一、NFS介绍shell 文件系统级别共享&#xff08;是NAS存储&#xff09; --------- 已经做好了格式化&#xff0c;可以直接用。 速度慢比如&#xff1a;nfs&#xff0c;sambaNFS NFS&#xff1a;Network File System 网络文件系统&#xf…

C++面向对象..

1.面向对象的常见知识点 类、 对象、 成员变量(属性)、成员函数(方法)、 封装、继承、多态 2.类 在C中可以通过struct、class定义一个类 struct和class的区别&#xff1a; struct的默认权限是public(在C语言中struct内部是不可以定义函数的) 而class的默认权限是private(该权…

Windows虚拟机的安装

Windows系统 总结知识点记忆级别&#xff1a; 1级&#xff1a;熟练记忆讲过的所有知识点(按照授课顺序&#xff0c;笔记顺序来提问)2级&#xff1a;灵活应用所学知识点(不按照顺序提问&#xff0c;面临陷阱提问)3级&#xff1a;应用所学知识解决实际问题4级&#xff1a;扩展应…

24 深度卷积神经网络 AlexNet【李沐动手学深度学习v2课程笔记】(备注:含AlexNet和LeNet对比)

目录 1. 深度学习机器学习的发展 1.1 核方法 1.2 几何学 1.3 特征工程 opencv 1.4 Hardware 2. AlexNet 3. 代码 1. 深度学习机器学习的发展 1.1 核方法 2001 Learning with Kernels 核方法 &#xff08;机器学习&#xff09; 特征提取、选择核函数来计算相似性、凸优…

陈景东:集中与分布式拾音与声信号处理 | 演讲嘉宾公布

一、声音与音乐技术专题论坛 声音与音乐技术专题论坛将于3月28日同期举办&#xff01; 声音的应用领域广泛而深远&#xff0c;从场所识别到乐器音响质量评估&#xff0c;从机械故障检测到心肺疾病诊断&#xff0c;声音都发挥着重要作用。在互联网、大数据、人工智能的时代浪潮中…

【python】random库函数使用简要整理

前言 简要快速清晰整理random库 函数 函数作用random()返回0-1间的浮点小数randint(1,10)返回1到10间的整数uniform(1,10)返回1-10间的小数randrange(1,10,2)从1每隔2取一个数到10&#xff0c;在这些数中返回一个choice(列表)从列表中随机返回一个 shuffle(列表) 对列表内容…

高等数学常用公式

高等数学常用公式 文章目录 内容大纲 内容 大纲 感谢观看 期待关注 有问题的小伙伴请在下方留言&#xff0c;喜欢就点个赞吧