C++修炼之路之多态---多态的原理(虚函数表)

目录

一:多态的原理 

1.虚函数表

 2.原理分析

3.对于虚表存在哪里的探讨

4.对于是不是所有的虚函数都要存进虚函数表的探讨

二:多继承中的虚函数表

三:常见的问答题 

接下来的日子会顺顺利利,万事胜意,生活明朗-----------林辞忧 

接上篇的多态的介绍后,接下来介绍多态的原理以及虚函数表的相关知识

一:多态的原理 

1.虚函数表

这里从一道经典笔试题引入

对于这道题我们可能想到的是计算类 大小的对齐规则,结果为4,但结果为8,这是因为有虚函数的类要多考虑一指针

在32位系统下是8

如果这里再添加几个虚函数呢?

 

所以在这里不管类里面有多少个虚函数 ,只要是包含虚函数的类计算大小都要考虑添加一指针,再考虑对齐

但这里的一指针是什么呢?

但这里我们就看到在b1中除了_b还存有 一个_vfptr的指针在对象的前面,这个指针就叫做虚函数表指针,其中v代表virtual,f代表funcation

每一个含有虚函数的类都至少有一个虚函数表指针,他的类型为函数指针数组,而虚函数的地址是存放在虚函数表中的,虚函数表也叫虚表

 2.原理分析

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func2()" << endl;}private:int _b = 1;
};
class Derived : public Base
{virtual void Func1(){cout << "Func()" << endl;}
private:int _a = 0;
};int main()
{Base b1;Derived d1;return 0;
}

 

解释多态调用的两个条件

对于条件一:必须是父类的指针或引用来调用函数

1.父类的指针指向父类对象时,依据虚函数表指针(vfptr),在虚函数表中找到函数的地址,再call这个地址来执行接下来的操作

2.父类的指针指向子类对象时,先完成切片,找到父类的那一部分,依据虚函数表指针(vfptr),在虚函数表中找到函数的地址,再call这个地址来执行接下来的操作

3.由于经过虚函数的重写后,虚函数的地址是不相同的,所以结果是不相同的,这是就形成了多态

对于编译器来说上面的两个调用是执行的同样的操作,都只是取对象的头四个字节,就是虚函数表指针,然后去虚表中找到对应调用函数的地址,然后执行接下来的操作

4.如果是父类的对象调用函数的话这时就要分析可能会总成的结果

这时尤其是这样的场景,Person* ptr=new Person,Student s;   *ptr=s ,这样如果支持能拷贝虚函数表指针的话,这时delete  ptr,就调用的是 Student类的析构函数,导致直接错误的

5.对于多态调用是在运行时,去虚表里面找到函数指针,确定函数指针后,调用函数;

对于普通调用是在编译链接时,确定函数地址

6.派生类中只有一个虚表指针(菱形继承除外),同一个类的对象共用一张虚表

7.虚函数也是也是和成员函数一样存在代码段的,不同的是虚函数会将自己的地址存在虚表中

对于条件二:虚函数的重写

从上面就可以看出虚函数的重写也叫覆盖,覆盖了原先虚函数的地址,重写是语法层的叫法,而覆盖是原理层的叫法

三:派生类的虚表生成

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

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

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

3.对于虚表存在哪里的探讨

对于栈和堆是不可能的,只有代码段或者静态区,但我们可以自己验证是存在哪里的

验证代码

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
void func()
{cout << "void func()" << endl;
}
int main()
{Base b1;Base b2;static int a = 0;int b = 0;int* p1 = new int;const char* p2 = "hello world";printf("静态区:%p\n", &a);printf("栈:%p\n", &b);printf("堆:%p\n", p1);printf("代码段:%p\n", p2);printf("虚表:%p\n", *((int*)&b1));printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", func);return 0;
}

对于这里的取虚表地址

 

可以这样来理解,&b1是整个类的地址,然后强转为(int*),再解引用取得就是头四个字节,即虚表地址 

 

我们发现 和虚表地址最接近的为代码段的地址,所以可以确定虚表是存在代码段的

4.对于是不是所有的虚函数都要存进虚函数表的探讨

首先确定答案 一定都是存在虚函数表的

接下来我们在vs上监视窗口来查看

分析代码

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; }void func5() { cout << "Derive::func5" << endl; }
private:int b;
};class X :public Derive {
public:virtual void func3() { cout << "X::func3" << endl; }
};int main()
{Base b;Derive d;X x;Derive* p = &d;p->func3();p = &x;p->func3();return 0;
}

  

对于这里监视窗口的显示,在这里对于b是只有两个虚函数都存进了虚函数表中,但对于d和x都应该是四个虚函数存进虚函数表的,但在这里都只存了两个虚函数,但验证多态调用的话,结果为

结果是多态调用, 这时我们就不得不质疑此时监视窗口 的结果了

为了进一步的证明。我们可以调用内存窗口来查看

在内存中我们就会发现后两个地址与前两个虚函数的地址很接近,所以我们暂时可以认为虚函数是都存在虚函数表中的,

为了确定结果,我们可以使用打印虚表来验证猜想

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; }void func5() { cout << "Derive::func5" << endl; }
private:int b;
};class X :public Derive {
public:virtual void func3() { cout << "X::func3" << endl; }
};typedef void (*VFUNC)();
//void PrintVFT(VFUNC a[])
void PrintVFT(VFUNC* a)
{for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];f();//(*f)();}printf("\n");
}int main()
{Base b;PrintVFT((VFUNC*)(*((long long*)&b)));//32位的话,可以采用intDerive d;X x;// PrintVFT((VFUNC*)&d);PrintVFT((VFUNC*)(*((long long*)&d)));PrintVFT((VFUNC*)(*((long long*)&x)));return 0;
}

 

这样看,只要是虚函数,都会将地址存到类的虚函数表里面的

 

二:多继承中的虚函数表

同样的我们可以采用例子来介绍

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;
};int main()
{Derive d;Base1* p1 = &d;p1->func1();Base2* p2 = &d;p2->func1();return 0;
}

采用监视窗口的话 

就会发现对于基类的两张虚表中都没有存derived类的fun3() ,但我们可以使用多态的调用来验证下

所以的话,fun3是一定存在基类的两张 虚表中的其中一个里面,这样采用内存来看

所以最好的方式,我们还是来打印两个基类的虚函数表的 

typedef void (*VFUNC)();
//void PrintVFT(VFUNC a[])
void PrintVFT(VFUNC* a)
{for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];f();//(*f)();}printf("\n");
}
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;
};int main()
{Derive d;PrintVFT((VFUNC*)(*(int*)&d));//PrintVFT((VFUNC*)(*(int*)((char*)&d+sizeof(Base1))));Base2* ptr = &d;PrintVFT((VFUNC*)(*(int*)ptr));/*Base1* p1 = &d;p1->func1();Base2* p2 = &d;p2->func1();*/return 0;
}

 

所以此时我们就会知道,派生类的虚函数地址是存在第一个基类的虚函数表里面的 

三:常见的问答题 

 

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

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

相关文章

五、【易 AI】鼠标事件与目标焦点

所谓辉煌的人生,不过是欲望的囚徒。 ——叔本华 注:自本节开始,所有的示例都以 OpenGLWidget 实现, 一、鼠标事件 重写鼠标事件, #ifndef MYOPENGL_H #define MYOPENGL_H#include <QOpenGLWidget> #include <QTimer> #include <QMouseEvent>class My…

CUDA编程【2】-(51-78)

系列文章目录 文章目录 系列文章目录前言51、寄存器溢出51.1 溢出概念51.1 使用控制 52、本地内存和共享内存52.1 本地内存52.2. 共享内存 53. 常量内存53.1 概念53.2 初始化 54. 全局内存54.1 概念54.2 初始化 55. GPU缓存和变量作用域55.1 缓存类型55.2 变量作用域 56. 静态全…

鲁抗医药专属采购商城上线,携手隆道公司注入数字化采购新动能

近日&#xff0c;国内领先的医药制造企业——山东鲁抗医药股份有限公司&#xff08;以下简称鲁抗医药&#xff09;与隆道公司联手打造的鲁抗医药专属采购商城上线运行。该商城&#xff0c;通过整合鲁抗医药合作电商和合格供应商资源&#xff0c;创新商城化采供协同模式&#xf…

【Hello算法】 > 第 3 关 >栈与队列

数据结构 之 数组与链表 1 栈 / 栈的常见操作、实现、应用2 队列 /队列的常见操作、实现、应用3 双向队列4 Tips ———————————————————————————————————————————————————————————- ————————————————…

动手学大模型应用开发--Chapter 03搭建并使用向量数据库

文章目录 前言一、学习目标二、学习知识点概要2.1 什么是词向量2.2 词向量的意义2.3 文本转为词向量的方法 三、总结四、引申阅读 前言 本学习笔记为datawhale动手学大模型应用开发的第三章&#xff0c;学习链接为&#xff1a; https://datawhalechina.github.io/llm-universe…

系统启动修复和SYSTEM丢失损坏故障处理

系统启动修复和SYSTEM丢失损坏故障处理 一、问题描述 你的电脑/设备需要修复。无法加载应用程序或操作系统&#xff0c;原因是所需文件丢失或包含错误。 文件:\Windows\system32\winload.exe 错误代码: 0xc000000e 二、问题分析 1.查询winload.exe是win7或者win10以上系统…

算法竞赛相关问题总结记录

前言 日常在校生或者是工作之余的同学或多或少都会参加一些竞赛,参加竞赛一方面可以锻炼自己的理解与实践能力&#xff0c;也能够增加自己的生活费&#xff0c;竞赛中的一些方案也可以后续作为自己论文的base,甚至是横向课题的框架。在算法竞赛中算法的差别个人感觉差距都不大&…

Transformer - 时间特征的处理

Transformer - 时间特征的处理 flyfish ETTm1.csv有如下内容 假如有2016/7/1 0:45:00有这样的时间字符串&#xff0c;如何变成时间特征列表 from typing import Listimport numpy as np import pandas as pd from pandas.tseries import offsets from pandas.tseries.freq…

携程 Java 暑期实习一面:HashMap 的 key 可以设置为 null 吗?那 ConcurrentHashMap 呢?

更多大厂面试内容可见 -> http://11come.cn 携程 Java 暑期实习一面&#xff1a;HashMap 的 key 可以设置为 null 吗&#xff1f;那 ConcurrentHashMap 呢&#xff1f; Java 基础 1、Java 中有哪些常见的数据结构&#xff1f; 图片来源于&#xff1a;JavaGuide Java 中常…

【数学归纳法 反证法】菲蜀定理

裴蜀定理&#xff08;或贝祖定理&#xff0c;Bzout’s identity&#xff09;得名于法国数学家艾蒂安裴蜀&#xff0c;说明了对任何整数a、b和它们的最大公约 数d&#xff0c;关于未知数x和y的线性不定方程&#xff08;称为裴蜀等式&#xff09;&#xff1a;若a,b是整数,且&…

【分治算法】Hanoi塔问题Python实现

文章目录 [toc]问题描述Python实现 个人主页&#xff1a;丷从心 系列专栏&#xff1a;Python基础 学习指南&#xff1a;Python学习指南 问题描述 设 a a a、 b b b、 c c c是三个塔座&#xff0c;开始时&#xff0c;在塔座 a a a上有一叠共 n n n个圆盘&#xff0c;这些圆盘…

LabVIEW连接PostgreSql

一、安装ODBC 下载对应postgreSQL版本的ODBC 下载网址&#xff1a;http://ftp.postgresql.org/pub/odbc/versions/msi/ 下载好后默认安装就行&#xff0c;这样在ODBC数据源中才能找到。 二、配置系统DSN 实现要新建好要用的数据库&#xff0c;这里的用户名&#xff1a;postg…