内存布局 C++ 对象

Posted

技术标签:

【中文标题】内存布局 C++ 对象【英文标题】:memory layout C++ objects [closed] 【发布时间】:2010-12-10 14:17:48 【问题描述】:

我基本上想知道 C++ 如何在内存中布置对象。所以,我听说动态转换只是用偏移量调整对象在内存中的指针;并且重新解释允许我们用这个指针做任何事情。我真的不明白这一点。细节将不胜感激!

【问题讨论】:

【参考方案1】:

内存布局主要留给实现。关键的例外是给定访问说明符的成员变量将按其声明顺序排列。

§ 9.2.14

具有相同访问权限的(非联合)类的非静态数据成员 分配控制权(第 11 条)以便后来的成员拥有更高的 类对象中的地址。非静态的分配顺序 未指定具有不同访问控制的数据成员 (11)。 实现对齐要求可能会导致两个相邻的成员 不得紧随其后分配;所以可能 管理虚拟功能的空间要求 (10.3) 和 虚拟基类 (10.1)。

除了成员变量之外,类或结构还需要为成员变量、基类的子对象、虚拟函数管理(例如虚拟表)以及这些数据的填充和对齐提供空间。这取决于实现,但 Itanium ABI 规范是一种流行的选择。 gcc 和 clang 坚持它(至少在一定程度上)。

http://mentorembedded.github.io/cxx-abi/abi.html#layout

Itanium ABI 当然不是 C++ 标准的一部分,也没有约束力。要获得更详细的信息,您需要查看实施者的文档和工具。 clang 提供了查看类的内存布局的工具。举个例子:

class VBase 
    virtual void corge();
    int j;
;

class SBase1 
    virtual void grault();
    int k;
;

class SBase2 
    virtual void grault();
    int k;
;

class SBase3 
    void grault();
    int k;
;

class Class : public SBase1, SBase2, SBase3, virtual VBase 
public:
    void bar();
    virtual void baz();
    // virtual member function templates not allowed, thinking about memory
    // layout and vtables will tell you why
    // template<typename T>
    // virtual void quux();
private:
    int i;
    char c;
public:
    float f;
private:
    double d;
public:
    short s;
;

class Derived : public Class 
    virtual void qux();
;

int main() 
    return sizeof(Derived);

创建使用类的内存布局的源文件后,clang 会显示内存布局。

$ clang -cc1 -fdump-record-layouts layout.cpp

Class的布局:

*** Dumping AST Record Layout
   0 | class Class
   0 |   class SBase1 (primary base)
   0 |     (SBase1 vtable pointer)
   8 |     int k
  16 |   class SBase2 (base)
  16 |     (SBase2 vtable pointer)
  24 |     int k
  28 |   class SBase3 (base)
  28 |     int k
  32 |   int i
  36 |   char c
  40 |   float f
  48 |   double d
  56 |   short s
  64 |   class VBase (virtual base)
  64 |     (VBase vtable pointer)
  72 |     int j
     | [sizeof=80, dsize=76, align=8
     |  nvsize=58, nvalign=8]

有关此 clang 功能的更多信息,请参阅 Eli Bendersky 的博客:

http://eli.thegreenplace.net/2012/12/17/dumping-a-c-objects-memory-layout-with-clang/

gcc 提供了一个类似的工具,`-fdump-class-hierarchy'。对于上面给出的类,它会打印(除其他外):

Class Class
   size=80 align=8
   base size=58 base align=8
Class (0x0x141f81280) 0
    vptridx=0u vptr=((& Class::_ZTV5Class) + 24u)
  SBase1 (0x0x141f78840) 0
      primary-for Class (0x0x141f81280)
  SBase2 (0x0x141f788a0) 16
      vptr=((& Class::_ZTV5Class) + 56u)
  SBase3 (0x0x141f78900) 28
  VBase (0x0x141f78960) 64 virtual
      vptridx=8u vbaseoffset=-24 vptr=((& Class::_ZTV5Class) + 88u)

它没有逐项列出成员变量(或者至少我不知道如何获取它),但您可以知道它们必须在偏移量 28 和 64 之间,就像在 clang 布局中一样。

您可以看到一个基类被单独指定为primary。当Class 作为SBase1 访问时,这消除了调整this 指针的需要。

gcc 的等价物是:

$ g++ -fdump-class-hierarchy -c layout.cpp

Visual C++ 的等价物是:

cl main.cpp /c /d1reportSingleClassLayoutTest_A

见:https://blogs.msdn.microsoft.com/vcblog/2007/05/17/diagnosing-hidden-odr-violations-in-visual-c-and-fixing-lnk2022/

【讨论】:

g++ 的信息似乎不正确:error: unrecognized command line option ‘-fdump-class-hierarchy’【参考方案2】:

每个类都按照声明的顺序排列其数据成员。 允许编译器在成员之间放置填充以提高访问效率(但不允许重新排序)。

dynamic_cast&lt;&gt; 的工作原理是编译器实现细节,标准没有定义。这一切都取决于编译器使用的 ABI。

reinterpret_cast&lt;&gt; 只需更改对象的类型即可。唯一可以保证有效的是,将指向 void* 的指针转换为相同的指针,指向类的指针将为您提供相同的指针。

【讨论】:

您的第一点并不完全正确。您拥有的唯一保证是同一访问块中的成员将具有定义的顺序。如果您想将其发挥到极致,您可以说即使访问权限相同,也不再保证订单。 @Richard。我不确定我是否理解你。不允许编译器对元素重新排序(这是为了与 C 向后兼容)。什么是访问块。您能否指出您从中获取信息的标准的正确部分? 访问块(实际上是访问说明符)是类中的public:private:protected:。我发现这篇文章:embedded.com/design/218600150?pgno=1 非常有用,这也是我第一次了解到 Richard 在评论中提到的内容。我查找了那里链接的 C++ 标准,以及 pg. 198(第 9.2 节第 12 节)它指出:“未指定具有不同访问控制的非静态数据成员的分配顺序” “分配顺序”是指内存地址位置的顺序还是分配发生的时间顺序? 与 C 的兼容性仅适用于具有“标准布局*”的类型。“标准布局”类型具有(以及其他限制)所有公共、所有私有或所有受保护的成员变量。允许编译器,例如首先布置所有私有成员,然后是受保护的,然后是公共的,但是在任何块中,它们必须按顺序排列(但不一定是连续的)。所以是的,编译器允许重新-order,只是不在访问说明符块内。实际上,我认为没有任何编译器会重新排序变量。【参考方案3】:

答案是“很复杂”。动态转换不是简单地用偏移量调整指针;它实际上可能会检索对象内部的内部指针以完成其工作。 GCC 遵循为 Itanium 设计但实施更广泛的 ABI。你可以在这里找到血淋淋的细节:Itanium C++ ABI。

【讨论】:

【参考方案4】:

如前所述,完整的细节很复杂,阅读起来很痛苦,并且真的只对编译器开发人员有用,并且在编译器之间会有所不同。基本上,每个对象都包含以下内容(通常按此顺序排列):

    运行时类型信息 非虚拟基础对象及其数据(可能按声明顺序)。 成员变量 虚拟基础对象及其数据(可能按某些 DFS 树搜索顺序)。

这些数据可能会或可能不会被填充以使内存对齐更容易等。隐藏在运行时类型信息中的是关于类型的内容,虚拟父类的 v-tables 等,所有这些都是编译器特定的。

当涉及到强制转换时,reinterpret_cast 只是改变了指针的 C++ 数据类型而不做任何其他事情,所以你最好确保在使用它时知道你在做什么,否则你很容易把事情搞砸了。 dynamic_cast 与 static_cast 做的事情非常相似(改变指针),除了它使用运行时类型信息来确定它是否可以转换为给定类型,以及如何转换。同样,所有这些都是编译器特定的。请注意,您不能dynamic_castvoid*,因为它需要知道在哪里可以找到运行时类型信息,这样它才能进行所有精彩的运行时检查。

【讨论】:

【参考方案5】:

这个问题已经在 http://dieharddeveloper.blogspot.in/2013/07/c-memory-layout-and-process-image.html 这是那里的摘录: 在进程的地址空间中间,有一个区域是为共享对象保留的。创建新进程时,进程管理器首先将两个段从可执行文件映射到内存中。然后它解码程序的 ELF 标头。如果程序头指示可执行文件链接到共享库,则进程管理器 (PM) 将从程序头中提取动态解释器的名称。动态解释器指向一个包含运行时链接器代码的共享库。

【讨论】:

虽然它是有价值的信息,但它回答了一个不同的问题。

以上是关于内存布局 C++ 对象的主要内容,如果未能解决你的问题,请参考以下文章

c++对象内存模型内存布局

C++ 内存布局:深入理解C++内存布局

浅析C++内存布局

C++ 对象的内存布局(上)

c++对象模型是什么,对象的内存布局和结构问题

C++类对象的内存布局