这是Jan Gray在1994的一篇文章,是以Visual C++作为实现来讲解C++对象布局、继承、多态等的实现
下面是我的阅读笔记:
Intruduction
理解编程语言如何实现是非常重要的。这些关于语言实现的知识驱散了类似“编译器究竟在这里做了什么手脚?”之类的恐惧和疑问;给予了使用新特性的信心;并提供了调试和学习其他语言特性时的领悟力。它也提供了每天写最有效率代码的感觉。
本篇认真探索C++的底层,解释“运行时”实现细节,如类布局技术和virtual函数调用机制。将解释的问题包括:
- 类如何内存布局?
- 数据成员如何访问?
- 成员函数如何调用?
- 什么是adjuster thunk?
- 代价如何:
- 单继承、多继承、虚继承
- 虚函数和虚函数调用
- cast到基类或虚基类
- 异常处理
Class Layout
本节,我们将考虑不同类型继承的内存布局
C-like Structs
和C是兼容的,struct遵循简单的结构体布局规则:按成员声明的顺序布局,并受制于实现定义的对齐填补
所有的C++厂商确保有效的struct由他们的C++编译器存储保持相同
例子:
struct A {
char c;
char i;
};
C-like Structs with C++ feature
这里B是一个带C++风格的的struct:有public/protected/private 访问控制符,成员函数,静态成员和嵌套的类型声明。
只有那些non-virtual 的数据成员在每个实例占有空间
struct B {
public:
int bm1;
protected:
int bm2;
private:
int bm3;
static int bsm;
void bf();
typedef void * bpv;
struct N {};
};
Single Inheritance
struct C {
int c1;
void cf();
};
struct D : C {
int d1;
void df();
};
在D中,虽然没有要求说c的实例数据必须优先于D的实例数据,但是语言实现这样布局可以确保D中的基类子对象c的起始地址和D实例对象的起始地址一样,这样当我们需要从D*得到子对象地址时可以减少开销。
于是,在单继承的类层次中,新的实例数据成员仅仅是简单的追加到基类布局的后面。
Multiple Inheritance
在大多数设计中,单继承已经有足够表达力来表达继承关系。但是有时,我们希望继承类能取得两个或更多类的行为,多继承正好满足了这个要求。
struct E {
int e1;
void ef();
};
struct F : public C, E {
inf f1;
void ef();
};
F继承自C和E,并且F的实例包含了每个基类的实例。但是不像单继承,不可能使得每个基类子对象的地址都和继承类的实例地址保持一样,其中只能有一个一样,其他的得做偏移。
既然存在地址偏移,我们注意到当我们做cast的时,对于需要地址偏移的cast显然有效率代价。
当然了,这种偏移对于语言使用者来说是透明的,偏移计算由编译器来实现。
Virtual Inheritance
struct G : virtual C {
int g1;
void gf();
}
struct H : virtual C {
int h1;
void hf();
}
struct I : G, H {
int i1;
void _if();
}
在G和H,C子对象的数据成员都是紧随各自数据成员之后。
但是当布局I时,我们不可能保留原有的位置关系,在上面例子中,对于G实例对象中,G到C的偏移与在I实例中的G到C的偏移并不相同。由于编译后,并不知道其从哪个类继承,于是必须有一种方法来计算继承类到虚基类的地址偏移。
在VC++中,这是通过添加一个隐藏的指针vbptr(虚基表指针)来实现的。它指向了一个类共享的地址偏移表。通常指向的虚基表中的第二项就是到虚基类的偏移
如图中的IdGvbptrC和IdHvbptrC
Data Member Access