【小白打造编译器系列9】实现面向对象特性

面向对象的特点在于封装。

对象可以把 数据 和 对数据的操作 封装在一起,构成一个不可分割的整体,尽可能地隐藏内部的细节,只保留一些接口与外部发生联系。


面向对象的语义特征

我们从以下三个语义角度去理解面向对象的封装特性。

  • 从类型角度

       我们允许程序员可以创建自己的类型,提高程序的扩展性。

  • 从作用域角度
  1. 类的可见性。作为一种类型,他应该再整个程序范围内都是可见的。
  2. 对象的成员的作用域。对象的属性可以在整个对象内部访问,无论在哪个位置声明。
  • 从生存期的角度
  1. 对象的成员变量的生存期,一般跟对象的生存期是一样的。在创建对象的时候,就对所有成员变量做初始化,在销毁对象的时候,所有成员变量也随着一起销毁。
  2. 与类型绑定的成员(如 JAVA 的静态成员)的作用域是所有对象实例,被所有实例共享的。因此他的生存期是在任何一个对象实例创建之前就存在,在最后一个对象销毁之前不会消失。

设计与解析类的语法规则

下面定义了一个类的语法规则。

classDeclaration
    : CLASS IDENTIFIER
      (EXTENDS typeType)?
      (IMPLEMENTS typeList)?
      classBody
    ;

classBody
    : '{' classBodyDeclaration* '}'
    ;

classBodyDeclaration
    : ';'
    | memberDeclaration
    ;

memberDeclaration
    : functionDeclaration
    | fieldDeclaration
    ;

functionDeclaration
    : typeTypeOrVoid IDENTIFIER formalParameters ('[' ']')*
      (THROWS qualifiedNameList)?
      functionBody
    ;

语法规则的解释:

  • CLASS IDENTIFIER :类的声明是以关键字 class 开头。
  • (EXTENDS typeType)? (IMPLEMENTS typeList)? :判断是否继承或者实现。
  • classBody :跟上一个类的主体。
  • 类的主体里要声明类的成员。
  • 函数声明现在的角色是类的方法。

那具体的解析过程呢?

做完词法分析和语法分析之后,解释器会在语义分析阶段扫描 AST,识别出所有自定义的类型,以便在其他地方引用这些类型来声明变量。因为类型的声明可以在代码中的任何位置,所以最好用单独的一次遍历来识别和记录类型。

接着,我们在声明变量时,就可以引用这个类型了。语义分析的另一个工作,就是做变量类型的消解。比如,当我们声明“Bird bird = Bird(); ”时,需要知道 Bird 对象的定义在哪里,以便正确地访问它的成员。

在做语义分析时,要把类型的定义保存在一个数据结构中:

public class Class extends Scope implements Type{
    ...
}

public abstract class Scope extends Symbol{
    // 该Scope中的成员,包括变量、方法、类等。
    protected List<Symbol> symbols = new LinkedList<Symbol>(
}

public interface Type {
    public String getName();    //类型名称

    public Scope getEnclosingScope();
}

在这个设计中,我们看到 Class 就是一个 Scope(作用域),Scope 里面原来就能保存各种成员,现在可以直接复用,用来保存类的属性和方法,画成类图如下:(可参考:【小白打造编译器系列8】作用域和生存期:实现块作用域和函数

在做词法分析时,我们会解析出很多标识符,这些标识符出现在不同的语法规则里,包括变量声明、表达式,以及作为类名、方法名等出现。在语义分析阶段,我们要把这些标识符逐个识别出来,这个是一个变量,指的是一个本地变量;那个是一个方法名等。

变量、类和函数的名称,我们都叫做符号。编译过程中的一项重要工作就是建立符号表,它帮助我们进一步地编译或执行程序,而符号表就用上面几个类来保存信息。在符号表里,我们保存它的名称、类型、作用域等信息。对于类和函数,我们也有相应的地方来保存类变量、方法、参数、返回值等信息。

对象是怎么实例化的?

首先通过 构造方法 来创建对象。

在语法中,我们没有用 new 这个关键字来表示对象的创建,而是省略掉了 new,直接调用一个跟类名称相同的函数,这是我们独特的设计,示例代码如下:

Mammal mammal = Mammal("dog"); //playscript特别的构造方法,不需要new关键字
Bird bird = Bird();            //采用缺省构造方法

但在语义检查的时候,在当前作用域中是肯定找不到这样一个函数的,因为类的初始化方法是在类的内部定义的,我们只要检查一下,Mammal 和 Bird 是不是一个类名就可以了。再进一步,Mammal 类中确实有个构造方法 Mammal(),而 Bird 类中其实没有一个显式定义的构造方法,但这并不意味着变量成员不会被初始化。我们借鉴了 Java 的初始化机制,就是提供缺省初始化方法,在缺省初始化方法里,会执行对象成员声明时所做的初始化工作。对象做了缺省初始化以后,再去调用显式定义的构造方法,这样才能完善整个对象实例化的过程。

如何在内存里管理对象的数据?

C 语言的结构体 struct 和 C++ 语言的对象,都可以保存在 栈 里。保存在栈里的对象是直接声明并实例化的,而不是用 new 关键字来创建的。

如果用 new 关键字来创建,实际上是在堆里申请了一块内存,并赋值给一个指针变量,如下图所示:

当对象保存在堆里的时候,可以有多个变量都引用同一个对象,比如图中的变量 a 和变量 b 就可以引用同一个对象 object1。类的成员变量也可以引用别的对象,比如 object1 中的类成员引用了 object2 对象。对象的生存期可以超越创建它的栈桢的生存期。

如果对象保存在里,那么它的生存期与作用域是一样的,可以自动的创建和销毁,因此不需要额外的内存管理。缺点是对象没办法长期存在并共享

而在里创建的对象虽然可以被共享使用,却增加了内存管理的负担

访问对象的属性和方法

我们用点操作符来访问对象的属性和方法:

mammal.speak();                          //访问对象方法
println("mammal.name = " + mammal.name); //访问对象的属性

属性和方法的引用也是一种表达式,语法定义如下:

expression
    : ...
    | expression bop='.'
      ( IDENTIFIER       //对象属性
      | functionCall     //对象方法
      )
     ...
     ;

另外,对象成员还可以设置可见性。也就是说,有些成员只有对象内部才能用,有些可以由外部访问。这个怎么实现呢?

这只是个语义问题,是在编译阶段做语义检查的时候,不允许私有的成员被外部访问,报编译错误就可以了。


参考:《极客时间-编译原理之美》

发布了62 篇原创文章 · 获赞 34 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_41960890/article/details/105300542