精华内容
下载资源
问答
  • 编译器报错or告警---未初始化的变量

    千次阅读 2017-10-15 16:13:20
    现象: vs开 SDL ,编译示例代码,按照逻辑 s_test应该是被分配空间了的,但是会报错。 观察实验: ...有时候,为了保证码农知道自己做了什么,编译器会在使用未人工赋值的变量时,告警或者报错。 结论:

    现象:

    vs开 SDL ,编译示例代码,按照逻辑 s_test应该是被分配空间了的,但是会报错。

    观察实验:

    这时候手动加个默认构造函数会过。成员变量声明方式改为  int c =1;也会过。

    原理:

    定义声明---大概是指这个过程,分配空间,赋初始值。有编译器赋初始值和码农赋初始值两种。有时候,为了保证码农知道自己做了什么,编译器会在使用未人工赋值的变量时,告警或者报错。

    结论:需要告诉编译器我知道我干了什么。

    示例代码:

    #include <iostream>

    using namespace std;
    struct test
    {
        int *a;
        int *b;
        int c;
    };
    int main()
    {
        int ia = 1;
        int ib = 2;
        int ic = 3;
        test s_test;
        //std::is_trivial<test>::value;
        cout << std::is_trivial<test>::value<<endl;
        s_test.a = &ia;
        s_test.b = &ib;
        //s_test.c = &ic;

        cout << s_test.a << endl;
        cout << s_test.b << endl;
        cout << s_test.c << endl;

    }


    展开全文
  • 编译器编译原理详细解析

    千次阅读 2014-11-06 13:30:06
    编译器编译原理详细解析 分类: c++专区 VS编译器有关 2014-08-01 14:16 219人阅读 评论(0) 收藏 举报 第一篇摘自:http://www.21ic.com/app/embed/201103/79359.htm 1. 词法分析 词法分析器根据...

    编译器编译原理详细解析

    分类: c++专区 VS编译器有关 219人阅读 评论(0) 收藏 举报

    第一篇摘自: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

    计算机能读懂的语言是机器码,但对人来说由1和0组合的二进制序列既难写又难读。于是出现了用英文字母代表操作码的汇编语言,汇编语言是机器语言的符号化,汇编语言是面向机器的,使用汇编语言编程需要直接安排存储,规定寄存器、运算器的动作次序,汇编语言与计算机紧密相关,不同的计算机在指令长度、寻址方式、寄存器数目、指令表示等方面都不一样,由于汇编语言不便于进行数学描述,而且不可移植,于是出现了高级语言。高级语言是面向计算过程的和面向问题的语言,只与解题的步骤有关,而将高级程序设计语言"翻译"成机器语言的工作则是由编译程序来完成的。程序员的工作则是把要计算的问题化成高级程序设计语言的表达式、语句、过程/函数、对象,而不是机器指令序列。
      把高级语言程序翻译成机器语言程序有两种做法:
    编译和解释,相应的翻译工具也分别叫做编译器和解释器。以下分别讲述。
    1.编译器工作原理
      编译器逐行扫描高级语言程序源程序,编译的过程如下:
    (1)
    词法分析(Lexical Analysis)。识别关键字、字面量、标识符 (变量名、数据名)、运算符、注释行(给人看的,一般不处理)、特殊符号(续行、语句结束、数组)等六类符号,分别归类等待处理。
    (2)
    语法分析 (Syntax Analysis)。一个语句看作一串记号 (Token)流,由语法分析器进行处理。按照语言的文法检查判定是否是合乎语法的句子。如果是合法句子就以内部格式保存,否则报错。直至检查完整个程序。
    (3)
    语义分析 (Semantic Analysis)。语义分析器对各句子的语法做检查:运算符两边类型是否相兼容;该做哪些类型转换 (例如,实数向整数赋值要"取整");控制转移是否到不该去的地方;是否有重名或者使语义含糊的记号,等等。如果有错误,则转出错处理,否则可以生成执行代码。
    (4)
    中间代码生成。中间代码是向目标码过渡的一种编码,其形式尽可能和机器的汇编语言相似,以便下一步的代码生成。但中间码不涉及具体机器的操作码和地址码。采用中间码的好处是可以在中间码上做优化。
    (5)
    优化。对中间码程序做局部优化和全局 (整个程序)优化,目的是使运行更快,占用空间最小。局部优化是合并冗余操作,简化计算,例如x:=0可用一条"清零"指令替换。全局优化包括改进循环、减少调用次数和快速地址算法等。
    (6)
    代码生成。由代码生成器生成目标机器的目标码 (或汇编)程序,其中包括数据分段、选定寄存器等工作,然后生成机器可执行的代码。
      高级语言源程序经编译后得到目标码程序,还不能立即装入机器执行,因为程序中如果用到标准函数(它们生成的目标码已存放在模块库中),还需对编译后得到的目标模块进行连接。连接程序 (Linker)找出需要连接的外部模块,然后到模块库中找出被调用的模块,调入内存并连接到目标模块上,形成可执行程序。执行时,把可执行程序加载 (Loading)到内存中合适的位置 (此时得到的是内存中的绝对地址),就可执行了。其示意图为图1.1所示
       

    图1.1 编译、连接和执行程序的过程

    2.高级语言程序的解释执行
      编译型语言由于可进行优化 ( 有的编译器可做多次优化 ) ,目标码效率很高,因此是目前软件开发的最主要编程语言。常见的程序设计语言,如 C/C++ 、 Pascal 、 FORTRAN 等都是编译型语言,用这些语言编写的源程序,都需要进行编译、连接,才能生成可执行程序。这对于大型程序、系统程序、支持程序来说是十分有利的,虽然编译时花费了不少时间,但程序的执行效率是很高的。不过,在有些场合,对程序的执行效率要求不高的场合,没有必要在编译上花费大量的时间,可以对高级语言源程序采取解释执行的方式。
      解释执行需要有一个解释器 (Interpreter) ,它将源代码逐句读入。第一步先作词法分析,建立内部符号表;再作语法和语义分析,并作类型检查 ( 解释语言的语义检查一般比较简单,因为它们往往采用无类型或动态类型系统 ) 。完成检查后把每一语句压入执行堆栈,并立即解释执行。因为解释执行时只看到一个语句,无法对整个程序进行优化。但是解释执行占用空间很少。
      操作系统的命令、Visual Basic、Java、JavaScript 都是解释执行的 ( 其中有些语言也可以编译执行 ) 。解释器不大,工作空间也不大,不过,解释执行难于优化、效率较低,这是这类语言的致命缺点 。 

    第三篇摘自: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的析构函数。

    展开全文
  • 接下来我们写一段简单的代码 demo.c 看一下编译器的工作过程,这里用的是ubuntu里面的vim编辑器以及gcc编译器。 #include <stdio.h> #define MAX_NUM 100 int main() { //打印MAX_NUM printf("MAX_NU...

    代码编译过程分为四个阶段:

    1. 预处理阶段
    2. 编译阶段
    3. 汇编阶段
    4. 链接阶段

    接下来我们写一段简单的代码 demo.c 看一下编译器的工作过程,这里用的是ubuntu里面的vim编辑器以及gcc编译器。

    #include <stdio.h>
    #define MAX_NUM 100
    int main()
    {
            //打印MAX_NUM
            printf("MAX_NUM=%d\n",MAX_NUM);
    
            int a=1,b=2;
    
            int c=a+b;
            printf("c=%d\n",c);
            
            return 0;
    }
    

    以上就是demo.c的代码。

    下面,我们将通过控制gcc编译器的参数,让gcc编译器一步一步的编译,而不是一次性完成编译。首先,简单看一下gcc的参数:
    在这里插入图片描述在ubuntu里面输入指令 man gcc即可找到gcc编译器的参数。我们将使用的是最前面的-C,-S,-E指令。下面我们开始用指令来控制。

    1.预处理阶段

    输入指令:gcc -E demo.c -o demo.i,得到的demo.i文件就是预处理之后的文件。我们可以进入这个文件。
    在这里插入图片描述
    进入demo.i,我们可以看到,预处理阶段将头文件、宏定义展开,并且删除了注释部分,而main()函数部分还是c语言代码。因此,预处理阶段的任务的就是:

    • 展开头文件
    • 展开宏定义
    • 删除所有注释

    2.编译阶段

    输入指令:gcc -S demo.i -o demo.S,得到的demo.S文件就是编译阶段的文件,.S后缀名表示的就是汇编代码。同样我们可以进入demo.S:
    在这里插入图片描述
    进入之后可以发现,代码都已经变成了汇编代码。这一阶段的主要任务就是:

    • 检查语法错误
    • 翻译成汇编语言

    3.汇编阶段

    输入指令:gcc -C demo.S -o demo.o,得到的demo.o就是汇编阶段后的代码:
    在这里插入图片描述
    我们可以看到,这个时候已经是机器码了。但是这个文件能不能直接运行呢?我们输入指令:./demo.o试一下:
    在这里插入图片描述
    这里会报错。报错就说明.o不是最终能跑起来的文件。但是为什么会报错呢?这已经是机器码了,为什么还是不能直接运行呢?
    因为我们看不懂机器码,所以我们先用反汇编来看看这个时候demo.o里面的内容。输入反汇编指令:objdump -d demo.o
    在这里插入图片描述
    系统里面会有两套地址,一套是系统自己用的的默认地址,一套是用户代码运行时用的内存地址。cpu是跳到内存里拿地址的,代码运行时,操作系统会把他放到用户代码运行的内存空间里,所以,编译器编译后的最终能运行的那个文件里面的地址不能是系统默认地址,而应该是内存地址。
    demo.o里面分配的是默认地址,操作系统把demo.o拿到内存时,不知道放在哪里(因为demo.o里面不是内存地址),因此无法运行。
    为了把代码放到内存里,要给代码的每一条指令链接一个内存地址,链接后的地址就是其在内存中存放的地址。这就是编译的第4个阶段。

    汇编阶段系统主要做的是

    • 将汇编语言生成机器指令

    4.链接阶段

    输入指令gcc demo.o -o demo.elf,这时候的.elf文件就是最终能运行的文件。我们可以反汇编一下这个文件,输入反汇编指令:objdump -d demo.elf,可以看到:
    在这里插入图片描述链接阶段主要做了三件事

    • 添加地址:把各种地址信息添加到headinfo中
    • 补代码:补充的是固定的进程的启动代码和初始化代码
    • 分段:一种技术,将机器码链接成段

    总结

    1. 上述是用代码控制了编译过程。如果想要一步到位,我们可以直接输入指令gcc demo.c -o demo.elf,后面的demo.elf,就算不加后缀名,默认也是elf的文件。
    2. 学习c语言不能单单学习语法,还要学习其他相关的基础知识。编译器是学习任何一门都需要接触的,所以了解编译器的工作原理也是很有必要的。

    以上就是文章的主要内容,如有错误,请各位朋友给我留言嗷。谢谢你们~
    展开全文
  • 编译器编译原理详解

    千次阅读 2012-05-04 20:15:23
    《Visual C 编译器原理》 Jan Gray在1994曾经写了一篇叫做C++ under the Hood的文章,介绍了Visual C++的实现细节。这篇指南就是基于Jan的文章之上,我同时会将Jan文章中让人难于理解的地方详细阐述。希望...

    第一篇摘自: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

    计算机能读懂的语言是机器码,但对人来说由1和0组合的二进制序列既难写又难读。于是出现了用英文字母代表操作码的汇编语言,汇编语言是机器语言的符号化,汇编语言是面向机器的,使用汇编语言编程需要直接安排存储,规定寄存器、运算器的动作次序,汇编语言与计算机紧密相关,不同的计算机在指令长度、寻址方式、寄存器数目、指令表示等方面都不一样,由于汇编语言不便于进行数学描述,而且不可移植,于是出现了高级语言。高级语言是面向计算过程的和面向问题的语言,只与解题的步骤有关,而将高级程序设计语言"翻译"成机器语言的工作则是由编译程序来完成的。程序员的工作则是把要计算的问题化成高级程序设计语言的表达式、语句、过程/函数、对象,而不是机器指令序列。
      把高级语言程序翻译成机器语言程序有两种做法:
    编译和解释,相应的翻译工具也分别叫做编译器和解释器。以下分别讲述。
    1.编译器工作原理
      编译器逐行扫描高级语言程序源程序,编译的过程如下:
    (1)
    词法分析(Lexical Analysis)。识别关键字、字面量、标识符 (变量名、数据名)、运算符、注释行(给人看的,一般不处理)、特殊符号(续行、语句结束、数组)等六类符号,分别归类等待处理。
    (2)
    语法分析 (Syntax Analysis)。一个语句看作一串记号 (Token)流,由语法分析器进行处理。按照语言的文法检查判定是否是合乎语法的句子。如果是合法句子就以内部格式保存,否则报错。直至检查完整个程序。
    (3)
    语义分析 (Semantic Analysis)。语义分析器对各句子的语法做检查:运算符两边类型是否相兼容;该做哪些类型转换 (例如,实数向整数赋值要"取整");控制转移是否到不该去的地方;是否有重名或者使语义含糊的记号,等等。如果有错误,则转出错处理,否则可以生成执行代码。
    (4)
    中间代码生成。中间代码是向目标码过渡的一种编码,其形式尽可能和机器的汇编语言相似,以便下一步的代码生成。但中间码不涉及具体机器的操作码和地址码。采用中间码的好处是可以在中间码上做优化。
    (5)
    优化。对中间码程序做局部优化和全局 (整个程序)优化,目的是使运行更快,占用空间最小。局部优化是合并冗余操作,简化计算,例如x:=0可用一条"清零"指令替换。全局优化包括改进循环、减少调用次数和快速地址算法等。
    (6)
    代码生成。由代码生成器生成目标机器的目标码 (或汇编)程序,其中包括数据分段、选定寄存器等工作,然后生成机器可执行的代码。
      高级语言源程序经编译后得到目标码程序,还不能立即装入机器执行,因为程序中如果用到标准函数(它们生成的目标码已存放在模块库中),还需对编译后得到的目标模块进行连接。连接程序 (Linker)找出需要连接的外部模块,然后到模块库中找出被调用的模块,调入内存并连接到目标模块上,形成可执行程序。执行时,把可执行程序加载 (Loading)到内存中合适的位置 (此时得到的是内存中的绝对地址),就可执行了。其示意图为图1.1所示
       

    图1.1 编译、连接和执行程序的过程

    2.高级语言程序的解释执行
      编译型语言由于可进行优化 ( 有的编译器可做多次优化 ) ,目标码效率很高,因此是目前软件开发的最主要编程语言。常见的程序设计语言,如 C/C++ 、 Pascal 、 FORTRAN 等都是编译型语言,用这些语言编写的源程序,都需要进行编译、连接,才能生成可执行程序。这对于大型程序、系统程序、支持程序来说是十分有利的,虽然编译时花费了不少时间,但程序的执行效率是很高的。不过,在有些场合,对程序的执行效率要求不高的场合,没有必要在编译上花费大量的时间,可以对高级语言源程序采取解释执行的方式。
      解释执行需要有一个解释器 (Interpreter) ,它将源代码逐句读入。第一步先作词法分析,建立内部符号表;再作语法和语义分析,并作类型检查 ( 解释语言的语义检查一般比较简单,因为它们往往采用无类型或动态类型系统 ) 。完成检查后把每一语句压入执行堆栈,并立即解释执行。因为解释执行时只看到一个语句,无法对整个程序进行优化。但是解释执行占用空间很少。
      操作系统的命令、Visual Basic、Java、JavaScript 都是解释执行的 ( 其中有些语言也可以编译执行 ) 。解释器不大,工作空间也不大,不过,解释执行难于优化、效率较低,这是这类语言的致命缺点 。 

    第三篇摘自: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的析构函数。


    展开全文
  • 编译器工作原理详解

    千次阅读 2015-03-31 09:18:40
    《Visual C 编译器原理》 Jan Gray在1994曾经写了一篇叫做C++ under the Hood的文章,介绍了Visual C++的实现细节。这篇指南就是基于Jan的文章之上,我同时会将Jan文章中让人难于理解的地方详细阐述。希望这篇...
  • 编译原理课程设计--C语言编译器实现甘肃政法学院编译原理课程设计题 目 C语言编译器实现计算机科学学院计算机科学与技术专业10 级 计本 班学 号: 201081010137姓 名: 杨青虎指导教师: 李 霞完成时间: 2013 年 6 ...
  • 编译器原理学习笔记 一

    千次阅读 2017-11-18 22:57:35
    俗称“龙书”的编译原理,希望可以通过记录来有所收获。
  • 编译器原理及相关

    千次阅读 2008-03-27 10:21:00
    转自:http://hi.baidu.com/angelanpan/blog/item/f30cbc3e3ee10cfa838b13cd.htmlLNK2005“符号已定义”错误及Linker工作原理2006-10-24 17:44 许多Visual C++的使用者都
  • (转)编译器原理

    千次阅读 2009-12-03 15:51:00
    预处理器-编译器-汇编器-链接器预处理器会处理相关的预处理指令,一般是以"#"开头的指令。如:#include "xx.h" #define等。编译器把对应的*.cpp翻译成*.s文件(汇编语言)。汇编器则处理*.s生成对应的*.o文件(obj目标...
  • 1. 预处理 此时编译器会扫描一个一个的C源文件,如果发现#include了某个.h文件,则把整个.h文件包含进.c文件中,形成一个新的.c文件(实际的实现过程可能不是这样,但原理是这样的)。同时进行相关预处理指令的解释...
  • ![图片说明](https://img-ask.csdn.net/upload/201905/31/1559309462_359759.png)
  • 编译器编译内核原理以及其应用 上篇文档简要介绍了一下编译器的编译内核,当然介绍的很简单,没有深入进去,俗话说不深入怎么High,所以这里我们深入进去搞一下,看看里面都有些什么有用的东东。 这里以市面上用的...
  • 编译原理编译器的前端技术

    千次阅读 2020-08-30 09:23:05
    文章目录编译器的前端技术 编译器的前端技术 “前端”指的是编译器对程序代码的分析和理解过程。它通常只跟语言的语法有关,跟目标机器无关。而与之对应的“后端”则是生成目标代码的过程,跟目标机器有关。 ...
  • 当时我们编译原理课程实践是PL/0语言编译器扩展,在原有PL/0语言文法进行扩展。我写这次博文一是为了回忆以前学的知识,加深记忆;二是和大家分享一下,希望可以互相学习,想对一些新手有所帮助。 首先,我先简单...
  • class Circle {  double x,y,r;... public Circle(double x,double y,double r) ... *因为你在Circle类里...编译器会自动检测这种继承关系是否正确,不正确就会报错,而不是说在运行时new出父类就可以的
  • 什么是GCC Linux下编译命令make,make install,./configure ...GNU编译器套件包括C、C++、Objective-C、Fortran、Java、Ada和Go语言前端,也包括了这些语言的库(如libstdc++,libgcj等。) GCC...
  • 编译器编译原理详解 这里并没不是讨论大学课程中所学的《编译原理》,只是写一些我自己对C++编译器及链接器的工作原理的理解和看法吧,以我的水平,还达不到讲解编译原理(这个很复杂,大学时几乎没学明白)。 要...
  • 报错:当遇到状态栈顶为某一状态下出现不该遇到的文法符号时,则报错,说明输入串不是该文法能接受的句子。 2.2.2 YACC 的文件格式 YACC 的文件格式分为三个部分: ...definitions...( % {} % )...
  • error,编译器报错,停止工作。 3.C++注意事项 ​C语言的扩展名.c,C++就是cpp 调用system,可以在c语言的代码中执行另外一个程序 system的返回值就是被调用程序中main函数的return值 不同平台c语言的...
  • 本文介绍了java虚拟机所能运行的基础指令,同时讲解了虚拟机是如何基于堆栈和队列配合相关基础指令,...最后我们给出了一段C语言代码,并详细讲解了我们的编译器如何把代码编译成能在java虚拟机上执行的java汇编语言
  • 编译原理C-编译器源代码,川大,绝对原创!
  • 编译原理 Tiny编译器和TM虚拟机

    千次阅读 2019-10-24 09:14:06
    编译器的设计流程 词法分析 语法分析 语义分析 Lex & Yacc Lex 首先,lex和yacc是开源工具,帮助开发者实现语法,词法分析。如果作为一个开发者去使用它们,就需要阅读它们的说明书,直到你会用,一句话,就是...
  • 本部分取自编译原理实验一:词法分析器生成工具FLEX在TINY语言上的应用。 我们知道,TINY是一门高级程序语言。TINY有它的词法规则和语法规则。TINY既然是一门高级程序语言,就要有TINY语言的编译器。TINY语言的...
  • 一个编译器的结构分为分析部分(编译器的前端)和综合部分(编译器的后端)。 编译器的前端:把源程序分解成为多个组成要素,并在这些要素之上加上语法结构。然后,它使用这个结构来创建该源程序的一个中间表示。如...
  • 解释器: 一段逐条解释源程序并结合输入产生输出的程序,有源程序报错功能而且比编译器效果好。 对于编译型语言分为两步:1. 源程序——编译器——目标程序 2. 输入——目标程序——输出 对于解释性语言只需要一步...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 35,357
精华内容 14,142
关键字:

编译器报错的原理