虚表指针

虚函数有个特点。存在虚函数的类会在类的数据成员中生成一个虚函数指针 vfptr,而vfptr 指向了一张表(简称,虚表)。正是由于虚函数的这个特性,C++的多态才有了发生的可能。

其中虚函数表由三部分组成,分别是 RTTI(运行时类型信息)、偏移及虚函数的入口地址。而虚表与类及类生成的对象有存在着以下两种关系:

  • 类与虚表的关系:一个类只有一个虚表
  • 对象与类的关系:所有对象共享一个虚表

如下图所示:对象通过一个 vfptr (虚表指针)共享虚表.

虚表指针在类中的布局

1、虚表指针 vfptr 在上,类成员变量 ma 在下(图左)

2、类成员变量 ma 在上,虚表指针 vfptr 在下(图右)



在类中,vfptr 的优先级最高,所以虚函数在类中的布局应该是上图左边的结构,其中vftpr指针指向虚表,在虚表的起始位置存放这虚表所属类的类型信息RTTI(运行时类型信息 Run-Time Type Identification)。可以通过 typeid(pb).name() 查看。

虚函数表在类中的布局

现有基类 Base、派生类 Deriver 为测试代码:

#include<iostream>

class Base		//定义基类
{
public:
Base(int a) :ma(a) {}
virtual void Show() // 声明为虚函数
{
std::cout << "Base: ma = " << ma << std::endl;
}
protected:
int ma;
};
class Deriver : public Base //派生类
{
public:
Deriver(int b) :mb(b), Base(b) {}
void Show() // 没有声明为虚函数
{
std::cout << "Deriver: mb = " << mb << std::endl;
}
protected:
int mb;
};
1. 查看Base类的内存布局

在VS 2019开发者命令提示中输入:

cl 虚函数.cpp /d1reportSingleClassLayoutBase

其中,虚函数.cpp 为源文件的文件名, 最后的Base为要查看的类

/* Base类 内存布局 */
class Base size(8):
+---
0 | {vfptr}
4 | ma
+--- Base::$vftable@:
| &Base_meta //运行时类型信息 Run-Time Type Identification
| 0 //虚函数指针相对于整体作用域的偏移
0 | &Base::Show //虚函数入口地址,虚函数入口地址有一个或多个
2. 查看Deriver内存布局

输入:cl 虚函数.cpp /d1reportSingleClassLayoutDeriver

我们查寻到 Deriver的内存布局中类对象占据12个字节的空间。

/* Deriver类 内存布局 */
class Deriver size(12):
+---
0 | +--- (base class Base)
0 | | {vfptr}
4 | | ma
| +---
8 | mb
+--- Deriver::$vftable@:
| &Deriver_meta
| 0
0 | &Deriver::Show

发现:我们在源代码中并没有把 Deriver::Show() 声明为虚函数,但在Deriver的类内存布局中也存在 {vfptr} 指针。

这里不得不说虚函数的另一个特点了,“基类中同名同参的函数是虚函数,派生类中同名同参的函数也会变成虚函数”。意思是,在派生类中同名同参的函数即使没有 virtual 关键字声明也默认是虚函数,也会产生一张虚表。

那么派生类中的虚表结构又是什么样的呢?

根据上面提到的 vfptr 的优先级最大,并且 Deriver 是继承自 Base 类。因此,我门推测 Deriver 的内存布局应该是如下格式16字节布局才对,但显然不是这样。那么在派生类 Deriver 的内存布局中究竟进行了怎样的操作,才形成了12字节的内存布局呢?

注:以下结构为错误示范

/* 我们推测的Deriver类的内存布局 */
class Deriver size(16):
+---
0 | {vfptr} // Deriver::
4 | +--- (base class Base)
4 | | {vfptr} // Base::
8 | | ma
| +---
12 | mb
+---
解释这个原因之前我们先得了解派生类的虚表是怎样生成的?

在编译基类时,基类生成了一张虚表,在编译派生类时,又生成一张虚表。

我们在基类中添加一个Print() 函数,派生类中没有该函数。在上述假设成立的前提下,对应内存布局如下:



如果是这样,那么试想,在调用Print的时候,就需要查询两张虚表,从而找到 Base::Show() 对应的入口地址。这样做的确可行,但是整个调用的效率会变得非常差。

那么怎么来解决这个效率问题呢?

虚表合并

其实,在派生类的虚表生成好之后还有一个步骤,就是虚表的合并,具体演示如下:

将派生类中同名的虚函数覆盖到基类的虚表中,虚表合成之后,其中一个虚表指针已经没用了,不如也一并合并了。虚表指针合并的方式为向内层合并。因此,通过这一步虚表合并最终得到了Deriver 了的12字节内存布局。

那么有人就问了:为什么虚表指针合并的方式是向内合并,就不能向外合并吗?

要知道在继承中基类的指针是可以指向派生类对象,更加具体的说法是,基类的指针指向派生类对象中基类的起始部分。如果虚表指针向外层合并,那么对应的结构如下图所示,其中 Base* pb = new Deriver(10);

注:以下结构为错误示范



而正如我们问到的那样,如果虚表指针向外层合并的话,我们会发现无法通过虚表指针找到我们的虚表,因为在 Base:: 作用域中已经不存在虚表指针了。并且,当我们想要释放 new 出的堆区资源时,也不再是用 delete pb 而是 delete (Base*)((char*)pb - 4),因为在申请空间时内存分配的程序往往在被分配出的内存块“头部”放上一些校验信息。释放时必须从此空间的头部开始释放,否则会报 “Expression: is_block_type_valid(header->block_use)”错误。而我们申请的内存空间头部是在 0x100 的位置,而不是 0x104 的位置。这样在我们实际操作中就会很麻烦。因此,选择向内层合并就不就有这种问题产生。

因此,虚表指针选择向内层合并。

C++ | 虚函数表内存布局的更多相关文章

  1. C++ 虚函数表与多态 —— 多重继承的虚函数表 & 内存布局

    多重继承的虚函数表会有两个虚表指针,分别指向两个虚函数表,如下代码中的 vptr_s_1.vptr_s_2,Son类继承自 Father 和 Mather 类,并且改写了 Father::func_1 ...

  2. C++ 虚函数表与多态 —— 继承的虚函数表 & 内存布局

    1. 使用继承的虚函数表: 如果不涉及多重继承,每个类只有1个虚函数表,当子类继承父类后,子类可以自己改写和新增虚函数,如下图所示: 子类重写 func_1 后,子函数的 func_1 将会有新的逻辑 ...

  3. vs查看虚函数表和类内存布局

    虚继承和虚基类 虚继承:在继承定义中包含了virtual关键字的继承关系:     虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是:class CSubClass : publ ...

  4. C++对象的内存布局以及虚函数表和虚基表

    C++对象的内存布局以及虚函数表和虚基表 本文为整理文章, 参考: http://blog.csdn.net/haoel/article/details/3081328 http://blog.csd ...

  5. C++ 虚函数表与多态 —— 虚函数表的内存布局

       C++面试经常会被问的问题就是多态原理.如果对C++面向对象本质理解不是特别好,问到这里就会崩. 下面从基本到原理,详细说说多态的实现:虚函数 & 虚函数表.   1. 多态的本质: 形 ...

  6. C++对象内存模型2 (虚函数,虚指针,虚函数表)

    从例子入手,考察如下带有虚函数的类的对象内存模型: class A { public: virtual void vfunc1(); virtual void vfunc2(); void func1 ...

  7. C++ 虚函数表与内存模型

    1.虚函数 虚函数是c++实现多态的有力武器,声明虚函数只需在函数前加上virtual关键字,虚函数的定义不用加virtual关键字. 2.虚函数要点 (1) 静态成员函数不能声明为虚函数 可以这么理 ...

  8. C++对象的内存分布和虚函数表

    c++中一个类中无非有四种成员:静态数据成员和非静态数据成员,静态函数和非静态函数. 1.非静态数据成员被放在每一个对象体内作为对象专有的数据成员.    2.静态数据成员被提取出来放在程序的静态数据 ...

  9. 深入理解类成员函数的调用规则(理解成员函数的内存为什么不会反映在sizeof运算符上、类的静态绑定与动态绑定、虚函数表)

    本文转载自:http://blog.51cto.com/9291927/2148695 总结: 一.成员函数的内存为什么不会反映在sizeof运算符上?             成员函数可以被看作是类 ...

随机推荐

  1. 带你掌握Redis数据类型:string和Hash

    摘要:Redis中有五大数据类型,分别是String.List.Set.Hash和Zset. 本文分享自华为云社区<Redis的string类型常用命令解析>,作者:灰小猿 . 先问大家一 ...

  2. C#正则表达式(通俗易懂)

    正则表达式有多重要,话不多说,直接入正题把. 首先我们来说说元字符. 1. . 点号代表除了换行意外的任意单个字符 例如 a.b  说明a和b之间只能有任意一个字符. 2.[]代表从这里面取出任意一个 ...

  3. $_SERVER["QUERY_STRING"],$_SERVER["REQUEST_URI"],$_SERVER["SCRIPT_NAME"] 和$_SERVER["PHP_SELF"]

    $_SERVER["QUERY_STRING"],$_SERVER["REQUEST_URI"],$_SERVER["SCRIPT_NAME" ...

  4. Springboot循环依赖实践纪实

    测试的Springboot版本: 2.6.4,禁止了循环依赖,但是可以通过application.yml开启(哈哈) @Lazy注解解决循环依赖 情况一:只有简单属性关系的循环依赖 涉及的Bean: ...

  5. Scala语法1

    目录 main方法和def 函数 变量,类型转换,字符串分割拼接 文件读写和JDBC 面向对象编程 继承 case类,最常用的 main方法和def 函数 package scala_01 /** * ...

  6. linux基础篇--复习重点成长之路

    linux阶段性复习提纲 1.xshell与shell之间的区别 shell ​ shell是一个由C语言编写的程序,它的主要作用就是在用户和操作系统之间搭起一道桥梁(人机操作界面).直接在命令行执行 ...

  7. Laravel消息队列怎么使用

    使用database驱动做队列 下面是简单使用教程 1. 修改.env文件配置 QUEUE_CONNECTION=sync改成QUEUE_CONNECTION=database 默认的sync是同步队 ...

  8. Dapr云原生应用开发系列7:工作流集成

    题记:这篇介绍一个很有意思的东西,Dapr和Logic Apps这样的工作流引擎集成. Dapr工作流 在1年多前,Dapr的孵化团队搞了一个很有意思的东西:把Dapr和Logic Apps集成起来, ...

  9. Python模板引擎Jinja2使用简介

    原文链接 背景 最近在项目开发中,需要针对 Jenkins 项目进行配置,Jenkins 的 job 配置采用的是 xml,在维护配置模板的过程中就遇到了问题,因为逐步发现配置灵活性超出了字符串的范畴 ...

  10. 《手把手教你》系列基础篇(七十六)-java+ selenium自动化测试-框架设计基础-TestNG实现DDT - 下篇(详解教程)

    1.简介 今天这一篇宏哥主要是结合实际工作中将遇到的测试场景和前边两篇学习的知识结合起来给大家讲解和分享一下,希望以后大家在以后遇到其他的测试场景也可以将自己的所学的知识应用到测试场景中. 2.测试场 ...