【C++ 学习 ⑲】- 多态(下)

news2024/11/25 16:48:45

目录

一、虚函数表和多态的原理

1.1 - 虚函数表

1.2 - 多态的原理

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

2.1 - 单继承关系中的虚函数表

2.2 - 多继承关系中的虚函数表

三、纯虚函数和抽象类



一、虚函数表和多态的原理

1.1 - 虚函数表

  1. 问:sizeof(b) 是多少?

    #include <iostream>
    using namespace std;
    ​
    class Base
    {
    public:
        virtual void func1() { cout << "Base::func1()" << endl; }
        virtual void func2() { cout << "Base::func2()" << endl; }
        void func3() { cout << "Base::func3()" << endl; }
    protected:
        int _i = 1;
    };
    ​
    int main()
    {
        Base b;
        cout << sizeof(b) << endl;
        return 0;
    }

    通过调试可以发现,在 b 对象内存模型中,除了 _i 成员,还有一个名为 _vfptr 的成员,它是虚函数表指针,所以 sizeof(b) 是 8 或 16 字节

    一个含有虚函数的类对象至少有一个指向虚函数表的指针,虚函数表本质上是一个存放虚函数地址的函数指针数组,一般情况下在这个数组的最后面还放了一个 nullptr

    虚函数表可以简称为虚表。因为 func3 不是虚函数,所以没有放进虚表中。

  2. 提一个很容易混淆的问题:虚函数存在哪里?虚表又存在哪里?虚函数和普通函数一样,都是存在代码段的;而虚表存在哪里可以通过以下代码得知

    #include <iostream>
    using namespace std;
    ​
    class Base
    {
    public:
        virtual void func1() { cout << "Base::func1()" << endl; }
        virtual void func2() { cout << "Base::func2()" << endl; }
        void func3() { cout << "Base::func3()" << endl; }
    protected:
        int _i = 1;
    };
    ​
    typedef void(*VFPTR)();
    ​
    int main()
    {
        int m = 0;
        printf("栈:%p\n", &m);
    ​
        int* p1 = new int;
        printf("堆:%p\n", p1);
    ​
        static int n = 0;
        printf("静态区:%p\n", &n);
    ​
        const char* str = "abcdef";
        printf("常量区:%p\n", str);
    ​
        Base b;
        // 通过对象的地址获取虚函数表的地址
        VFPTR* p2 = (VFPTR*)*(int*)&b;  
        printf("虚表:%p\n", p2);
        return 0;
    }

    根据输出结果,我们有理由相信虚表也是存在代码段的

  3. 让派生类 Derive 继承自 Base,然后在派生类中重写基类虚函数 func1:

    #include <iostream>
    using namespace std;
    ​
    class Base
    {
    public:
        virtual void func1() { cout << "Base::func1()" << endl; }
        virtual void func2() { cout << "Base::func2()" << endl; }
        void func3() { cout << "Base::func3()" << endl; }
    protected:
        int _i = 1;
    };
    ​
    class Derive : public Base
    {
    public:
        virtual void func1() { cout << "Derived::func1()" << endl; }
    protected:
        int _j = 2;
    };
    ​
    int main()
    {
        Base b;
        Derive d;
        return 0;
    }

    因为在派生类中重写了基类的虚函数 func1,所以基类对象 b 和派生类对象 d 的虚表是不一样的

    d 的虚表中存的是重写的 Derive::func1,所以虚函数的重写也叫作覆盖,覆盖就是虚表中虚函数的覆盖。重写是语法上的叫法,覆盖是原理层的叫法

1.2 - 多态的原理

#include <iostream>
using namespace std;
​
class Base
{
public:
    virtual void func1() { cout << "Base::func1()" << endl; }
    virtual void func2() { cout << "Base::func2()" << endl; }
    void func3() { cout << "Base::func3()" << endl; }
protected:
    int _i = 1;
};
​
class Derive : public Base
{
public:
    virtual void func1() { cout << "Derive::func1()" << endl; }
protected:
    int _j = 2;
};
​
int main()
{
    Base b;
    Base* pb = &b;
    pb->func1();  // Base::func1()
​
    Derive d;
    pb = &d;
    pb->func1();  // Derive::func1()
    return 0;
}

当基类指针 pb 指向基类对象 b 时,pb->func1(); 就是在 b 的虚表中找到虚函数 Base::func1

当基类指针 pb 指向派生类对象 d 时,pb->func1(); 就是在 d 的虚表中找到虚函数 Derive::func1

这样就让基类指针表现出了多种形态

注意:不满足多态的函数调用是编译时确定好的,满足多态的函数调用是运行时去对象中找的

Base b;
b.func1();
// 00195182 lea     ecx,[b]
// 00195185 call    Person::func1 (01914F6h)
// 汇编代码分析:
// 虽然 func1 是虚函数,但是 b 是对象,不满足多态的条件,所以这里是普通函数的调用,
// 编译时就确定好了函数的地址,直接 call。
​
Base* pb = &b;
pb->func1();
// 注意:不相关的汇编代码被省去了
// 001940DE mov     eax,dword ptr [pb]
// 001940E1 mov     edx,dword ptr [eax]
// 00B823EE mov     eax,dword ptr [edx]
// 001940EA call    eax
// 汇编代码分析:
// 1、pb 中存的是 b 对象的地址,将 pb 移动到 eax 中
// 2、[eax] 就是取 eax 值指向的内容,相当于把 b 对象中的虚表指针移动到 edx
// 3、[edx] 就是取 edx 值指向的内容,相当于把虚表中第一个虚函数的地址移动到 eax
// 4、call eax 中存的虚函数地址
// 由此可以看出满足多态的函数调用,不是在编译时确定的,而是运行起来后去对象中找的。

 


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

2.1 - 单继承关系中的虚函数表

#include <iostream>
using namespace std;
​
class Base
{
public:
    virtual void func1() { cout << "Base::func1()" << endl; }
    virtual void func2() { cout << "Base::func2()" << endl; }
protected:
    int _i = 1;
};
​
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; }
protected:
    int _j = 2;
};
​
int main()
{
    Base b;
    Derive d;
    return 0;
}

在 d 的虚表中,我们看不到虚函数 func3 和 func4,这可能是监视窗口故意隐藏了这两个函数,也可能是一个小 bug,我们可以通过以下代码进行验证

typedef void(*VFPTR)();
​
void PrintVftable(VFPTR vftable[])
{
    for (size_t i = 0; vftable[i] != nullptr; ++i)
    {
        printf("第 %d 个虚函数地址:0X%p, -->", i, vftable[i]);
        vftable[i]();
    }
    cout << endl;
}
​
void test()
{
    Base b;
    VFPTR* p1 = (VFPTR*)*(int*)&b;
    PrintVftable(p1);
​
    Derive d;
    VFPTR* p2 = (VFPTR*)*(int*)&d;
    PrintVftable(p2);
}

 

2.2 - 多继承关系中的虚函数表

#include <iostream>
using namespace std;
​
class Base1
{
public:
    virtual void func1() { cout << "Base1::func1()" << endl; }
    virtual void func2() { cout << "Base1::func2()" << endl; }
protected:
    int _i1;
};
​
class Base2
{
public:
    virtual void func1() { cout << "Base2::func1()" << endl; }
    virtual void func2() { cout << "Base2::func2()" << endl; }
protected:
    int _i2;
};
​
class Derive : public Base1, public Base2
{
public:
    virtual void func1() { cout << "Derive::func1()" << endl; }
    virtual void func3() { cout << "Derive::func3()" << endl; }
protected:
    int _j;
};
​
typedef void(*VFPTR)();
​
void PrintVftable(VFPTR vftable[])
{
    for (size_t i = 0; vftable[i] != nullptr; ++i)
    {
        printf("第 %d 个虚函数地址:0X%p, -->", i, vftable[i]);
        vftable[i]();
    }
    cout << endl;
}
​
int main()
{
    Derive d;
    VFPTR* p1 = (VFPTR*)*(int*)&d;
    PrintVftable(p1);
​
    // VFPTR* p2 = (VFPTR*)*(int*)((char*)&d + sizeof(Base1));
    // PrintVftable(p2)
    // 或者:
    Base2* p2 = &d;
    PrintVftable((VFPTR*)*(int*)p2);
    return 0;
}

  1. 派生类中的虚函数 func3 放在第一个继承自基类部分的虚函数表中

  2. 假设有以下场景

    Derive d;
    Base1* p1 = &d;
    p1->func1();
    Base2* p2 = &d;
    p2->func1();

    首先要确定的是

    所以在语句 p2->func1(); 中,需要修正 this 指针

    这也是为什么在 d 的两个虚表中,重写的虚函数 func1 的地址不一样


三、纯虚函数和抽象类

纯虚函数是一种特殊的虚函数,在某些情况下,在基类中不能对虚函数给出有意义的实现,就可以把它声明为纯虚函数。纯虚函数只有函数名、参数和返回值类型,没有函数体,具体实现留给派生类去做。具体语法:virtual 返回值类型 函数名(参数列表) = 0;

含有纯虚函数的类被称为抽象类(或接口类),不能实例化对象,但可以创建指针和引用

派生类必须重写抽象类中的纯虚函数,否则也属于抽象类

#include <iostream>
using namespace std;
​
class Car
{
public:
    virtual void Drive() = 0;
};
​
class AITO : public Car
{
public:
    virtual void Drive() { cout << "Intelligent" << endl; }
};
​
class AVATR : public Car
{
public:
    virtual void Drive() { cout << "Comfortable" << endl; }
};
​
void func1(Car* p) { p->Drive(); }
​
void func2(Car& c) { c.Drive(); }
​
int main()
{
    AITO aito;
    AVATR avatr;
​
    func1(&aito);  // Intelligent
    func1(&avatr);  // Comfortable
​
    func2(aito);  // Intelligent
    func2(avatr);  // Comfortable
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用docker部署db2

1.使用docker部署db2 1.1 拉db2镜像 将db2镜像拉起到本地。 docker pull ibmcom/db21.2启动容器 docker run -d -p 50000:50000 --name db2 --privilegedtrue -e DB2INST1_PASSWORDdbPassword DBNAMEjumpdb -e LICENSEaccept -v /usr/local/db2:/database ibmcom/db2实例化…

选择成都优优聚的优势是什么?

美团代运营是一种服务模式&#xff0c;旨在帮助商家提升线上销售业绩&#xff0c;并有效降低经营风险。通过专业团队的运营管理&#xff0c;商家可以获得更加稳定和可靠的线上业务经营。美团代运营提供了一整套解决方案&#xff0c;包括线上推广、店铺运营、商品管理、客户服务…

武汉旅游地

原文链接&#xff1a;https://www.cnblogs.com/MrFlySand/p/17678215.html 发表时间&#xff1a;2023年9月4日21:59:14 更新时间&#xff1a;2023年9月4日21:59:06 东湖飞鸟世界(动物园) 地址&#xff1a;东湖风景区沿湖大道20号时间&#xff1a;9:00-17:00交通&#xff1a;地铁…

远距离WiFi模组方案,实现移动设备之间高效通信,无人机远程图传应用

随着科技的不断进步&#xff0c;无线通信技术也在日新月异地发展。其中&#xff0c;WiFi技术已经成为现代生活中不可或缺的一部分。 从室内到室外&#xff0c;WiFi的应用场景正在不断扩大&#xff0c;为我们的日常生活和工业生产带来了极大的便利。 WiFi技术&#xff0c;即无…

斩获两大年度奖项,这家厂商如何决胜汽车智能化下半场

汽车智能化决战下半场的鼓声已经敲响。 一方面&#xff0c;智能座舱正在向3.0时代迈进&#xff0c;域集中式架构、多域融合已经成为了全新的市场趋势。 另一方面&#xff0c;软件正在成为车企构建差异化产品的重要手段&#xff0c;未来将成为车企盈利的重要组成部分。在这样的…

【算法与数据结构】700、LeetCode二叉搜索树中的搜索

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;二叉搜索树的性质&#xff1a;左节点键值 < 中间节点键值 < 右节点键值。那么我们根据此性质&am…

Dice系数衡量图像分割中的重叠区域

学习目标 Dice系数和mIoU是均是语义分割的评价指标&#xff0c;今天这里就着重讲讲Dice系数&#xff0c;顺便提一下Dice Loss&#xff0c;以后有时间区分一下在语义分割中两个常用的损失函数&#xff0c;交叉熵和Dice Loss。 语义分割中评价指标的重要性 语义分割是计算机视…

Cannot read property ‘database‘ of undefined解决办法

PS&#xff1a;在最近项目部署的时候&#xff0c;后台遇到如下的报错&#xff0c;显示数据库未定义&#xff0c;研究了半天没有找到原因&#xff0c;但是能解决掉这个报错 TypeError: Cannot read property ‘database’ of undefined 我们查看下具体的文件目录 我们需要返回…

2023年人力资源服务行业研究报告

第一章 行业概况 1.1 定义 在2017年6月发布的《国民经济行业分类》文件中&#xff0c;人力资源服务行业被定义为提供劳动者就业和职业发展的相关服务&#xff0c;以及为雇主管理和开发人力资源的相关服务。这些服务主要包括人力资源招聘、职业指导、人力资源和社会保障事务代…

【1++的数据结构】之map与set(二)

&#x1f44d;作者主页&#xff1a;进击的1 &#x1f929; 专栏链接&#xff1a;【1的数据结构】 文章目录 一&#xff0c;前言二&#xff0c;红黑树的概念及其性质三&#xff0c;红黑树的插入四&#xff0c;红黑树的验证五&#xff0c;map与set的封装红黑树迭代器的实现map重载…

c语言flag的使用

flag在c语言中标识某种状态或记录某种信息&#xff0c;可以通过修改flag中来控制程序流程,判断某种状态是否存在或记录某种信息 操作:(1)初始化 (2)赋值 (3)判断 (4)修改 (5)去初始化 #include <stdlib.h>int power_state_check;int main() {int i 0;power_state_check…

统计命令汇总

适用于Unix体系 关于wc命令 Word Count 用于统计指定文件中的字节数、字数、行数&#xff0c;并将统计结果显示输出。 wc [-lcw] c 统计字节数 l 统计行数 m 统计字符数&#xff0c;此标志不能与-c标志一起使用 w 统计字数。一个字定义为由空白、跳格或换行字符分隔的字符串 统…

mysql数据库使用技巧整理

查看当前数据库已建立的client连接 > SHOW VARIABLES LIKE max_connections; -- 查看数据库允许的最大连接数&#xff0c;不是实时正在使用的连接数 > SHOW STATUS LIKE Threads_connected; -- 查看当前数据库client的连接数 > SHOW PROCESSLIST; -- 查看具体的连接

界面控件DevExtreme(v23.2)下半年发展路线图

在这篇文章中&#xff0c;我们将介绍DevExtreme在v23.2中发布的一些主要特性&#xff0c;这些特性既适用于DevExtreme JavaScript (Angular、React、Vue、jQuery)&#xff0c;也适用于基于DevExtreme的ASP. NET MVC/Core控件。 DevExtreme包含全面的高性能和响应式UI小部件集合…

论文翻译 : 风廓线对地面机载风能系统性能的影响

目录 摘要1 引言2. 风力条件2.2 风况2.3 风况聚类2.4 聚类分析2.5 聚类统计分析 3 引言 摘要 摘要&#xff1a;本研究通过确定基于现实垂直风速剖面的循环可行的功率优化飞行轨迹&#xff0c;来研究抽水模式地面生成的空中风能系统&#xff08;AWESs&#xff09;的性能。这些1…

基于Springcloud的基础框架,统一gateWay网关鉴权demo,附下载地址

基于Springcloud的基础框架&#xff0c;统一gateWay网关鉴权demo&#xff0c;附下载地址 使用方式&#xff1a; 1、搭建nacos环境&#xff0c;修改对应nacos地址 2、修改mysql地址,导入sql语句 ###框架内容 SpringcloudGatewayJWTNacosFeginmysqlMybatis plus 具体功能 基于…

基于Matlab实现多个图像融合案例(附上源码+数据集)

图像融合是将多幅图像合成为一幅图像的过程&#xff0c;旨在融合不同图像的信息以获得更多的细节和更丰富的视觉效果。在本文中&#xff0c;我们将介绍如何使用Matlab实现图像融合。 文章目录 简单案例源码数据集下载 简单案例 首先&#xff0c;我们需要了解图像融合的两种主…

Linux之修改服务端口号

本次演示以SSH服务为例&#xff0c;SSH默认监听端口是22,先保留了22端口&#xff0c;所以我们要进入ssh的配置文件添加新端口并注释或删掉原有端口。 1、使用vi编辑器修改文件 sshd_config,路径是/etc/ssh/sshd_config,找到“#Port 22”,添加新的端口号10086。 2、如果你关闭了…

2023开学礼中国海洋大学《乡村振兴战略下传统村落文化旅游设计》许少辉新海洋图书馆

2023开学礼中国海洋大学《乡村振兴战略下传统村落文化旅游设计》许少辉新海洋图书馆

建筑结构健康监测系统:智能监测建筑结构健康状况

大型公共建筑因其投资大、结构形式复杂、建设工期长、施工工艺复杂、使用年限长等特点。在使用期间受超常荷载、材料老化、构件缺陷等因素的作用&#xff0c;结构将逐渐产生损伤累积&#xff0c;从而使结构的承载能力降低&#xff0c;抵抗自然灾害的能力下降。如遇地震、台风等…