前言

C++对象模型是个常见、且复杂的话题,本文基于Itanium C++ ABI通过程序实践介绍了几种 简单C++继承 场景下对象模型,尤其是存在虚函数的场景,并通过图的方式直观表达内存布局。
本文展示的程序构建环境为Ubuntu,glibc 2.24,gcc 6.3.0。由于clang和gcc编译器都是基于Itanium C++ ABI(详细信息参考gcc ABI policy),因此本文介绍的对象模型对clang编译的程序也基本适用。

虚函数表简介

虚函数表布局

含有虚函数的类,编译器会为其添加一个虚函数表(vptr)。
用如下程序验证含有虚函数的类的内存布局,该程序很简单,只定义了构造函数,虚析构函数,和一个int成员变量。

// Derive.h
class Base_C
{
public:
Base_C();
virtual ~Base_C(); private:
int baseC;
}; // Derive.cc
Base_C::Base_C()
{
} Base_C::~Base_C()
{
}

gcc编译器可通过-fdump-class-hierarchy参数,查看类的内存布局。可得到如下信息:

// g++ -O0 -std=c++11 -fdump-class-hierarchy Derive.h
Vtable for Base_C
Base_C::_ZTV6Base_C: 4u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI6Base_C)
16 (int (*)(...))Base_C::~Base_C
24 (int (*)(...))Base_C::~Base_C Class Base_C
size=16 align=8
base size=12 base align=8
Base_C (0x0x7fb8e9185660) 0
vptr=((& Base_C::_ZTV6Base_C) + 16u)

从类Base_C的定义来看,类占用的空间包括一个虚函数表指针vptr和一个整型变量。由于内存对齐的原因,类占用16字节
接下来看虚函数表,表中一共有4个entry,每个entry都是函数指针,指向具体的虚函数,因此每个entry在测试的机器上编译占8字节(指针大小)。

注意看到表中虚析构函数有两个,这实际上是Itanium C++ ABI规定的:

The entries for virtual destructors are actually pairs of entries.
The first destructor, called the complete object destructor, performs the destruction without calling delete() on the object.
The second destructor, called the deleting destructor, calls delete() after destroying the object.
Both destroy any virtual bases; a separate, non-virtual function, called the base object destructor,
performs destruction of the object but not its virtual base subobjects, and does not call delete().

虚析构函数在虚函数表中占用两条目,分别是complete object destructordeleting destructor

除了析构函数,虚函数表还有两个条目,紧靠析构函数的是typeinfo指针,指向类型信息对象(typeinfo object),用于运行时类型识别(RTTI)。

第一个条目看起来可能比较陌生,是offset,该偏移存储了从当前虚表指针(vtable pointer)位置到对象顶部的位移。在ABI文档中这两个条目均有详细的介绍:

// typeinfo指针
The typeinfo pointer points to the typeinfo object used for RTTI. It is always present.
All entries in each of the virtual tables for a given class must point to the same typeinfo object.
A correct implementation of typeinfo equality is to check pointer equality, except for pointers (directly or indirectly) to incomplete types.
The typeinfo pointer is a valid pointer for polymorphic classes, i.e. those with virtual functions, and is zero for non-polymorphic classes.
// offset偏移
The offset to top holds the displacement to the top of the object from the location within the object of the virtual table pointer that addresses this virtual table, as a ptrdiff_t. It is always present.
The offset provides a way to find the top of the object from any base subobject with a virtual table pointer. This is necessary for dynamic_cast<void*> in particular.
In a complete object virtual table, and therefore in all of its primary base virtual tables, the value of this offset will be zero.
For the secondary virtual tables of other non-virtual bases, and of many virtual bases, it will be negative. Only in some construction virtual tables will some virtual base virtual tables have positive offsets,
due to a different ordering of the virtual bases in the full object than in the subobject's standalone layout.

另外需要注意的是:vptr=((& Base_C::_ZTV6Base_C) + 16u),虽然虚函数表中有四个条目,但是vptr的指针实际上并不是指向表的起始位置,而是指向第一个虚函数的位置。

Base_C的内存布局如下图所示:

继承下的C++对象模型

单继承下C++对象模型

首先,看一个单继承场景的例子:

// 此处省略类的实现部分
class Base_C
{
public:
Base_C();
virtual ~Base_C(); private:
int baseC;
}; class Base_D : public Base_C
{
public:
Base_D(int i);
virtual ~Base_D();
virtual void add(void) { cout << "Base_D::add()..." << endl; }
virtual void print(void); private:
int baseD;
}; class Derive_single : public Base_D
{
public:
Derive_single(int d);
void print(void) override;
virtual void Derive_single_print(); private:
int Derive_singleValue;
};

单继承场景下,派生类有且只有一个虚表(将基类的虚表复制),同时派生类中override的虚函数,会在虚函数表中对原函数进行覆盖派生类新增的虚函数也将追加到虚函数表的尾部
从整体内存布局上来看,派生类中新增的非静态成员变量,也会追加到基类的成员变量之后
打印类内存布局如下:

Vtable for Derive_single
Derive_single::_ZTV13Derive_single: 7u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI13Derive_single)
16 (int (*)(...))Derive_single::~Derive_single
24 (int (*)(...))Derive_single::~Derive_single
32 (int (*)(...))Base_D::add
40 (int (*)(...))Derive_single::print
48 (int (*)(...))Derive_single::Derive_single_print Class Derive_single
size=24 align=8
base size=20 base align=8
Derive_single (0x0x7fb8e93fe8f0) 0
vptr=((& Derive_single::_ZTV13Derive_single) + 16u)
Base_D (0x0x7fb8e93fe958) 0
primary-for Derive_single (0x0x7fb8e93fe8f0)
Base_C (0x0x7fb8e91857e0) 0
primary-for Base_D (0x0x7fb8e93fe958)

内存布局如下图所示,内存布局和上述描述一致:

多继承下C++对象模型(非菱形)

接下来考虑非菱形多继承场景,此时对于派生类,会将其每个基类的虚函数表“拷贝”一份,最终组成虚函数表组,虚函数表排列顺序,由基类在类定义中的声明顺序决定。
派生类的虚函数被放在声明的第一个基类的虚函数表中,派生类对基类函数override时,会覆盖所有基类中对应的函数。

// 此处省略类的实现部分
class Base_A
{
public:
Base_A(int i);
virtual ~Base_A();
int getValue();
static void countA();
virtual void print(void); private:
int baseA;
static int baseAS;
}; class Base_B
{
public:
Base_B(int i);
virtual ~Base_B();
int getValue();
virtual void add(void);
static void countB();
virtual void print(void); private:
int baseB;
static int baseBS;
}; class Derive_multiBase : public Base_A, public Base_B
{
public:
Derive_multiBase(int d);
void add(void) override;
void print(void) override;
virtual void Derive_multiBase_print(); private:
int Derive_multiBaseValue;
};

打印类内存布局如下:

Vtable for Derive_multiBase
Derive_multiBase::_ZTV16Derive_multiBase: 13u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI16Derive_multiBase)
16 (int (*)(...))Derive_multiBase::~Derive_multiBase
24 (int (*)(...))Derive_multiBase::~Derive_multiBase
32 (int (*)(...))Derive_multiBase::print
40 (int (*)(...))Derive_multiBase::add
48 (int (*)(...))Derive_multiBase::Derive_multiBase_print
56 (int (*)(...))-16
64 (int (*)(...))(& _ZTI16Derive_multiBase)
72 (int (*)(...))Derive_multiBase::_ZThn16_N16Derive_multiBaseD1Ev
80 (int (*)(...))Derive_multiBase::_ZThn16_N16Derive_multiBaseD0Ev
88 (int (*)(...))Derive_multiBase::_ZThn16_N16Derive_multiBase3addEv
96 (int (*)(...))Derive_multiBase::_ZThn16_N16Derive_multiBase5printEv Class Derive_multiBase
size=32 align=8
base size=32 base align=8
Derive_multiBase (0x0x7fb8e910cd20) 0
vptr=((& Derive_multiBase::_ZTV16Derive_multiBase) + 16u)
Base_A (0x0x7fb8e91855a0) 0
primary-for Derive_multiBase (0x0x7fb8e910cd20)
Base_B (0x0x7fb8e9185600) 16
vptr=((& Derive_multiBase::_ZTV16Derive_multiBase) + 72u)

从内存布局中可看到存在两个vptr(分别指向两个虚函数表),对应Derive_multiBase从两个基类Base_ABase_B拷贝得到的虚函数表。
派生类Derive_multiBase中所有虚函数都拓展在主虚函数表(primary virtual table),也即从Base_A拷贝得到的虚函数表。
Base_B拷贝得到的虚函数表也称为辅助虚函数表(secondary virtual tables),从内存布局中看到其offset-16,因为此虚函数表指针距对象内存的初始位置16个字节。

同时注意到此虚函数表中虚函数符号为non-virtual thunk to...,这个和函数跳转的机制有关,通过thunk对调用不同父类的函数的地址进行修正,可以参考深入探索 C++多态②-继承关系C++对象模型中的介绍。

// thunk
A segment of code associated (in this ABI) with a target function, which is called instead of the target function for the purpose of modifying parameters (e.g. this) or
other parts of the environment before transferring control to the target function,
and possibly making further modifications after its return.
A thunk may contain as little as an instruction to be executed prior to falling through to an immediately following target function,
or it may be a full function with its own stack frame that does a full call to the target function.

内存布局如下图所示:

讨论:enable_shared_from_this特性如何影响内存布局

enable_shared_from_this文档中有如下描述:

A common implementation for enable_shared_from_this is to hold a weak reference (such as std::weak_ptr) to *this.
For the purpose of exposition, the weak reference is called weak-this and considered as a mutable std::weak_ptr member.

enable_shared_from_this的通常实现是让实例拥有一个“弱引用”,可表现为实例有个std::weak_ptr的成员变量
可在单继承场景的测试代码上进行验证,对Derive_single类增加继承自std::enable_shared_from_this<Derive_single>,其他不变:

class Derive_single : public Base_D, public std::enable_shared_from_this<Derive_single>
{
public:
Derive_single(int d);
void print(void) override;
virtual void Derive_single_print(); private:
int Derive_singleValue;
};
首先打印类内存布局如下:
Vtable for Derive_single
Derive_single::_ZTV13Derive_single: 7u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI13Derive_single)
16 (int (*)(...))Derive_single::~Derive_single
24 (int (*)(...))Derive_single::~Derive_single
32 (int (*)(...))Base_D::add
40 (int (*)(...))Derive_single::print
48 (int (*)(...))Derive_single::Derive_single_print Class Derive_single
size=40 align=8
base size=36 base align=8
Derive_single (0x0x7fd5c76431c0) 0
vptr=((& Derive_single::_ZTV13Derive_single) + 16u)
Base_D (0x0x7fd5c7639750) 0
primary-for Derive_single (0x0x7fd5c76431c0)
Base_C (0x0x7fd5c7632780) 0
primary-for Base_D (0x0x7fd5c7639750)
std::enable_shared_from_this<Derive_single> (0x0x7fd5c76327e0) 16

对比前文,可发现Derive_single内存占用由24字节增大到40字节,原因是std::enable_shared_from_this<Derive_single>的继承多占用了16字节。从std::weak_ptr的文档中可知std::weak_ptr的典型实现实际上是存储了两个指针,和这里的16字节内存增长一致。

// std::weak_ptr
Like std::shared_ptr, a typical implementation of weak_ptr stores two pointers:
-- a pointer to the control block;
-- the stored pointer of the shared_ptr it was constructed from.

另外,特别注意此时Derive_single类虚函数表和前文没有差异,因此enable_shared_from_this特性不影响虚函数表的内容

参考资料

Itanium C++ ABI
图说C++对象模型:对象内存布局详解
C++对象模型
C++深入探索C++多态②-继承关系
虚函数继承-thunk技术初探
C++:虚函数内存布局解析(以clang编译器为例)

 
 

C++内存模型实践探索的更多相关文章

  1. 理论与实践中的 C# 内存模型

    转载自:https://msdn.microsoft.com/magazine/jj863136 这是该系列(包含两部分内容)的第一部分,这部分将以较长的篇幅介绍 C# 内存模型. 第一部分说明 C# ...

  2. Java 理论与实践: 修复 Java 内存模型,第 2 部分(转载)

    在 JSR 133 中 JMM 会有什么改变? 活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型(Java Memory Model, JMM)的公开建议.在本系列文章的 ...

  3. 理论与实践中的 C# 内存模型,第 2 部分

    转载自:https://msdn.microsoft.com/zh-cn/magazine/jj883956.aspx 这是介绍 C# 内存模型的系列文章的第二篇(共两篇). 正如在 MSDN 杂志十 ...

  4. 探索 Linux 内存模型--转

    引用:http://www.ibm.com/developerworks/cn/linux/l-memmod/index.html 理解 Linux 使用的内存模型是从更大程度上掌握 Linux 设计 ...

  5. C++11 并发指南七(C++11 内存模型一:介绍)

    第六章主要介绍了 C++11 中的原子类型及其相关的API,原子类型的大多数 API 都需要程序员提供一个 std::memory_order(可译为内存序,访存顺序) 的枚举类型值作为参数,比如:a ...

  6. JVM内存模型与性能调优

    堆内存(Heap) 堆是由Java虚拟机(JVM,下文提到的JVM特指Sun hotspot JVM)用来存放Java类.对象和静态成员的内存空间,Java程序中创建的所有对象都在堆中分配空间,堆只用 ...

  7. 修复 Java 内存模型,第 1 部分——Brian Goetz

    转自Java并发大师Brain Goetz:http://www.ibm.com/developerworks/cn/java/j-jtp02244/ (中文地址) http://www.ibm.co ...

  8. Java的内存模型

    "让计算机并发执行若干个运算任务"与"更充分地利用计算机处理器的效能"之间的因果关系,看起来顺理成章,实际上它们之间的关系并没有想象中的那么简单,其中一个重要的 ...

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

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

  10. java内存模型及分块

    转自:http://www.cnblogs.com/BangQ/p/4045954.html 1.JMM简介 2.堆和栈 3.本机内存 4.防止内存泄漏   1.JMM简介   i.内存模型概述 Ja ...

随机推荐

  1. 人类社会学:“重男轻女"思潮的比重,从多孩男女性别比角度思考

    参考资料: https://baijiahao.baidu.com/s?id=1780697594797038227 https://mbd.baidu.com/newspage/data/video ...

  2. Apache DolphinScheduler如何开启开机自启动功能?

    转载自东华果汁哥 Apache DolphinScheduler 是一个分布式.去中心化的大数据工作流调度系统,支持大数据任务调度.若要设置 DolphinScheduler 开机自启动,通常需要将其 ...

  3. 无缝融入,即刻智能[一]:Dify-LLM大模型平台,零编码集成嵌入第三方系统,42K+星标见证专属智能方案[含ollama部署]

    无缝融入,即刻智能[一]:Dify-LLM大模型平台,零编码集成嵌入第三方系统,42K+星标见证专属智能方案 1.Dify 简介 1.1 功能情况 Dify,一款引领未来的开源大语言模型(LLM)应用 ...

  4. 关于Springboot理解

    面向对象五大基本原则 graph LR A(面向对象五大原则);B(单一职责原则);C(开放封闭原则);D(里式替换原则);E(依赖倒置原则);F(接口隔离原则); A---B;A---C;A---D ...

  5. 使用 navigateTo 实现灵活的路由导航

    title: 使用 navigateTo 实现灵活的路由导航 date: 2024/8/13 updated: 2024/8/13 author: cmdragon excerpt: 摘要:本文详细介 ...

  6. SpringBoot整合RabbitMQ 通俗易懂 超详细 【内含案例】

    SpringBoot结合RabbitMq SpringBoot 框架部署 HelloWorld 简单模式 Topic 通配符模式 一.SpringBoot 框架部署 1.创建Maven工程(我用的ID ...

  7. 瑞芯微 | 如何固定以太口地址为指定ip?ifplugd妙用

    rxw的RK3568的evb1公板,有2个以太口, 默认UI界面只能配置eth0,无法配置eth1, 实际应用中,有时需要一旦有网线插入,就需要该地址设置为指定IP地址. 本文介绍2个最简单的方法实现 ...

  8. 【原创】VSCode 快捷键汇总(已整理成一张 A4 纸大小,方便打印)

    由于经常记不住 VSCode 的快捷键,每次查找不方便,于是乎做了一份 VSCode 快捷键汇总,已整理成一张 A4 纸大小,方便打印,这样查找就直观多了~ 直接保存图片,横向打印即可.

  9. 推荐2款实用的持续集成与部署(CI&CD)自动化工具

    前言 最近DotNetGuide技术社区交流群有不少同学在咨询:持续集成与部署(CI&CD)自动化工具有什么好用的推荐?今天大姚给大家推荐2款实用且免费的持续集成与部署(CI&CD)自 ...

  10. MiniMax:如何基于 JuiceFS 构建高性能、低成本的大模型 AI 平台

    MiniMax 成立于 2021 年 12 月,是领先的通用人工智能科技公司,致力于与用户共创智能.MiniMax 自主研发了不同模态的通用大模型,其中包括万亿参数的 MoE 文本大模型.语音大模型以 ...