C++:多态的原理

news2024/11/28 10:09:31

目录

一、多态的原理

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*强转为函数指针类型后,赋值给函数指针f
        VFPTR f = vTable[i];
        // 调用函数
        f();
    }
    cout << endl;
}
 
int main()
{
    Base b;
    Derive d;
 
    /*思路:取出b、d对象的头4字节,就是虚表的指针,
      前面我们说到虚函数表本质是一个存虚函数指针的指针数组,
      这个数组最后面放了一个nullptr
      1、先取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]);
        // 把虚表各元素赋值给函数指针f
        VFPTR 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.coloradmin.cn/o/2248977.html

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

相关文章

Ubuntu 硬盘分区并挂载

一、什么是挂载 1.挂载的定义 在 Ubuntu&#xff08;或其他 Linux 系统&#xff09;中&#xff0c;挂载&#xff08;Mount&#xff09; 是将一个存储设备或分区连接到系统的文件系统层次结构中的过程。挂载后&#xff0c;你可以通过某个目录&#xff08;挂载点&#xff09;访问…

python-docx -- 读取word页眉、页脚

文章目录 sections介绍访问section添加section页眉、页脚综合案例:sections介绍 word支持section的概念,即一个文档的划分部分,不同的部分均包含相同的页面布局设置,如相同的边距、页面方向等;在每个section中可以定义页眉、页脚来应用于该section下的所有页面;大部分wor…

开源加密库mbedtls及其Windows编译库

目录 1 项目简介 2 功能特性 3 性能优势 4 平台兼容性 5 应用场景 6 特点 7 Windows编译 8 编译静态库及其测试示例下载 1 项目简介 Mbed TLS是一个由ARM Maintained的开源项目&#xff0c;它提供了一个轻量级的加密库&#xff0c;适用于嵌入式系统和物联网设备。这个项…

《生成式 AI》课程 第7講:大型語言模型修練史 — 第二階段: 名師指點,發揮潛力 (兼談對 ChatGPT 做逆向工程與 LLaMA 時代的開始)

资料来自李宏毅老师《生成式 AI》课程&#xff0c;如有侵权请通知下线 Introduction to Generative AI 2024 Springhttps://speech.ee.ntu.edu.tw/~hylee/genai/2024-spring.php 摘要 这一系列的作业是为 2024 年春季的《生成式 AI》课程设计的&#xff0c;共包含十个作业。…

公司金融期末考试题目

公司金融期末考试题 选择题 1.现金折扣和信用条件&#xff08;教材P253&#xff09; 题目类似&#xff1a; 下列不属于信用条件的是&#xff08;&#xff09;。 现金折扣 数量折扣信用期限 折扣期限 给定的信用条件为"1/10&#xff0c;n/40"&#xff0c;则其含义…

【前端】JavaScript中的字面量概念与应用详解

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: 前端 文章目录 &#x1f4af;前言&#x1f4af;字面量1. 数字字面量2. 字符串字面量3. 布尔字面量4. 空值字面量&#xff08;null&#xff09;5. 对象字面量6. 数组字面量7. 正则表达式字面量8. 特殊值字面量9. 函数字…

Kotlin DSL Gradle 指南

本文是关于 Kotlin DSL Gradle 的指南&#xff08;上篇&#xff09;&#xff0c;介绍了 Gradle 作为 Android 开发构建工具的作用及优势&#xff0c;包括初始配置、生命周期、依赖管理、Task 相关内容。如 Task 的创建、自定义、各种方法和属性&#xff0c;以及文件操作等&…

Web开发:使用stackexchange.redis库对redis进行增删改查

一、安装第三方库 二、官网 StackExchange.Redis |通用型 redis 客户端 三、连接示例 private static string redisConnectionString "localhost:6379,passwordyourpassword,defaultDatabase0,allowAdmintrue,asyncTimeout10000";private static string redisConn…

2024年第15届蓝桥杯C/C++组蓝桥杯JAVA实现

目录 第一题握手&#xff0c;这个直接从49累加到7即可&#xff0c;没啥难度&#xff0c;后面7个不握手就好了&#xff0c;没啥讲的&#xff0c;(然后第二个题填空好难&#xff0c;嘻嘻不会&#xff09; 第三题.好数​编辑 第四题0R格式 宝石组合 数字接龙 最后一题:拔河 第…

Django基础之路由

一.前言 前面我们说了django的安装于基础配置&#xff0c;基础知识点我就细分下来&#xff0c;每天和大家讲一点&#xff0c;今天就要和大家说django的基础知识点了&#xff0c;我们今天先来讲路由&#xff0c;内容不多&#xff0c;希望大家记住 二.传统路由 路由就是前面一个…

gitlab ssh-key 绑定

windows环境下配置 gitlab的ssh key&#xff1a; 1.打开本地git bash,使用如下命令生成ssh公钥和私钥对: ssh-keygen -t rsa -C xxxxxx.com 2.一直回车&#xff1b; 3.然后打开公钥文件&#xff1a;C:/Users/Administrator/.ssh/id_rsa.pub文件&#xff0c;复制其中的内容; 4…

26.100ASK_T113-PRO 测试摄像头 输出信息

1.测试代码 读到摄象头参数 输出 video_test.c #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <linux/type…

【人工智能】深入解析GPT、BERT与Transformer模型|从原理到应用的完整教程

在当今人工智能迅猛发展的时代&#xff0c;自然语言处理&#xff08;NLP&#xff09;领域涌现出许多强大的模型&#xff0c;其中GPT、BERT与Transformer无疑是最受关注的三大巨头。这些模型不仅在学术界引起了广泛讨论&#xff0c;也在工业界得到了广泛应用。那么&#xff0c;G…

【Flink-scala】DataStream编程模型之 窗口的划分-时间概念-窗口计算程序

DataStream编程模型之 窗口的划分-时间概念-窗口计算程序 1. 窗口的划分 1.1 窗口分为&#xff1a;基于时间的窗口 和 基于数量的窗口 基于时间的窗口&#xff1a;基于起始时间戳 和终止时间戳来决定窗口的大小 基于数量的窗口&#xff1a;根据固定的数量定义窗口 的大小 这…

虚拟地址空间与物理内存(Linux系统)

个人主页&#xff1a;敲上瘾-CSDN博客 个人专栏&#xff1a;Linux学习、游戏、数据结构、c语言基础、c学习、算法 目录 问题引入 一、什么是虚拟内存 二、虚拟内存的描述与组织 三、页表的优势 四、虚拟内存区域划分 问题引入 为引入今天的话题&#xff0c;我们先来看下面…

docker-compose搭建xxl-job、mysql

docker-compose搭建xxl-job、mysql 1、搭建docker以及docker-compose2、下载xxl-job需要数据库脚本3、创建文件夹以及docker-compose文件4、坑来了5、正确配置6、验证-运行成功 1、搭建docker以及docker-compose 略 2、下载xxl-job需要数据库脚本 下载地址&#xff1a;https…

【ArcGIS Pro实操第11期】经纬度数据转化成平面坐标数据

经纬度数据转化成平面坐标数据 数据准备ArcGIS操作步骤-投影转换为 Sinusoidal1 投影2 计算几何Python 示例 另&#xff1a;Sinusoidal (World) 和 Sinusoidal (Sphere) 的主要区别参考 数据准备 数据投影&#xff1a; 目标投影&#xff1a;与MODIS数据相同&#xff08;Sinu…

【模型学习之路】PyG的使用+基于点的任务

这一篇是关于PyG的基本使用 目录 前言 PyG的数据结构 演示 图的可视化 基于点的任务 任务分析 MLP GCN 前言 对图结构感兴趣的朋友可以学一下常用的有关图结构的库&#xff1a;networkx详细介绍 networkx 库&#xff0c;探讨它的基本功能、如何创建图、操作图以及其常…

如何监控Elasticsearch集群状态?

大家好&#xff0c;我是锋哥。今天分享关于【如何监控Elasticsearch集群状态&#xff1f;】面试题。希望对大家有帮助&#xff1b; 如何监控Elasticsearch集群状态&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 监控 Elasticsearch 集群的状态对于确保…

Edify 3D: Scalable High-Quality 3D Asset Generation

Deep Imagination Research | NVIDIA 目录 一、Abstract 二、核心内容 1、多视图扩散模型 3、重建模型&#xff1a; 4、数据处理模块&#xff1a; 三、结果 1、文本到 3D 生成结果 2、图像到 3D 生成结果 3、四边形网格拓扑结构 一、Abstract NVIDIA 开发的用于高质量…