第一篇摘自:http://www.21ic.com/app/embed/201103/79359.htm
1. 词法分析
词法分析器根据词法规则识别出源程序中的各个记号(token),每个记号代表一类单词(lexeme)。源程序中常见的记号可以归为几大类:关键字、标识符、字面量和特殊符号。词法分析器的输入是源程序,输出是识别的记号流。词法分析器的任务是把源文件的字符流转换成记号流。本质上它查看连续的字符然后把它们识别为“单词”。
2. 语法分析
语法分析器根据语法规则识别出记号流中的结构(短语、句子),并构造一棵能够正确反映该结构的语法树。
3. 语义分析
语义分析器根据语义规则对语法树中的语法单元进行静态语义检查,如果类型检查和转换等,其目的在于保证语法正确的结构在语义上也是合法的。
4. 中间代码生成
中间代码生成器根据语义分析器的输出生成中间代码。中间代码可以有若干种形式,它们的共同特征是与具体机器无关。最常用的一种中间代码是三地址码,它的一种实现方式是四元式。三地址码的优点是便于阅读、便于优化。
5. 中间代码优化
优化是编译器的一个重要组成部分,由于编译器将源程序翻译成中间代码的工作是机械的、按固定模式进行的,因此,生成的中间代码往往在时间和空间上有很大浪费。当需要生成高效目标代码时,就必须进行优化。
6. 目标代码生成
目标代码生成是编译器的最后一个阶段。在生成目标代码时要考虑以下几个问题:计算机的系统结构、指令系统、寄存器的分配以及内存的组织等。编译器生成的目标程序代码可以有多种形式:汇编语言、可重定位二进制代码、内存形式。
7 符号表管理
符号表的作用是记录源程序中符号的必要信息,并加以合理组织,从而在编译器的各个阶段能对它们进行快速、准确的查找和操作。符号表中的某些内容甚至要保留到程序的运行阶段。
8 出错处理
用户编写的源程序中往往会有一些错误,可分为静态错误和动态错误两类。所谓动态错误,是指源程序中的逻辑错误,它们发生在程序运行的时候,也被称作动态语义错误,如变量取值为零时作为除数,数组元素引用时下标出界等。静态错误又可分为语法错误和静态语义错误。语法错误是指有关语言结构上的错误,如单词拼写错、表达式中缺少操作数、begin和end不匹配等。静态语义错误是指分析源程序时可以发现的语言意义上的错误,如加法的两个操作数中一个是整型变量名,而另一个是数组名等。
第二篇摘自:http://jpkc.nwpu.edu.cn/dzjc/jsjrj/text/chapter01/section01/r2a.htm
第三篇摘自:http://apps.hi.baidu.com/share/detail/32154894
《Visual C 编译器原理》
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]中放的什么,请让我知道? br />对于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的析构函数。