内存布局 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<>
的工作原理是编译器实现细节,标准没有定义。这一切都取决于编译器使用的 ABI。
reinterpret_cast<>
只需更改对象的类型即可。唯一可以保证有效的是,将指向 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_cast
和void*
,因为它需要知道在哪里可以找到运行时类型信息,这样它才能进行所有精彩的运行时检查。
【讨论】:
【参考方案5】:这个问题已经在 http://dieharddeveloper.blogspot.in/2013/07/c-memory-layout-and-process-image.html 这是那里的摘录: 在进程的地址空间中间,有一个区域是为共享对象保留的。创建新进程时,进程管理器首先将两个段从可执行文件映射到内存中。然后它解码程序的 ELF 标头。如果程序头指示可执行文件链接到共享库,则进程管理器 (PM) 将从程序头中提取动态解释器的名称。动态解释器指向一个包含运行时链接器代码的共享库。
【讨论】:
虽然它是有价值的信息,但它回答了一个不同的问题。以上是关于内存布局 C++ 对象的主要内容,如果未能解决你的问题,请参考以下文章