C++ 类对象内存映射
Posted
技术标签:
【中文标题】C++ 类对象内存映射【英文标题】:C++ class object memory map 【发布时间】:2010-03-11 06:14:54 【问题描述】:当我们创建一个类的对象时,它的内存映射是什么样的。我对对象如何调用非虚拟成员函数更感兴趣。编译器是否会创建一个类似 vtable 的表,在所有对象之间共享?
class A
public:
void f0()
int int_in_b1;
;
A * a = new A;
a 的内存映射是什么?
【问题讨论】:
如果您想了解如何对 C++ 对象进行建模,我推荐 Stanley Lippman 的“Inside the C++ Object Model”(我说可以,因为有多种方法可以实现 C++ 内部结构)。 如果你更正了你的代码,为什么不运行你的编译器,看看它生成了什么? 【参考方案1】:你可以想象这段代码:
struct A
void f()
int int_in_b1;
;
int main()
A a;
a.f();
return 0;
被转化成类似的东西:
struct A
int int_in_b1;
;
void A__f(A* const this)
int main()
A a;
A__f(&a);
return 0;
调用 f 是直截了当的,因为它是非虚拟的。 (有时对于虚拟调用,如果知道对象的动态类型,就可以避免虚拟调度,就像这里一样。)
一个更长的例子可以让你了解虚函数是如何工作的,或者让你非常困惑:
struct B
virtual void foo() puts(__func__);
;
struct D : B
virtual void foo() puts(__func__);
;
int main()
B* a[] = new B(), new D() ;
a[0]->foo();
a[1]->foo();
return 0;
变成这样:
void B_foo(void) puts(__func__);
void D_foo(void) puts(__func__);
struct B_VT
void (*foo)(void);
B_vtable = B_foo ,
D_vtable = D_foo ;
typedef struct B
struct B_VT* vt;
B;
B* new_B(void)
B* p = malloc(sizeof(B));
p->vt = &B_vtable;
return p;
typedef struct D
struct B_VT* vt;
D;
D* new_D(void)
D* p = malloc(sizeof(D));
p->vt = &D_vtable;
return p;
int main()
B* a[] = new_B(), new_D();
a[0]->vt->foo();
a[1]->vt->foo();
return 0;
每个对象只有一个vtable指针,你可以在类中添加许多虚方法而不影响对象大小。 (vtable 会增长,但每个类存储一次,并且不会产生显着的大小开销。)请注意,我在此示例中简化了许多细节,但 does work:未解决析构函数(此处应该是虚拟的) ,它会泄漏内存,并且 __func__ 值会略有不同(它们是由编译器为当前函数的名称生成的)等等。
【讨论】:
第二个例子是我几周前写的,现在我发现我忘了添加 this 指针,即使它们没有被使用。如果您看不到如何添加它们,请告诉我,我可以编辑;否则我将保持它与键盘链接中的编译代码相同。【参考方案2】:认识到 C++ 语言并未指定或强制要求有关对象内存布局的所有内容。也就是说,大多数编译器的做法都差不多。
在您的示例中,类型 A 的对象只需要足够的内存来保存 int
。因为它没有虚函数,所以它不需要 vtable。如果 f0
成员被声明为虚拟成员,那么 A 类型的对象通常会以指向 A 类 vtable(由所有 A 类型的对象共享)的指针开头,后跟 int 成员。
反过来,vtable 有一个指向每个定义、继承或覆盖的虚函数的指针。为对象调用虚函数包括从对象指向 vtable 的指针,然后使用到 vtable 的固定偏移量(在编译时为每个虚函数确定)来查找要调用的函数的地址。
【讨论】:
我知道 vtable 是如何工作的。我对编译器如何处理非虚拟函数感兴趣。他们也有单独的表格吗? @Peter:函数与类的大小无关,与布局无关。这些函数就像您编写的任何其他函数一样,它们驻留在内存中等待调用的某个地方。成员函数唯一的一点是它们有一个你看不到的隐式this
指针。
@Peter:和其他函数一样。跳过所有技术寻址业务,该函数有一些编译地址。这是编译器用来调用类实例上的函数的地址。
对于编译器来说,在 a.f0() 中找到要调用的函数的地址与调用全局函数没有区别:它只是一个全局函数名。编译器生成直接调用指令,链接器将其修复。诀窍在于,对于非虚拟函数,编译器会“破坏”名称,因此两个不同类中具有相同名称的两个非虚拟函数最终在完整程序中具有两个不同的全局名称。例如,A 类的 f0() 可能全局称为 __1A2f0vv。
@Peter:可访问性(公共、受保护、私有)在编译时检查。如果您违反它,编译器将给出错误并且不会发出代码(例如程序集)。类似地检查非静态成员函数是否被类的实例调用:尝试一些无效的东西,你会得到一个错误。【参考方案3】:
函数不会根据它们所在的类来存储。
通常编译器只会像对待任何其他函数一样对待任何成员函数,除了为“this”指针添加一个参数。当您根据调用它的对象的地址调用它时,它会自动传递给函数。
所有的函数,静态的,成员的,甚至虚拟的成员都以同样的方式存储在内存中,它们都只是函数。
当编译器构建代码时,它会在进入内存的位置进行硬编码,然后链接器会遍历您的代码并将“使用此名称调用函数”命令替换为“在此硬编码地址调用函数”
【讨论】:
【参考方案4】:class A
public:
void f0()
void f1(int x) int_in_b1 = x;
int int_in_b1;
;
A *a = new A();
在内部实现(表示)如下:(函数名实际上是错位的)
struct A
int int_in_b1;
;
void Class_A__constructor(struct a*) // default constructor
void Class_A__f0(struct a*)
void Class_A__f1(struct a*, int x) a->int_in_b1 = x;
// new is translated like this: (inline)
void* new()
void* addr = malloc(sizeof(struc a));
Class_A__constructor(addr);
return addr;
可以通过对目标文件执行命令“nm”来验证(结果命名为重整)
【讨论】:
@Roger:谢谢,我没注意到以上是关于C++ 类对象内存映射的主要内容,如果未能解决你的问题,请参考以下文章