编译器构造及程序设计语言基础

本文主要内容:
1、编译器构造
2、程序设计语言基础

1、编译器构造

1、什么是编译器

编译器就是一个程序,它可以阅读以某一种语言编写的程序,并把该程序翻译成为一个的等价的、用另一种语言编写的程序。解释器是另一种常见的语言处理器。它并不通过翻译方式生产目标程序。而是直接利用用户的输入执行源程序中指定的操作。编译器更快翻译,解释器错误诊断效果通常比编译器更好。

2、编译器编译的步骤

1. 词法分析:
词法分析器读入组成源程序的字符流,并将它们组织称为有意义的词素的序列。

2. 语法分析:
根据词法分析器生成的各个词法单元的第一个分量来创建树形的中间表示。该中间表示给出了词法分析产生的词法单元流的语法结构。一个常用的表示方法是语法树。树中的每个内部节点表示一个运算,而该节点的子节点表示该运算的分量。
3. 语义分析:

使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。并把这些信息存放在语法树或符号表中,以便在随后的中间代码生成过程中使用。一个重要的部分就是类型检查。编译器检查每个运算符是否具有匹配的元算分量。比如说,数组下标必须是正整数程序设计语言可能允许某些类型的转换,这被称为自动类型转换。比如,一个二元算数符支持整数和浮点数运算,那么在整数和浮点数运算时,这个过程自动把整数转换成一个浮点数。

4. 中间代码生成:

在把一个源程序翻译称为目标代码的过程中,一个编译器可能构造出一个或多个中间表示。这些中间表示可以有多种形式。语法树是其中一种,常用在语法分析和语义分析中使用。在这2个分析之后,很多编译器生成一个明确的低级的或类机器语言的中间表示。这种中间表示具备两个重要性质:它应该易于生成,且能够被轻松地翻译为目标机器的语言。

5. 代码优化:

机器无关的代码优化步骤试图改进中间代码,以便生成更好的代码。”更好”通常以为着更快,但也有可能是更短或者更低耗的目标代码。比如把60从整数转换为浮点数的运算可以在编译时刻一劳永逸地完成。因此用浮点数60.0来替代证书60就可以消除相应的inttofloat运算。不同的编译器所做的代码优化工作量相差很大。那些优化工作做得最多的编译器,即所谓的”优化编译器”,会在优化阶段花相当多的时间。有些简单的优化方法可以极大地提高目标程序的运行效率而不会降低编译的速度。

6. 代码生成:
代码生成器以源程序的中间表示形式作为输入,并把它映射到目标语言。如果目标语言是机器代码,那么就必须为程序使用的每个变量选择寄存器或内存位置。

2、程序设计语言基础

这里讨论一下在程序设计语言的研究中出现的最重要的术语和他们的区别。

1、静态和动态的区别

在设计一个编译器的时候,我们需要面对的一个重要的问题之一是编译器能够对一个程序做出哪些判定。如果一个语言使用的策略支持编译器静态决定一个问题,那么我们就说语言使用了一个静态(static)策略,另一方面如果一个语言支持在程序运行期进行做出决定的策略称为动态策略。很明显Java在这个方面都有所支持。我们需要注意的另一个问题是声明的作用域。x的一个声明作用域是指程序的一个区域,在其中对x的使用都指向这个声明。如果仅能通过阅读程序就可以确定一个声明的作用域,那么这个语言使用的是静态作用域。否则这个语言使用的是动态作用域。大部分语言(比如Java、C)采用的都是静态作用域。

在Java中,一个变量是用于存放数据值的某个内存位置的名字。这里的static指的并不是变量的作用域,而是编译器确定用于存放被声明变量的内存位置的能力。
例子:public static int x;
上述x成为类变量,不论创建了多少个类对象,只存在一个x的拷贝。此外,编译器可以确定内存中被用于存放整数x的位置。反之,如果x是对象级的变量,那么类的每个对象都会有它自己的用于存放x的位置,编译器没办法在程序运行之前预先确定所有这些位置。

2、环境与状态

在程序语言设计中我们必须了解的另一个重要区别就是在程序运行过程中发生的改变是否会影响数据的值,还是仅仅影响的对那个值的映射(变量)。在Java中我们经常口口到来,”变量有没有变化”,这包括了”变量的值,或者是变量(内存位置)”
环境:是从一个变量名到存储位置的映射。即变量指的就是内存位置。(C语言中称为左值)
状态:是从一个内存位置到它的值的映射。(C语言中称为右值)

3、静态作用域和块结构
看了半天感觉其实就是Java里的作用域的意思。不同的程序语言有自己的作用域规则。

4、显示访问控制

诸如public\private\protected这样的关键字的使用,进行作用域的控制。

这里插个小曲:
在程序设计语言概念中,声明告诉我们事物的类型,而定义是告诉我们它们的值。

5、动态作用域

动态作用域解析是多态过程必不可少的。所谓多态过程是指对于同一个名字根据参数类型具有两个或者多个定义的过程。在某些语言中,人们可以静态地确定名字所有使用的类型。而在其他语言中, 比如Java和C++中,编译器有时不能做出这样的决定。

一个经典的例子如下:
(1).有一个类C,它有一个名字为m()方法
(2).D是C的一个子类,而D有一个它自己的名字为m()的方法
(3).有一个形如x.m()的对x的调用,其中x是类C的一个对象
正常情况下在编译期不可能指出x的指向是D类还是C类的对象。如果这个方法被多次应用,那么很可能某些调用作用在由x指向的类C的对象,而不是类D的对象,而其他调用最用于类D的对象之上。只有到了运行期才能决定应到调用m的哪个定义。因此,编译器生成的代码必须决定对象x的类,并调用其中某一个名字为m的方法。

《Java编程思想》中提到的动态绑定:
将一个方法调用同一个方法主体连接到一起就称为“绑定”( Binding)。若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何程序化语言里都是不可能的。 C 编译器只有一种方法调用,那就是“早期绑定”。“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:
它们都要在对象中安插某些特殊类型的信息。Java 中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定—— 它是自动发生的。
为什么要把一个方法声明成 final 呢?正如上一章指出的那样,它能防止其他人覆盖那个方法。但也许更重要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可为 final 方法调用生成效率更高的代码。

6、参数传递机制

程序设计语言中方法参数的传递方式:

  • 引用调用(call by reference):表示方法接收的是调用者提供的变量地址。
  • 值调用(call by value):表示方法接收的是调用者提供的值。
  • 命名调用(call by name):已经成为历史。

我们这里谈谈Java的值调用。Java中数组、字符串、所有类的对象都是值调用。什么是值调用呢,举个例子,有一个数组a,且它被以方法调用,传递给相应的形式参数(即入参)x,那么像x[2] = i这样的赋值语句实际上是改变了数组a[i],原因是xa的值的一个拷贝,而这个值实际上是一个指向数组a存储地址的指针(即引用)。

Java使用值调用,而且只有值调用。也就是说方法得到的是参数值的一个拷贝,并不是参数值本身,所以,方法不能修改传递给它的的任何参数变量本身。

看下面代码:

public class test {
  public static void main(String[] args) {
    int value= 10;
    changeValue(value);
    System.out.println(value);
  }
  public static void changeValue(int x){
    x = x * 3;
  }
}
输出:10

一个方法不可能修改一个基本数据类型的参数。而对象引用作为参数就不同了。这时,方法得到一个对象引用的拷贝。对象引用和其拷贝,同时引用着一个对象。

看下面代码:

public class test {
  public static void main(String[] args) {
    Circle c = new Circle();
    c.r = 1;
    bigger(c);
    System.out.println(c.r);
  }

  public static void bigger(Circle c2){
    C2.r = c2.r+3;
  }
}

class Circle{
  int r;
}
输出:4

很多程序设计语言(特别是c++和Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员认为java程序设计语言对对象采用的是引用调用,实际上这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例,来详细阐述一下这个问题。

首先编写一个交换两个圆对象的方法:

public static void swap(Circle x,Circle y){
  Circle temp = x;
  x = y;
  y = temp;
}

如果java程序设计语言对对象采用的是引用调用的话,这个方法应该能够实现交换数据的效果:

Circle a = new Circle(1);
Circle b = new Circle(2);
swap(a,b);
System.out.println(a.r);
System.out.println(b.r);
输出:
1
2

但是,方法并没有改变存储在变量ab中的对象引用。Swap方法的参数xy被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝,ab中的对象引用并没有变化。这个过程说明:Java对对象采用的不是引用调用,而是值传递。只不过可以传递值或者是引用而已,所以我们要分清引用和引用调用。

总结:

1. Java只有值调用,调用可以传入值的复制或者引用的复制,由于值的复制改变后不影响原来的值,所以Java基本类型传入方法后,对其值的复制进行修改,不会影响原来的值;而引用的复制和原引用都是指向同一个值的,其中一个通过指向对值修改了,另一个也会受到改变。由于值调用,要将整个原值拷贝到形式参数的空间。当参数很大的时候,这种拷贝代价很大,所以Java为了解决如数组、字符串和对象的参数传递问题,引入了对象引用的概念,复制对象引用,给人感觉就行实在引用调用一样。

2. C++中的”ref”是引用调用,如果理解了第一点,那么引用调用就很好理解了,传入的就是值的引用,和Java中要区别的是,Java中传入的是引用对象的复制。

7、别名

引用调用或者其他类似方法,比如Java中那样把对象的引用作为值传递,会有一个有趣的结果。有可能2个形式参数指向同一个值的位置,这样的变量称为另一个变量的别名(alias)。看一下下面的例子。对引用对象a的修改,使得引用对象b的调用也发生了改变,其实原理还是之前说的,引用调用传入的是值的指针,我们对其修改,实质上是直接修改了值。


public class Test {
    public static void main(String[] args) {
        int[] arr = new int[]{1, 4, 2, 29};
        test(arr, arr);
    }

    public static void test(int[] a, int[] b) {
        a[2] = 666;
        System.out.println(b[2]);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值