很多人都用vc++来写程序,但是不知道你们想没想过,在vc++的内部,它是如何进行编译的?
下面就是对vc++内部机制的简单介绍。
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 * co
nst。(有人想过为什么是会是这样的声明而不是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的析构函数。
您可能还对这些文章感兴趣
最新Godaddy优惠码 2012年 - 长期更新
中国电信 中国移动 互联网 优惠码 博客 域名 广告 建站 微软 心情 战略 技巧 操作系统 新闻 游戏娱乐 电脑维修 病毒 百度 硬件相关 硬盘 站长评论 编程开发 网上调查 网站事务 网站技术 网络奇趣 网络赚钱 蜘蛛 计算机安全 软件 软件技巧 通信 通信产业 闲话杂谈 e godaddy Google 广告 IT Photo Shop seo vc windows7 Windows XP wordpress z-blog
WP Cumulus Flash tag cloud by Roy Tanck and Luke Morton requires Flash Player 9 or better.


Recent Comments