现在的位置: 首页 > 综合 > 正文

c++ 对象内存布局

2013年08月08日 ⁄ 综合 ⁄ 共 5763字 ⁄ 字号 评论关闭

很奇怪不是么?很多人在使用C++。但很少有人真正关心C++ Compile是如何实现的。

Jan Gray在1994曾经写了一篇叫做C++ under the Hood的文章,介绍了Visual
C++的实现细节。这篇指南就是基于Jan的文章之上,我同时会将Jan文章中让人难于理解的地方详细阐述。希望这篇指南可以让更多的人了解C++的底层
实现机制。

The layout of a Class

struct B {
public:
   int bm1;
protected:
   int bm2;
private:
   int bm3;
};
Struct B 在内存中的layout是怎么样的? Visual C++保证B中的member variables 在内存中的layout与它们生命的顺序一致。Struct B在内存的中layout应该是这个样子的:

 

Single Inheritance

struct C {
   int c1;
   void cf();
};
struct D : C {
   int d1;
   void df();
};
在Visual C++中保证在C的member variables 在内存中的位置永远在D的起始位置。就像这样:

这样做的好处是当C* pC = new D();Visual C++不需要为pC做额外的displacement 转换。pC 的address equal  D* pD = new D();中的pD.

Multiple Inheritance

比较复杂:
struct E {
   int e1;
   void ef();
};
struct F : C, E {
   int f1;
   void ff();
};
多重继承比较复杂,他们的Base和Derived的指针的位置不再相同。
F f;
// (void*)&f == (void*)(C*)&f;
// (void*)&f <  (void*)(E*)&f;
通过如下的Diagram of layout你可以看得更加清楚:

为什么在图中C在E的上面?这是Visual C++ 的convention罢了,基类在内存中的layout correspond to 他们的的声明顺序。因为C的声明在E的前面,所以我们看到的F在内存的layout就是这样子的。

此图可知,E *pE = new F() 与C *pC = new F()中的pE 和pC指向的内存位置并不相同,对于pC
来说compiler不需要额外做任何事情,但是对于pE,为了让它指向E在内存中的位置compiler需要进行一种叫做displacement的调
整。

Virtual Inheritance

请考虑这种情形:
struct Employee { ... };
struct Manager : Employee { ... };
struct Worker : Employee { ... };
struct MiddleManager : Manager, Worker { ... };
 
无疑,按照我们之前的叙述,MiddleManager在内存中的layout应该是这个样的:

在内存中的有两个Employee的实例,如何Employee 很小那么这种冗余是可以忽略的,可是如果Employee很大呢?
那么有没有什么方法可以让Manager 和Worker在内存中共享同一个Instance呢?这就是Virtual
Inheritance需要解决的问题。
在享受这种优化的服务之前,你应该将你的类体系结构编程这样:
struct Employee { ... };
struct Manager : virtual Employee { ... };
struct Worker : virtual Employee { ... };
struct MiddleManager : Manager, Worker { ... };
 
也就是在希望被sharing 的基类前面加上Virtual关键字,多么直观啊。
struct G : virtual C {
   int g1;
   void gf();
};
struct H : virtual C {
   int h1;
   void hf();
};
struct I : G, H {
   int i1;
   void _if();
};
 
之后你的类在内存中的就应该是这个样子:

其中vbptr中存储的是对Employee的相对displacement.
Data Member Access
在没有继承的情形:
C* pc;
pc->c1; // *(pc + dCc1);
 
c1 的访问类似于*(pC + displacement of c1 within C);在本例子中根据Class C的定义和Diagram of layout我们可以发现displacement == 0.
在单继承的情形中:
D* pd;
pd->c1; // *(pd + dDC + dCc1); // *(pd + dDCc1);
pd->d1; // *(pd + dDd1);
 
根据我们之前的Diagram不难看出pd->c1 == *(pd + displacement from D to C + displacement from C to c1).这种情形中displacement == 0。
pd->d1 == *(pd + displacement from D to d1).  这种情形中 displacement == 4。
在多重继承中,情形稍微复杂些,但所有的displacement 还都是常量(constant)。
F* pf;
pf->c1; // *(pf + dFC + dCc1); // *(pf + dFc1);
pf->e1; // *(pf + dFE + dEe1); // *(pf + dFe1);
pf->f1; // *(pf + dFf1); 
我想何以根据我们之前的Diagram轻松的算出每一个displacement。
虚拟继承又是怎么的呢?
I* pi;
pi->c1; // *(pi + dIGvbptr + (*(pi+dIGvbptr))[1] + dCc1);
pi->g1; // *(pi + dIG + dGg1); // *(pi + dIg1);
pi->h1; // *(pi + dIH + dHh1); // *(pi + dIh1);
pi->i1; // *(pi + dIi1);
I i;
i.c1; // *(&i + IdIC + dCc1); // *(&i + IdIc1);
 
对g1,h1,以及i1的访问很容易理解,我想说说对c1的访问。
pi->c1
是一种动态的访问。在runtime的时候编译器不知道pi的真正type是什么,这时就要用到之前说过的vbptr,(*(pi +
dIGvbptr))[1]是指在特定的vbptr中(不论vbptr是属于 G还是H)其对于base virtual
class的偏移地址。至于为什么是(*(pi + dIGvbptr))[1] 而不是 (*(pi +
dIGvbptr))[0],我猜这也是Visual C++的设计使然吧。 如果你知道(*(pi +
dIGvbptr))[0]中放的什么,请让我知道
对于i.c1的访问,因为这是一种静态的访问,为了节省开销C++对它的处理直接而干脆。之所以C++敢于这么做是因为在I中displacement of i在这种静态声明中是固定不变的。

Casts

理解了以上概念相信Casts between 2 types就不是什么问题了,一下是我们常见的一些cast在Visual C++中的实现手段。
对于多重继承来说:
F* pf;
(C*)pf; // (C*)(pf ? pf + dFC : 0); // (C*)pf;
(E*)pf; // (E*)(pf ? pf + dFE : 0);
对于虚拟继承来说:
I* pi;
(G*)pi; // (G*)pi;
(H*)pi; // (H*)(pi ? pi + dIH : 0);
(C*)pi; // (C*)(pi ? (pi+dIGvbptr + (*(pi+dIGvbptr))[1]) : 0);
 
什么,没看懂?那么就再看一遍我对Data Member Access的描述吧。
Member Functions
struct P {
   int p1;
   void pf(); // new
   virtual void pvf(); // new
};
对于一个non-static 成员变量的访问应该是这样的(我想因该大部分程序员都会了解吧)member function被调用的的时候会被传入一个this指针他的类型是:
Type X * const。(有人想过为什么是会是这样的声明而不是const Type X * const 或者const Type X *么?

果声明为const Type X *那么我们将无法通过this指针修改member variables。至于const Type X *
const么实际上当你 将pf定义成:void pf() const;那么传入的this就是const Type X *
const的。通过Type X * const 我们不能擅自修改this指针本身,不信你试试。)
所以对于pf的调用实际上应该是这个样子的:
void P::pf() { // void P::pf([P *const this])
   ++p1;   // ++(this->p1);
}

Overriding Member Functions

考虑以下声明:
struct Q : P {
   int q1;
   void pf();  // overrides P::pf
   void qf();  // new
   void pvf(); // overrides P::pvf
   virtual void qvf(); // new
};
Overridden member function包括 static 和 dynamic 调用。在C++中使用virtual关键字来区分。
情形1:static resolution:
当一个member function被重写且没有virtual那么,对他的调用在compiling 的时候就已经determined.
P p; P* pp = &p; Q q; P* ppq = &q; Q* pq = &q;
pp->pf();  // pp->P::pf();  // P::pf(pp);
ppq->pf(); // ppq->P::pf(); // P::pf(ppq);
pq->pf();  // pq->Q::pf();  // Q::pf((P*)pq);
pq->qf();  // pq->Q::qf();  // Q::qf(pq);
 
当pp->pf()
以及 ppq->pf()这两种情形,调用它们的指针类型在compiling是就已经安插。因为没有Virtual
那么就没有多态的干扰,Visual C++将忠实于->运算符左侧的类型,并且将此类型作为this传入此函数。

情形 2:dynamic resolution:
pp->pvf();  // pp->P::pvf();  // P::pvf(pp);
ppq->pvf(); // ppq->Q::pvf(); // Q::pvf((Q*)ppq);
pq->pvf();  // pq->Q::pvf();  // Q::pvf((P*)pq);
 
可怜的C++编译器,将如何决议overridden member function 的类型呢?为了解决这个问题vfptr被引入。
通常被安插在memory layout的第一个位置,它指向此class的 vftable。 Vftable中存储的是所有virtual functions的地址。就像这样:

当子类重写了父类的方法那么vftable中相应的entry 就应该被改写,如图:

C++就是通过这种方式来进行overridden member function 的dynamic resolution。

Virtual Functions: Multiple Inheritance

这是本指南最刺激和有趣的一部分,我要向你介绍著名的Thunk技术。
考虑一下情形:
struct R {
   int r1;
   virtual void pvf(); // new
   virtual void rvf(); // new
};
struct S : P, R {
   int s1;
   void pvf(); // overrides P::pvf and R::pvf
   void rvf(); // overrides R::rvf
   void svf(); // new
}; 
 
这样的layout应该如何画?我猜是这样的:


S s; S* ps = &s;
((P*)ps)->pvf(); // ((P*)ps)->P::vfptr[0])((S*)(P*)ps)
((R*)ps)->pvf(); // ((R*)ps)->R::vfptr[0])((S*)(R*)ps)

我lunching以上两种调用,我所期望的的函数语义应该是就像每个函数注释后面的一样。毕竟->运算符左侧的是一个S*对吧,所以传入
member function的指针也应该是S*。当使用P*是问题很简单,P*和S*指向的是相同的内存地址,C++
compiler不需要做任何事情。但是当使用R*后有点问题,R*和S*指向的内存地址不同。那么我们就要使用一些技巧让R*指针转化为S*。对于这个
问题的解决办法基本上就是使用一种叫做Thunk的技术。重写 entry of pvf within vftable。
重写的方法很多,在VC++中重写后的结果像这样:
 
S::pvf-adjust: // MSC++
   this -= SdPR;
   goto S::pvf() 
呵呵,很简单是么,将原先指向R*的this指针- displacement of S from R, 然后jump 到真正的S::pvf()的函数地址中。

Constructors and Destructors

Constructor 和 Destructor我们常见,但是不能使用。通常有compiler将其分解成为多部构造。
Constructor 被分解后应该是这样的:
1)对于一个most derived类,初始化vbptr,并调用virtual base 的构造函数。
 2)调用non-virtual base classes 的构造函数。
 3)调用data members的构造函数
4)初始化vfptr。
5)执行用户写在constructor中的代码。
Destructor被分解后应该是这样的:
1) 初始化vfptr
2) 执行用户卸载destructor中的代码。
3) 调用data member 的析构函数,顺序是与data member 在类中声明的顺序相反。
4) 调用non-virtual bases的析构函数,与声明的顺序相反。
5) 对于一个most derived 的类,调用它的virtual base的析构函数。

抱歉!评论已关闭.