继承类的内存布局

Posted

技术标签:

【中文标题】继承类的内存布局【英文标题】:memory layout of inherited class 【发布时间】:2011-12-29 19:04:25 【问题描述】:

我很想知道这些课程在记忆中的具体安排方式,尤其是。具有继承和虚函数。

我知道这不是由 c++ 语言标准定义的。但是,是否有任何简单的方法可以通过编写一些测试代码来找出您的特定编译器将如何实现这些?

编辑:- 使用下面的一些答案:-

#include <iostream>

using namespace std;

class A 
  public:
    int a;
    virtual void func() 
;

class B : public A 
  public:
    int b;
    virtual void func() 
;

class C 
  public:
    int c;
    virtual void func() 
;

class D : public A, public C 
  public:
    int d;
    virtual void func() 
;

class E : public C, public A 
  public:
    int e;
    virtual void func() 
;

class F : public A 
  public:
    int f;
    virtual void func() 
;

class G : public B, public F 
  public:
    int g;
    virtual void func() 
;

int main() 
  A a; B b; C c; D d; E e; F f; G g;
  cout<<"A: "<<(size_t)&a.a-(size_t)&a<<"\n";
  cout<<"B: "<<(size_t)&b.a-(size_t)&b<<" "<<(size_t)&b.b-(size_t)&b<<"\n";
  cout<<"C: "<<(size_t)&c.c-(size_t)&c<<"\n";
  cout<<"D: "<<(size_t)&d.a-(size_t)&d<<" "<<(size_t)&d.c-(size_t)&d<<" "<<(size_t)&d.d-    (size_t)&d<<"\n";
  cout<<"E: "<<(size_t)&e.a-(size_t)&e<<" "<<(size_t)&e.c-(size_t)&e<<" "<<(size_t)&e.e-    (size_t)&e<<"\n";
  cout<<"F: "<<(size_t)&f.a-(size_t)&f<<" "<<(size_t)&f.f-(size_t)&f<<"\n";
  cout<<"G: "<<(size_t)&g.B::a-(size_t)&g<<" "<<(size_t)&g.F::a-(size_t)&g<<" "    <<(size_t)&g.b-(size_t)&g<<" "<<(size_t)&g.f-(size_t)&g<<" "<<(size_t)&g.g-(size_t)&g<<"\n";

输出是:-

A: 8
B: 8 12
C: 8
D: 8 24 28
E: 24 8 28
F: 8 12
G: 8 24 12 28 32

所以所有类在 loc 0 处都有一个大小为 8 的 v-ptr。 D 在位置 16 有另一个 v-ptr。对于 E 也是如此。 G 似乎在 16 处也有一个 v-ptr,尽管根据我(有限的)理解,我猜它有更多。

【问题讨论】:

【参考方案1】:

一种方法是打印出所有成员的偏移量:

class Parent
public:
    int a;
    int b;

    virtual void foo()
        cout << "parent" << endl;
    
;

class Child : public Parent
public:
    int c;
    int d;

    virtual void foo()
        cout << "child" << endl;
    
;

int main()

    Parent p;
    Child c;

    p.foo();
    c.foo();

    cout << "Parent Offset a = " << (size_t)&p.a - (size_t)&p << endl;
    cout << "Parent Offset b = " << (size_t)&p.b - (size_t)&p << endl;

    cout << "Child Offset a = " << (size_t)&c.a - (size_t)&c << endl;
    cout << "Child Offset b = " << (size_t)&c.b - (size_t)&c << endl;
    cout << "Child Offset c = " << (size_t)&c.c - (size_t)&c << endl;
    cout << "Child Offset d = " << (size_t)&c.d - (size_t)&c << endl;

    system("pause");

输出:

parent
child
Parent Offset a = 8
Parent Offset b = 12
Child Offset a = 8
Child Offset b = 12
Child Offset c = 16
Child Offset d = 20

所以你可以在这里看到所有的偏移量。您会注意到偏移量 0 处没有任何内容,因为这可能是指向 vtable 的指针所在的位置。

还要注意继承的成员在 Child 和 Parent 中具有相同的偏移量。

【讨论】:

+1 这个示例代码与我想出的最接近。尽量避免依赖内存布局。无法保证它在编译器的未来版本中会保持不变(甚至在具有相同编译器版本的其他上下文中,例如优化)。我敢打赌,几乎总会有更好的方法来解决问题。 谢谢。这确实有点帮助。将您的答案与 Azza 的答案结合起来……我也对如果我们有 A 级会发生什么感兴趣; B类; C类:公共A,公共B;这似乎给出了存在多个 vtable 指针的结果。 A 的数据成员似乎排在 B 之前。 我从来没有处理过多重继承。但是您仍然可以尝试查看偏移量显示的内容。老实说,我不知道多重继承在下面是如何工作的。 编辑了我的问题以提供一些多重继承的示例。我没有看到任何 v-table 指针。可能 gcc 对待它们与 msvcc 不同。 @owagh 我认为,在在内存中探索它们之前,您需要对可能的不同实现虚拟方法/继承有一些基本的了解。不同的实现会将 vptr 放在对象的不同位置,可能(罕见的实现)将指向虚拟基类的指针放入对象本身,并以不同的方式组织 vtable 本身。通常,单个 vptr 放在对象前面,它指向 vtable,在正偏移处具有虚函数地址,在负偏移处具有 虚拟基类偏移量【参考方案2】:

Visual Studio 至少有一个 hidden compiler option /d1reportSingleClassLayout(从 ~32:00 开始)。

用法:/d1reportSingleClassLayoutCLASSNAME,其中编译器开关和CLASSNAME 之间不应有空格(显然将其替换为您感兴趣的类的名称)。

【讨论】:

嘿,这很棒,正是我想要的。 gcc、icc等其他编译器有没有类似的功能? @owagh:抱歉,我不知道。 :// @owagh 对于 gcc,这似乎不是一个完美的等价物,但也许它仍然有用:***.com/questions/15951597/…(简而言之:使用-g-ggdb 在调试模式下编译,然后在目标文件上使用pahole 实用程序)。【参考方案3】:

创建一个类对象,将指向它的指针转换为您机器的字,使用sizeof 查找对象的大小,并检查该位置的内存。像这样的:

#include <iostream>

class A

 public:
  unsigned long long int mData;
  A() :
   mData( 1 )
  
        
  virtual ~A()
  
  
;
class B : public A

 public:
  unsigned long long int mData1;
  B() :
   A(), mData1( 2 )
  
  
;

int main( void )

 B lB;

 unsigned long long int * pB = ( unsigned long long int * )( &lB );

 for( int i = 0; i < sizeof(B) / 8; i++ )
 
  std::cout << *( pB + i ) << std::endl;
 

 return ( 0 );



Program output (MSVC++ x86-64):

5358814688 // vptr
1          // A::mData
2          // B::mData1

顺便说一句,Stanley B. Lippman 有一本很棒的书 "Inside the C++ Object Model"。

【讨论】:

【参考方案4】:

最好的方法可能是编写一些简单的测试用例,然后在汇编器中编译和调试它们(所有优化关闭):一次运行一条指令,你会看到一切都适合。

至少我是这么学的。

如果您发现任何特别具有挑战性的案例,请在 SO 中发帖!

【讨论】:

【参考方案5】:

只要你坚持单继承,子对象通常按照它们声明的顺序排列。前面有一个指针,它们在前面键入信息,例如用于动态调度。一旦涉及到多重继承,事情就会变得更加复杂,尤其是涉及到虚拟继承时。

要找到至少一种 ABI 风格的准确信息,您可以查找 Itanium ABI。这记录了所有这些细节。它至少在某些 Linux 平台上用作 C++ ABI(即,多个编译器可以生成链接到一个可执行文件中的目标文件)。

要确定布局,只需打印给定对象的子对象的地址。也就是说,除非您碰巧实现了编译器,否则它通常无关紧要。我怀疑对象布局的唯一真正用途是安排成员以最小化填充。

【讨论】:

如果没有virtual函数,肯定不会添加vptr?我试图就没有任何 virtual 的 POD 的单一继承是否会导致成员的确切顺序(即 base.m1, base.m2, derived.m1, derived.m2)得到一个确凿的答案。我知道我可以尝试一下,看看它是否适用于我的实现,但我真的想找到一种有保证的跨平台方式来做到这一点。 (背景是我正在映射到顺序至关重要的二进制格式。)

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

虚继承之单继承的内存布局(VC在编译时会把vfptr放到类的头部,这和Delphi完全一致)

c++头脑风暴-多态虚继承多重继承内存布局

c++头脑风暴-多态虚继承多重继承内存布局

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

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

Java对象的内存布局