C++:多态的原理

目录

一、多态的原理

1.虚函数表 

2.多态的原理 

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

1、单继承中的虚函数表

2、多继承中的虚函数表 


 

一、多态的原理

1.虚函数表 

首先我们创建一个使用了多态的类,创建一个对象来看其内部的内容:

#include<iostream>
using namespace std;class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}
private:int _b = 1;
};int main()
{Base b;cout << sizeof(Base) << endl;return 0;
}

通过运行在x64下,Base的大小是16btyes,在x86下,Base的大小是8btyes。在通过监视窗口,出了有_b成员,还多了一个_vfptr数组,这个指针数组实际上叫做虚函数表指针数组,严格意义来说,一个含有虚函数的类中至少有一个虚函数表指针数组,这个数组中存放的是虚函数的函数地址,虚函数表也叫做虚表。为什么要这么设计呢?

针对上面的代码我们在进行改造:

1.增加一个继承了基类的派生类

2.派生类中去重写虚函数

3.基类中增加一个虚函数和一个普通函数(派生类不进行重写和不存在这两个函数)

#include<iostream>
using namespace std;class Base
{
public:virtual void Func1(){cout << "Base::Func2()" << 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、虚函数表指针_vfptr创建的:当对象实例化出来后,会调用构造函数,在构造函数的初始化列表中有_vfptr赋值的语句,并且把虚函数表的首地址赋给虚表指针。

2、派生类对象d中也有一个虚表指针,其中是由两个部分构成的,一部分是继承父类成员,另一部分是虚表指针,也就是说是虚函数。

3、基类 b 对象和派生类 d 对象虚表地址是不一样的,在虚表中我们发现,有一个函数指针地址是一样的,有一个是不一样的。虚表地址不一样说明派生类中重写的函数地址发生了改变。基类虚函数 Func1 在派生类中完成了重写,d 的虚表中存的是重写的 Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。所以,派生类对象 d 的虚表中本来该放的是基类虚函数的地址,但是因为派生类重写了基类的虚函数,所以基类虚函数的地址就被覆盖变成了派生类虚函数的地址,本意是调用基类的虚函数,结果却调到了派生类的虚函数,这就实现了多态。

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

5、基类和派生类,无论是否完成了虚函数的重写,都有各自独立的虚表。

6、一个类的所有对象共享同一张虚表。(就像一个类的所有对象共享成员函数一样)

【虚函数表的生成过程】

1、先将基类中的虚表内容拷贝一份到派生类虚表中。

2、如果派生类重写了基类中某个虚函数,用派生类自己重写的虚函数覆盖虚表中基类的虚函数。

3、派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

 虚函数存在哪里?虚表存在哪里?

错误回答:虚函数存在虚表,虚表存在对象中。

正确回答:虚表中存的是虚函数的指针,并不是虚函数,虚函数和普通的函数是一样的,都是存放在代码段中,只是将它的指针放到了虚表中,而在对象中存放的是虚表的指针,也不是虚表,虚表在vs下也是存放在代码段的位置中。

VS下进行验证 

class Base
{
public:virtual void func1(){cout << "Base::func1" << endl;}
private:int a;
};
int Test()
{return 0;
}int main()
{Base b;int a1 = 0; // 栈帧int* p1 = new int; // 堆区const char* p2 = "hello"; // 常量区auto pf = Test(); // 函数地址static int a2 = 1; // 静态区printf("栈帧        :0x%p\n", &a1);printf("堆区        :0x%p\n", p1);printf("常量区      :0x%p\n", p2);printf("函数地址    :0x%p\n", pf);printf("静态区      :0x%p\n", &a2);printf("虚函数表地址:0x%p\n", *((int*)&b));return 0;
}

 

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、这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数

为什么? 

(1)基类对象的指针 / 引用调用虚函数的原理是什么? 

不管基类指针 / 引用指向的是基类还是派生类,执行这段代码 p.BuyTicket() 的指令是一模一样的,先找到虚表指针,通过虚表指针找到虚表,取对应虚函数的地址并调用该虚函数。

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

p中存的是Mike对象的指针,将p移动到eax中
001940DE  mov         eax,dword ptr [p]
[eax]就是取eax值指向的内容,这里相当于把Mike对象头4个字节(虚表指针)移动到了edx
001940E1  mov         edx,dword ptr [eax]
[edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE  mov         eax,dword ptr [edx]
call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
001940EA  call           eax   22  |  001940EC  cmp         esi,esp   

可以看出,足多态以后的函数调用,不是在编译时确定的,而是运行起来以后到对象的中去找的。不满足多态的函数调用是编译时确认好的。 

(2)为什么多态必须要用基类的指针 / 引用来调用虚函数,而用基类对象调用却不行? 

派生类对象赋值给基类对象,不会拷贝派生类的虚表指针,只会拷贝对象中的数据成员过去。不妨这样来理解:一个类的所有对象共享同一张虚表,就像一个类的所有对象共享成员函数一样,只能供这个类自己的对象使用,所以派生类对象是不可能把虚表拷贝过去的,不然就违背同一个类共享的规则了。那么既然不会把派生类的虚表指针拷贝过去,那基类对象自然就不能调用到派生类的虚函数了。

 由上图,我们可以看到,Johnson赋值给Amy,但是Amy的虚表并没有变成派生类Johnson的虚表。

下面则是上面继承关系中的 Person 类对象 Mike 和 Student 类对象 Johnson 模型:解释了用基类引用 / 指针引用不同对象去完成同一行为时,如何展现出不同的形态。

(3)动态绑定与静态绑定

1、静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为编译时多态性和静态多态,比如:函数重载、内联函数、函数模板。
2、动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为运行时多态性和动态多态,比如:虚函数。
3、前面买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。

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

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 的虚表呢?下面我们使用代码打印出虚表中的函数

// 函数指针VFPTR
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]);// 把虚表各元素由void*强转为函数指针类型后,赋值给函数指针fVFPTR f = vTable[i];// 调用函数f();}cout << endl;
}int main()
{Base b;Derive d;/*思路:取出b、d对象的头4字节,就是虚表的指针,前面我们说到虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr1、先取b的地址,强转成一个int*的指针2、再解引用取值,就取到了b对象头4字节的值,这个值就是指向虚表的指针3、再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。4、虚表指针传递给PrintVTable进行打印虚表5、需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。*/VFPTR* vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb); // 打印对象b的虚表VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled); // 打印对象d的虚表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;
};// 函数指针VFPTR
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]);// 把虚表各元素赋值给函数指针fVFPTR f = vTable[i];// 调用函数f();}cout << endl;
}int main()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1); // 打印第一张虚表// 必须先强转成char*,然后加Base1大小个字节,再强转成int*,解引用,强转成VFPTR*VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));PrintVTable(vTableb2); // 打印第二张虚表return 0;
}

1、Base1 和 Base2 中都有虚函数 func1,那么 Derive 类中的 func1 到底是重写的哪一个基类的呢? 

答:两个基类 Base1 和 Base2 中的虚函数 func1 都会被重写,因为要满足多态条件。

2、多继承体系,Derive 继承了两个基类,那么 Derive 对象中有几张虚表呢?
答:Derive 对象中有两张虚表。

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

这里 Derive 对象的两张虚表中的重写的 Derive::func1 函数,虽然函数地址不一样,但是当 Base1 或 Base2 指针指向 Derive对象时,调的都是 Derive 中的 func1,是同一个函数。这其中的具体原因和编译器的设计有关。  

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

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

相关文章

Local Changes不展示,DevEco Studio的git窗口中没有Local Changes

DevEco Studio的git窗口中&#xff0c;没有Local Changes&#xff0c;怎么设置可以调出&#xff1f; 进入File-->Settings-->Version Control&#xff0c;将Use non-modal commit interface前的勾选框取消勾选&#xff0c;点击OK即可在打开git窗口&#xff0c;就可以看到…

情绪识别项目

文章目录 1、mp4s文件转mp3文件2、Audition下载3、Audition安装4、Audition使用&#xff1a; 1、mp4s文件转mp3文件 在线转&#xff1a;Convert audio to MP3&#xff08;https://audio.online-convert.com/convert-to-mp3&#xff09; 2、Audition下载 Audition CC2019/64位…

Windows Qtcreator不能debug 调试 qt5 程序

Windows下 Qt Creator 14.0.2 与Qt5.15.2 正常release打包都是没有问题的&#xff0c;就是不能debug&#xff0c;最后发现是两者不兼容导致的&#xff1b; 我使用的是 编译器是 MinGW8.1.0 &#xff0c;这个版本是有问题的&#xff0c;需要更新到最新&#xff0c;我更新的是Mi…

解决整合Django与Jinja2兼容性的问题

提问 解决整合Django与Jinja2时遇到了一些兼容性问题。已经按照常规步骤在我的settings.py中配置了Jinja2作为模板引擎&#xff0c;同时保留了Django默认的模板设置。然而尝试同时使用Django和Jinja2时&#xff0c;系统报错提示我没有指定模板。如果我尝试移除Django的默认模板…

如何搭建C++环境--1.下载安装并调试Microsoft Visual Studio Previerw(Windows)

1.首先&#xff0c;打开浏览器 首先&#xff0c;搜索“Microsoft Visual Studio Previerw” 安装 1.运行VisualStudioSetup (1).exe 无脑一直点继续 然后就到 选择需要的语言 我一般python用pycharm Java&#xff0c;HTML用vscode&#xff08;Microsoft Visual Studio cod…

【Kubernetes 指南】基础入门——Kubernetes 简介(一)

目录 一、Kubernetes 简单介绍 二、Kubernetes 是一个平台 三、Kubernetes 不是什么&#xff1f; 一、Kubernetes 简单介绍 Kubernetes 是谷歌开源的容器集群管理系统&#xff0c;是 Google 多年大规模容器管理技术 Borg 的开源版本&#xff0c;主要功能包括&#xff1a; ①…

Java线程的使用

Java中的线程是用来实现多任务并发执行的机制。在Java中&#xff0c;主要有两种方式来创建和使用线程&#xff1a;实现Runnable接口和继承Thread类。 实现Runnable接口&#xff1a; 创建一个类&#xff0c;实现Runnable接口&#xff0c;并重写run()方法。在run()方法中定义线程…

go clean -modcache命令清理缓存

go clean -modcache命令用于清理Go模块的本地缓存。Go模块缓存位于$GOPATH/pkg/mod/cache目录下&#xff0c;存储了所有下载和使用的模块版本。当执行go clean -modcache时&#xff0c;这个命令会删除该目录下的所有内容&#xff0c;迫使Go在下次构建时重新下载所有依赖的模块。…

Python 中的 Lxml 库与 XPath 用法

Python 中的 Lxml 库与 XPath 用法 Python 中的 Lxml 库与 XPath 用法Lxml安装 Lxml基础用法加载文档解析与查询创建新的 XML/HTML 高级特性1. 复杂的 XPath 查询2. DTD 和 Schema 验证3. XSLT 变换4. 自定义命名空间5. 异常处理6. 大文件流式处理7. 并发和线程安全性8. 性能优…

共享售卖机语音芯片方案选型:WTN6020引领智能化交互新风尚

在共享经济蓬勃发展的今天&#xff0c;共享售卖机作为便捷购物的新形式&#xff0c;正逐步渗透到人们生活的各个角落。为了提升用户体验&#xff0c;增强设备的智能化和互动性&#xff0c;增加共享售卖机的语音功能就显得尤为重要。 共享售卖机语音方案选型&#xff1a; WTN602…

关闭AWS账号后,服务是否仍会继续运行?

在使用亚马逊网络服务&#xff08;AWS&#xff09;时&#xff0c;用户有时可能会考虑关闭自己的AWS账户。这可能是因为项目结束、费用过高&#xff0c;或是转向使用其他云服务平台。然而&#xff0c;许多人对关闭账户后的服务状态感到困惑&#xff0c;我们九河云和大家一起探讨…

新版本PasteSpider开发中专用部署工具介绍(让2GB的服务器也能使用CI/CD,简化你的部署过程)

如果你有linux服务器&#xff0c;可以试试这个PasteSpider&#xff0c;利用容器管理软件(docker/podman)&#xff0c;可以快速上手&#xff01; 拉取镜像并安装 【【【PasteSpider的下载和安装(支持docker的一键模式)】】】 建议使用 最简单的模式SqliteMemoryCache 测试嘛…

【小白学机器学习37】用numpy计算协方差cov(x,y) 和 皮尔逊相关系数 r(x,y)

目录 1 关于1个数组np.array&#xff08;1组数据&#xff09;如何求各种统计数据 2 关于2个数组np.array&#xff08;2组数据&#xff09;如何求数组的相关关系&#xff1f; 2.1 协方差公式和方差公式 2.2 协方差 公式 的相关说明 2.3 用np.cov(x,y,ddof0) 直接求协方差矩…

C++ 11重点总结1

智能指针 智能指针: C11引入了四种智能指针: auto_ptr(已弃用)、unique_ptr、shared_ptr和weak_ptr。智能指针可以更有效地管理堆内存,并避免常见的内存泄漏问题。 shared_ptr: 自定义删除器。 shared_ptr使用引用计数来管理它指向的对象的生命周期。多个shared_ptr实例可以指向…

2024年nvm保姆级安装教程

需求&#xff1a;当前我的nodejs的版本是6.14.10&#xff0c;想切换为更高的版本。故使用nvm工具来实现不同node版本之间的切换 目录 一、删除node二、nvm安装三、配置nvm镜像四、安装所需要的nodejs版本nvm常用命令 一、删除node 第一步&#xff1a;首先在控制面板删除node.j…

Java部分新特性

模式匹配 instance of 模式匹配 之前写法 public void print(Object o) {if (o instanceof String){String str (String) obj;System.out.println("This is a String of length " s.length());} else {System.out.println("This is not a String");} …

Flink--API 之 Source 使用解析

目录 一、Flink Data Sources 分类概览 &#xff08;一&#xff09;预定义 Source &#xff08;二&#xff09;自定义 Source 二、代码实战演示 &#xff08;一&#xff09;预定义 Source 示例 基于本地集合 基于本地文件 基于网络套接字&#xff08;socketTextStream&…

【三维生成】Edify 3D:可扩展的高质量的3D资产生成(英伟达)

标题&#xff1a;Edify 3D: Scalable High-Quality 3D Asset Generation 项目&#xff1a;https://research.nvidia.com/labs/dir/edify-3d demo&#xff1a;https://build.nvidia.com/Shutterstock/edify-3d 文章目录 摘要一、前言二、多视图扩散模型2.1.消融研究 三、重建模型…

Android Framework禁止弹出当前VOLTE不可用的提示窗口

文章目录 VoLTE简介VoLTE 的优势 当前VOLTE不可用的弹窗弹窗代码定位屏蔽弹出窗口 VoLTE简介 VoLTE&#xff08;Voice over LTE&#xff09;是一种基于4G LTE网络的语音通话技术。它允许用户在4G网络上进行高质量的语音通话和视频通话&#xff0c;而不需要回落到2G或3G网络。V…

微信小程序生命周期详解

一、全局生命周期 微信小程序的全局生命周期指的是小程序从启动到销毁期间经历的一系列阶段和事件。全局生命周期主要在 App() 构造器中定义&#xff0c;包括以下几个阶段&#xff1a; onLaunch&#xff1a;小程序初始化完成时触发&#xff0c;全局只触发一次。onShow&#x…