C++对象布局及多态实现之带虚函数的类

面试系列之C++的对象布局【建议收藏】 我们都知道C++多态是通过虚函数表来实现的,那具体是什么样的大家清楚吗?开篇依旧提出来几个问题: 普通类对象是什么布局? 带虚函数的类对象是什么布局? 单继承下不含有覆盖函数的类对象是什么布局? 单继承下含有覆盖函数的类对象是什么布局? 多继承下不含有覆盖函数的类对象是什么布局? 多继承下含有覆盖函数的类对象的是什么布局? 多继承中不同的继承顺序产生的类对象布局相同吗? 虚继承的类对象是什么布局? 菱形继承下类对象是什么布局? 为什么要引入虚继承? 为什么虚函数表 阅读详情
  如果类中存在虚函数时,情况会怎样呢?我们知道当一个类中有虚函数时,编译器会为该类产生一个虚函数表,并在它的每一个对象中插入一个指向该虚函数表的指针,通常这个指针是插在对象的起始位置。所谓的虚函数表实际就是一个指针数组,其中的指针指向真正的函数起始地址。我们来验证一下,定义一个无成员变量的类C040,内含一个虚函数。

struct C040
{
 virtual void foo() {}
};

  运行如下代码打印它的大小及对象中的内容。

PRINT_SIZE_DETAIL(C040)

  结果为:

The size of C040 is 4
The detail of C040 is 40 b4 45 00

  果然它的大小为4字节,即含有一个指针,指针指向的地址为0x0045b440。

  同样再定义一个空类C050,派生自类C040。

struct C050 : C040
{};

  由于虚函数会被继承,且维持为虚函数。那么类C050的对象中同样应该含有一个指向C050的虚函数表的指针。

  运行如下代码打印它的大小及对象中的内容。

PRINT_SIZE_DETAIL(C050)

  结果为:

The size of C050 is 4
The detail of C050 is 44 b4 45 00

  果然它的大小也为4字节,即含有一个指向虚函数表(后称虚表)的指针(后称虚表指针)。

  虚表是类级别的,类的所有对象共享同一个虚表。我们可以生成类C040的两个对象,然后通过观察对象的地址、虚表指针地址、虚表地址、及虚表中的条目的值(即所指向的函数地址)来进行验证。

  运行如下代码:

C040 obj1, obj2;
PRINT_VTABLE_ITEM(obj1, 0, 0)
PRINT_VTABLE_ITEM(obj2, 0, 0)

  结果如下:

obj1 : objadr:0012FDC4 vpadr:0012FDC4 vtadr:0045B440 vtival(0):0041D834
obj2 : objadr:0012FDB8 vpadr:0012FDB8 vtadr:0045B440 vtival(0):0041D834


  (注:第一列为对象名,第二列(objadr)为对象的内存地址,第三列(vpadr)为虚表指针地址,第四列(vtadr)为虚表的地址,第五列(vtival(n))为虚表中的条目的值,n为条目的索引,从0开始。后同)

  果然对象地址不同,虚表指针(vpadr)位于对象的起始位置,所以它的地址和对象相同。两个对象的虚表指针指向的是同一个虚表,因此(vtadr)的值相同,虚表中的第一条目(vtival(0))的值当然也一样。

  接下来,我们再观察类C040和从它派生的类C050的对象,这两个类各有自己的虚表,但由于C050没有重写继承自C040的虚函数,所以它们的虚表中的条目的值,即指向的虚函数的地址应该是一样的。

  运行如下代码:

C040 c040;
C050 c050;
PRINT_VTABLE_ITEM(c040, 0, 0)
PRINT_VTABLE_ITEM(c050, 0, 0)

  结果为:

c040 : objadr:0012FD4C vpadr:0012FD4C vtadr:0045B448 vtival(0):0041D834
c050 : objadr:0012FD40 vpadr:0012FD40 vtadr:0045B44C vtival(0):0041D834

  果然这次我们可以看到虽然前几列皆不相同,但最后一列的值相同。即它们共享同一个虚函数。

  定义一个C043类,包含两个虚函数。再定义一个C071类,从C043派生,并重写继承的第一个虚函数。

struct C043
{
 virtual void foo1() {}
 virtual void foo2() {}
};
struct C071 : C043
{
 virtual void foo1() {}
};

  我们可以预料到,C043和C071各有一个包含两个条目的虚表,由于C071派生自C043,并且重写了第一个虚函数。那么这两个类的虚表的第一个条目值是不同的,而第二项应该是相同的。运行如下代码。

C043 c043;
C071 c071;
PRINT_SIZE_DETAIL(C071)
PRINT_VTABLE_ITEM(c043, 0, 0)
PRINT_VTABLE_ITEM(c071, 0, 0)
PRINT_VTABLE_ITEM(c043, 0, 1)
PRINT_VTABLE_ITEM(c071, 0, 1)

  结果为:

The size of C071 is 4
The detail of C071 is 5c b4 45 00
c043 : objadr:0012FCD4 vpadr:0012FCD4 vtadr:0045B450 vtival(0):0041D4F1
c071 : objadr:0012FCC8 vpadr:0012FCC8 vtadr:0045B45C vtival(0):0041D811
c043 : objadr:0012FCD4 vpadr:0012FCD4 vtadr:0045B450 vtival(1):0041DFE1
c071 : objadr:0012FCC8 vpadr:0012FCC8 vtadr:0045B45C vtival(1):0041DFE1

  观察第1、2行的最后一列,即两个类的虚表的第一个条目,由于C071重写了foo1函数,所以这个值不一样。而第3、4行的最后一列为两个类的虚表的第二个条目,由于C071并没有重写它,所以这两个值是相同的。和我们之间的猜测是一致的。

  接下来我们看看多重继承。定义两个类,各含一个虚函数,及一个数据成员。再从这两个类派生一个空子类。

struct C041
{
 C041() : c_(0x01) {}
 virtual void foo() { c_ = 0x02; }
 char c_;
};
struct C042
{
 C042() : c_(0x02) {}
 virtual void foo2() {}
 char c_;
};
struct C051 : public C041, public C042
{};

  运行如下代码:

PRINT_SIZE_DETAIL(C041)
PRINT_SIZE_DETAIL(C042)
PRINT_SIZE_DETAIL(C051)

  结果为:

The size of C041 is 5
The detail of C041 is 64 b3 45 00 01
The size of C042 is 5
The detail of C042 is 68 b3 45 00 02
The size of C051 is 10
The detail of C051 is 6c b4 45 00 01 68 b4 45 00 02

  注意,首先我们观察C051的对象输出,发现它的大小为10字节,这说明它有两个虚表指针,从导出的内存数据我们可以推断,首先是一个虚表指针,然后是从C041继承的成员变量,值也是我们在C041的构造函数中赋的值0x01,然后又是一个虚表指针,再是从C042继承的成员变量,值为0x02。

  为了验证,我们再运行如下代码:

C041 c041;
C042 c042;
C051 c051;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_VTABLE_ITEM(c042, 0, 0)
PRINT_VTABLE_ITEM(c051, 0, 0)
PRINT_VTABLE_ITEM(c051, 5, 0)

  注意最后一行的第二个参数,5。它是从对象起始地址开始到虚表指针的偏移值(按字节计算),从上面的对象内存输出我们看到C041的大小为5字节,因此C051中第二个虚表指针的起始位置距对象地址的偏移为5字节。输出的结果为:

  (注:这个偏移值是通过观察而判断出来的,并不通用,而且它依赖于我们前面所说的编译器在生成代码时所用的结构成员对齐方式,我们将这个值设为1。如果设为其他值会影响对象的大小及这个偏移值。参见第一篇起始处的说明。下同。)

c041 : objadr:0012FB88 vpadr:0012FB88 vtadr:0045B364 vtival(0):0041DF1E
c042 : objadr:0012FB78 vpadr:0012FB78 vtadr:0045B368 vtival(0):0041D43D
c051 : objadr:0012FB64 vpadr:0012FB64 vtadr:0045B46C vtival(0):0041DF1E
c051 : objadr:0012FB64 vpadr:0012FB69 vtadr:0045B468 vtival(0):0041D43D

  这下我们可以看到C051的两个虚表指针指向两个不现的虚表(第3、4行的vtadr列),而虚表中的条目的值分别等于C041和C042(即它的两个父类)的虚表条目的值(第1、3行和2、4行的vtival列的值相同)。

  为什么子类要有两个虚表,而不是将它们合并为一个。主要是在处理类型的动态转换时这种对象布局更方便调整指针,后面我们看到这样的例子。

  如果子类重写父类的虚函数会怎么样?前面的类C071我们已经看到过一次了。我们再定义一个从C041和C042派生的类C082,并重写这两个父类中的虚函数,同时再增加一个虚函数。

struct C041
{
 C041() : c_(0x01) {}
 virtual void foo() { c_ = 0x02; }
 char c_;
};
struct C042
{
 C042() : c_(0x02) {}
 virtual void foo2() {}
 char c_;
};
struct C082 : public C041, public C042
{
 C082() : c_(0x03) {}
 virtual void foo() {}
 virtual void foo2() {}
 virtual void foo3() {}
 char c_;
};

  运行和上面类似的代码:

PRINT_SIZE_DETAIL(C082)
C041 c041;
C042 c042;
C082 c082;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_VTABLE_ITEM(c042, 0, 0)
PRINT_VTABLE_ITEM(c082, 0, 0)
PRINT_VTABLE_ITEM(c082, 5, 0)

  结果为:

The size of C082 is 11
The detail of C082 is 70 b3 45 00 01 6c b3 45 00 02 03
c041 : objadr:0012FA74 vpadr:0012FA74 vtadr:0045B364 vtival(0):0041DF1E
c042 : objadr:0012FA64 vpadr:0012FA64 vtadr:0045B368 vtival(0):0041D43D
c082 : objadr:0012FA50 vpadr:0012FA50 vtadr:0045B370 vtival(0):0041D87A
c082 : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(0):0041D483

  果然C082的两个虚表中的条目值都和父类的不一样了(vtival列),指向了重写后的新函数地址。观察C082的大小和对象内存,我们可以知道它并没有为新定义的虚函数foo3生成新的虚表。那么foo3的函数地址到底是加到了类的第一个虚表,还是第二个虚表中?在调试状态下,我们在“局部变量”窗口中展开c082对象。我们可以看到两个虚表及其中的条目,但两个虚表都只能看到第一个条目。这应该是VC7.1IDE的一个小BUG。看来我们只有另想办法来验证。我们先把两个虚表中的第二个条目位置上的值打印出来。运行如下代码。

PRINT_VTABLE_ITEM(c082, 0, 1)
PRINT_VTABLE_ITEM(c082, 5, 1)

  结果如下:

c082 : objadr:0012FA50 vpadr:0012FA50 vtadr:0045B370 vtival(1):0041D32F
c082 : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(1):0041D87A

  然后我们调用一下foo3函数:

c082.foo3();

  查看它的汇编代码:

004225F3 lea ecx,[ebp+FFFFFB74h]
004225F9 call 0041D32F

  第2条call指令后的地址就是foo3的函数地址了(实际上是一个跳转指令),对照前面的输出我们就可以知道,子类新定义的虚函数对应的虚表条目加入到了子类的第一个虚表中,并位于继承自父类的虚表条目之后。
C++多态与虚函数 阅读详情

相关推荐

从内存角度剖析虚函数实现多态的原理

刚开始学习C++虚函数的时候,只知道虚函数就是用来构成动态多态的,以及如何使用虚函数。但是我发现当碰到稍微复杂的多态问题的时候,就很难以理解其实现原理,导致使用出错,比如:一个派生类继承基类,生成对象有两种方式:调用基类生成对象、调用派生类生成对象,调用完毕后释放对象,哪种方式必须要用虚函数实现基类和派生类的析构函数,如果不用会出现什么问题?(这个问题后面我会单独写一个博客再解释)。而《C++ Primer》中更多的是对虚函数使用的讲解,查阅有关虚函数的博客也仅仅是在结论层次对虚函数、纯虚函数进行分析和比较

weixin_49199646的博客 1198

干货——C++虚函数的使用方法

虚函数的作用 虚函数的使用方法 在什么情况下应该声明虚函数 使用虚函数时,有两点要注意: 只有类的成员函数才能被声明虚函数,即虚函数只用于类的继承层次结构中 一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个该虚函数同名、同类型和同参数同名非虚函数。 使用虚函数,系统要有一定的额外空间开销。 当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function ta...

qq_45993891的博客 567

C++ 对象内存布局和多态实现原理

进入主题前,先把工具设置好。本文使用编译测试环境:Visual Studio 2013 VS2013查看类内存布局设置方式如下截图: 先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX

Do-Program的专栏 1288

C++ 类对象大小计算(二)含有虚函数类

五、包含虚函数的类         包含虚函数的类,对象生成时,会在类对象当中插入一个指针,这个指针称做虚函数表指针,简称虚表指针(vPtr)。该指针指向一个虚函数表(简称虚表),虚函数表中存储了虚函数的入口地址。基类当中有虚函数时,会产生该虚函数表;创建基类对象,对象中的vPtr会指向该表;调用虚函数时,是通过vPtr在此表当中寻找函数入口地址的。         当派生类继承含有虚函数的子

跬步千里,窥叶知秋 1162

C++作业题:基于模板的链表、虚函数、多态

异质链表: 是指可以链表指针所指向的数据类型并不一致,比如一个链表中可以既存储整形数据,又可以存储浮点性的数据。在面向对象的语言中甚至就是可以指向不同的对象。而普通链表其数据类型必须一致。 C++多态:通过虚函数表实现多态。

微机原理与接口技术第六版周荷琴课后习题答案及解析(期末重点)

第二章 1.8086/8088CPU可直接寻址多少个内存(字节)单元?多少个I/O端口?它们的外部数据总线各有多少根? 8086/8088CPU使用的是20位地址总线,因此可以直接寻址2的20次方个内存单元(即1,048,576),每个单元大小为1字节,因此可以直接寻址2的20次方个字节的内存。 8086/8088CPU有64K个I/O端口,每个端口占用1字节的地址空间,因此可以直接寻址6...

Wmicheng的博客 1万+

38.【C++ 虚函数 纯虚函数 虚基类 (最全详解)】

虚函数、虚基类、纯虚函数

qq_69683957的博客 3468

weblogic启动异常故障处理(boot认证失败)

root 用户生成的文件属主为 root,WebLogic进程将无权读取,直接导致认证失败。· ⚠️ 密码必须完全一致:第3步 AdminAccount 命令中设置的密码,与第5步所有 boot.properties 文件中 password= 字段的值,必须保证完全一致。如果您的域下配置了受管服务器实例(如 ms_server1、ms_server2 等),必须逐一更新各实例下的免密启动文件,确保实例重启时密码与AdminServer保持一致,否则实例启动会因认证失败而报错。此步骤至关重要,必须执行。

weixin_53389944的博客 220

C++对象模型 第二章 虚函数

编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口。 多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。 多态还可分为: 动态多态(dynamic polymorphism):通过类继承机制和虚函数机制生效于运行期。可以优雅地处理异质对象集合,只要其共同的基类定义了虚函数的接口。也被称为子类型多态(Subtype polymorphism)或包含多态(inclusion polymorphism)。在面向对象程序设.

qq_53111905的博客 1048

从零到一:如何用YOLOv8和PyQt5打造你的第一个花卉识别桌面应用

本文详细介绍了如何使用YOLOv8和PyQt5从零开始构建一个花卉识别桌面应用。涵盖环境搭建、数据准备、模型训练、界面设计、模型集成与性能优化等全流程,帮助开发者快速掌握深度学习目标识别技术的实际应用。

weixin_29048775的博客 452

MinerU 系列教程 | 第二课:快速上手 -- 安装与第一次解析

本篇教程作为的第二课,将带你从零完成 MinerU 的环境搭建、模型下载,并成功运行第一次文档解析。上一课我们从宏观视角认识了 MinerU,本课开始正式动手实践。

weixin_43674738的博客 299

[C++基础]对象内存分布--基本

对于C++开发者来说,了解其内存是非常必要的,同时C++的多态(动态绑定)的原理也是很重要的。VS打开工程,右键工程名称—>属性—>c/c++—>命令行—> /d1 reportAllClassLayout 空类 class emptyClass {}; //1 > class emptyClass size(1) : //1 > +-- - //1 &g...

超越 1692

自然语言处理6 -- 情感分析

1 概述 情感分析是自然语言处理中常见的场景,比如淘宝商品评价,饿了么外卖评价等,对于指导产品更新迭代具有关键性作用。通过情感分析,可以挖掘产品在各个维度的优劣,从而明确如何改进产品。比如对外卖评价,可以分析菜品口味、送达时间、送餐态度、菜品丰富度等多个维度的用户情感指数,从而从各个维度上改进外卖服务。 情感分析可以采用基于情感词典的传统方法,也可以采用基于深度学习的方法,下面详细讲解 ...

谢杨易的博客 5万+

C++多态虚函数实现原理,对象和虚函数表的内存布局

基本概念 我们知道C++动态多态是用虚函数实现的,而虚函数的实现方式虽说C++标准没有要求,但是基本都是用虚函数表实现的(编译器决定)。所以我们有必要了解一下虚函数表的实现原理。 用virtual关键字声明的成员函数是虚函数。 具有虚函数的类及其派生的类会在编译时创建虚函数表,简称虚表(vtbl),虚表是虚函数指针的数组。 具有虚函数的类对象有一个虚表指针(vfptr),是编译器生成的指针,在对象构造时初始化。虚表指针vfptr指向虚表的第一个虚函数指针(即vfptr的值是虚表第一个...

回头太难 3707

Camunda 7.x 系列【22】全局监听器

Camunda支持义一个全局ExecutionListener和一个全局TaskListener,会捕获流程中发生的所有事件的通知,这样可以定义一个全局的审批人检查监听器,作用于所有用户任务。

记录知识、锤炼自我 2941

QT+QGIS跨平台编译(基于Qt,采用一套代码、一套框架,完成QGIS第三方库、核心库、应用程序的跨平台编译)

QT+QGIS跨平台编译。 基于Qt,采用一套代码、一套框架,完成QGIS第三方库、核心库、应用程序的跨平台编译。

LuckyHanMo的博客 4373

C++ 类的继承与多态机制,从语法到内存布局(含虚函数详解)

普通继承(不加 virtual)虚继承(加 virtual)A子对象存在两份A子对象只存在一份成员访问有歧义成员访问统一无歧义需要显式指定路径可直接访问内存占用更大占用更少。

SoulruiA的博客 3050

Python内置函数zip()——《Python内置函数手册》Python老吕编著

zip()函数是 Python 中的一个内置函数,用于将多个可迭代对象(如列表、元组、字符串等)中对应的元素打包成一个个元组,然后返回由这些元组组成的迭代器。zip()函数常用于将多个序列并行迭代,或者用于数据对齐。zip()它返回一个迭代器,这意味着你可以逐个获取打包后的元素,而不是一次性将所有元素加载到内存中。如果输入的可迭代对象长度不一致,zip()会以最短的可迭代对象为准,只打包到最短对象的末尾。它可以用来并行迭代多个序列,使得代码更加简洁和高效。在接下来的章节中,我们将详细介绍。

Python老吕的博客 680

C++继承和多态(虚函数、纯虚函数、虚继承)

C++继承和多态(虚函数、纯虚函数、虚继承) 一:继承 继承的概念:为了代码的复用,保留基类的原始结构,并添加派生类的新成员。 继承的本质:代码复用 我们用下图解释下: 那么我们这里就可以提出几个问题了: ①:进程的方式有哪些呢? 这里有三种继承方式: public:任意位置可以访问 protected:只允许本类类中以及子类类中访问 private:只允许...

IT_Quanwudi的博客 9058

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)

本篇文章,我们学习了面向对象编程的最后一点特性——多态。从多态的概念、多态的构成条件到多态的实现、多态的原理,以及多态在编程中的应用,我们进行了全面而深入的学习。

2301_80955819的博客 1576

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

C++多态,虚函数作用及底层实现原理

简述C++虚函数作用及底层实现原理 1.forewordC++是面向对象程序设计,其包括3项特点: (1)数据抽象:接口和实现分离 (2)继承:父类和子类 (3)多态:动态绑定本文讨论多态。当父类希望子类重新定义某些函数时,用virtual关键字声明为虚函数。当我们使用一个基类类型的引用或者指针,调用一个虚函数时就引发动态绑定/多态的发生。函数运行版本由传入的实参类型决定。可以用父类型的指针指

鸟恋旧林的博客 3903

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

虚函数实现多态的原理

1、C++中如何实现多态 基类中先声明一个虚函数 至少有一个继承该基类的子类 2、虚函数实现多态的原理 当一个类中出现虚函数或着子类继承了虚函数时,就会在该类中产生一个虚函数表(virtual table),虚函数表实际上是一个函数指针数组(在有的编译器作用下是链表),里面的每一个元素对应指向该类中的某一个虚函数的指针。被该类声明的对象会包含一个虚函数表指针(virtual table pointer),指向该类的虚函数表的地址。 虚函数的调用过程: 当一个对象要调用到虚函数时,先将对象内存中的vpt

Cynan_Gattackon 4282

浅析C++中虚函数的调用及对象的内部布局

     在我那篇《浅析C++中的this指针》中,我通过分析C++代码编译后生成的汇编代码来分析this指针的实现方法。这次我依然用分析C++代码编译后生成的汇编代码来说明C++中虚函数调用的实现方法,顺便也说明一下C++中的对象内部布局。下面所有的汇编代码都是用VC2005编译出来的。虽然,不同的编译器可能会编译出不同的结果,对象的内部布局也不尽相同;但是,只要是符合C++标准的编译器,编译结

寒星轩 1万+

C++类对象在内存中的布局

目录 一、前言 二、C++ 类对象的内存布局 2.1 只有数据成员的对象 2.2 没有虚函数的对象 2.3 拥有仅一个虚函数的类对象 2.4 拥有多个虚函数的类对象 三、继承关系中的C++类对象内存分布 3.1 存在继承关系且本身不存在虚函数的派生类的内存布局 ​3.2本身不存在虚函数(不严谨)但存在基类虚函数覆盖的单继承类的内存布局 3.3 定义了基类没有的虚函数的单继承的类对象布局 3.4 多继承且存在虚函数覆盖同时又存在自身定义的虚函数的类对象布局 3.5如果第1个直接基...

qq_41687938的博客 3601
上一篇: 转来的链接
下一篇: 界面处理相关API&MFC函数(随时更新)
ShenRui
博客等级 码龄20年 63粉丝 36原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值