跳转至

12 Object-Oriented Languages

面向对象语言 (Object-Oriented Languages) 的编译器需要将类、继承、方法重写等高层抽象高效地映射到机器代码和数据布局上。核心挑战是:如何在运行时以最低开销实现动态方法分派、字段访问和类型检测。

12.1 Core Concepts

基于类的面向对象语言有三个基本特征:

  • 继承 (Inheritance):类可以扩展另一个类,子类自动拥有父类的所有字段和方法,并可重写方法以精化行为。
  • 封装 (Encapsulation):对象的内部状态隐藏在公共接口之后,外部代码只能通过暴露的方法访问。私有性检查在编译时完成,不影响运行时性能。
  • 多态 (Polymorphism):同一方法调用可根据对象的运行时实际类型执行不同代码。典型实现机制为虚函数表 (VTable)。

Object-Tiger 语法:在标准 Tiger 基础上增加了类声明、继承、对象创建和方法调用。

// 类声明
dec  → classdec
classdec → class id extends id { classfield* }
classfield → vardec
           | method
method → method id ( tyfields ) = exp
       | method id ( tyfields ) : type-id = exp

// 对象表达式
exp → new id                    // 创建对象
    | lvalue . id ( )           // 无参方法调用
    | lvalue . id ( exp {, exp} )  // 带参方法调用

继承规则

  • 父类的所有字段和方法隐式属于子类,无需重复声明。
  • 方法可以重写 (Override),子类用同签名方法覆盖父类实现。
  • 字段不可以重写——子类不能声明与父类同名字段。
  • 重写方法必须保持与父类相同的参数类型和返回类型(签名一致)。

Self 参数:每个方法隐式携带一个 self 参数,运行时自动绑定到接收者对象(即方法调用中 . 左侧的对象)。self 不是关键字——它只是一个约定名称的隐式形参,在方法体中引用当前对象。

编译 Object-Tiger 程序需要解决三个核心问题:

  • 字段布局 (Field Layout):如何将对象的字段排布在内存中,使得字段访问高效。
  • 方法分派 (Method Dispatch):如何找到某个方法调用对应的具体实现。
  • 成员测试 (Membership Test):如何在运行时判断对象是否属于某类型(如 instanceof)。

后续章节依次展开这三方面的实现技术。

12.2 Single Inheritance

单继承是最简单的继承模型——每个类最多有一个父类,形成一棵类继承树。

12.2.1 Field Layout: Prefix Method

前缀法 (Prefix Method) 是单继承下字段布局的标准策略:子类对象的内存布局是父类布局的前缀——先按父类顺序排放字段,再追加子类新增字段。

优势:父类字段在所有子类中的偏移量完全一致。无论对象实际是 AB 还是 C,字段 a 始终在偏移量 4 处,编译器可以直接用 [obj + 4] 生成访问指令,无需任何运行时查表。

12.2.2 Method Dispatch

编译器按方法调用是否允许多态分为两种分派方式:

静态方法 (Static Dispatch):编译时根据变量声明类型确定调用目标,直接跳转到固定标签。

在语义分析阶段,编译器为每个变量建立指向其类描述符 (Class Descriptor) 的环境条目。类描述符中包含父类指针和方法实例列表,每个方法实例关联一个机器代码标签。

调用 c.f() 时: 1. 从变量 c 的声明类 C 开始查找方法 f 2. 若 C 中未找到,沿继承链向上查找父类 → 祖父类 → ... 3. 在祖先类 A 中找到后,直接生成 call A_f 指令

整个过程在编译时完成,不涉及运行时查表——调用的目标取决于变量的声明类型而非对象的实际类型。因此静态方法无多态、无运行时开销,等同于普通函数调用。

动态方法 (Dynamic Dispatch):运行时根据对象实际类型解析调用目标。核心机制是 VTable(虚函数表)

12.2.3 VTable

每个类的类描述符 (Class Descriptor) 中嵌入一个 VTable——一个按固定顺序排列的方法指针数组。

每个对象在头部存储一个指向其类 VTable 的指针。动态方法调用的机器代码序列:

// 调用 obj.m3() 的底层实现:
r1 ← obj->vtable_ptr        // ① 加载 VTable 地址 (1次 mem load)
r2 ← r1[m3_offset]          // ② 加载方法指针  (1次 mem load)
call r2                      // ③ 间接跳转

开销:2 次内存加载 + 1 次间接跳转。相比于静态调用(1 次直接跳转),额外开销可接受,且换来了多态的灵活性。

12.3 Multiple Inheritance

多继承下,一个类有多个父类。简单的前缀法失效——无法同时将所有父类的字段都放在子类开头。

12.3.1 Graph Coloring

将字段偏移量分配建模为图着色问题

  • 节点:字段名(如 a, b, c
  • :两个字段共存于同一个类中(它们需要不同的偏移量)
  • 颜色:内存偏移量

着色算法为每个字段分配一个不冲突的颜色(偏移量),保证任何类中共存的字段使用不同的偏移。

12.3.2* Hash Table (Dynamic Loading)

对于支持动态类加载的语言(如 Java),无法在编译时完成全程序分析。此时类描述符中存储键值对表:

  • Ktab (Key Table):字段名 / 方法名的哈希键
  • Ftab (Field/Function Table):对应的字段偏移或方法指针

访问字段时通过哈希查找获取偏移量。开销较大但支持运行时的类动态链接和反射。

12.4* Class Membership Testing

instanceof 和类型强制转换需要在运行时判断"对象是否是某类的实例"。

从对象的类描述符开始,沿 super 指针逐级向上查找目标类:

function instanceof(obj, targetClass):
    cls = obj->class_desc
    while cls != null:
        if cls == targetClass: return true
        cls = cls->super           // 沿继承链向上
    return false

缺点:时间复杂度 \(O(d)\)\(d\) 为继承深度),每次 instanceof 都要沿链查找,长继承链下性能差。

12.4.2 Display Array

预计算每个类的祖先数组 (Display Array),记录各深度的祖先类:

class A (depth 0):  Display = [A, null, null, ...]
class B extends A (depth 1):  Display = [A, B, null, ...]
class C extends B (depth 2):  Display = [A, B, C, null, ...]

判断算法

function instanceof(obj, targetClass):
    d = targetClass->depth
    return obj->class_desc->display[d] == targetClass

只需一次数组索引 + 一次指针比较——时间复杂度 \(O(1)\)。每个类的 display 数组在类加载时通过拷贝父类 display 并追加自身即可完成。

12.5 Private Members

私有性(private)纯粹是编译时概念

  • 编译器在类型检查阶段验证访问权限——检查当前代码是否有权访问目标字段/方法。
  • 一旦类型检查通过,生成的代码与公有成员完全相同——无任何运行时检查开销。
  • 私有字段在内存布局中依然遵循继承规则:子类对象包含父类的私有字段(占用空间),只是子类代码无法直接通过名字访问它们。
class A {
  private field secret: int
  public  method get(): int = self.secret
}
class B extends A {
  method get(): int = self.secret   // ★ 编译错误! 子类不能访问 A 的 private
}

这意味着封装是纯粹的编译期安全机制,不付出额外的运行时代价。

Comparison

静态方法 动态方法 字段访问
解析时机 编译时 (声明类型) 运行时 (实际类型) 编译时 (固定偏移)
实现机制 直接跳转 VTable + 间接跳转 [obj + offset] 直接寻址
运行时开销 1 次 jump 2×load + 1×indirect call 1×load
多态支持
单继承 多继承
字段布局 前缀法 — 简单高效 图着色 / 哈希表 — 复杂
方法分派 VTable 前缀继承 VTable 需额外调整 this 指针
成员测试 Display Array — O(1) 同样适用
工程语言 C#, Swift C++, Python

评论区

对你有帮助的话请给我个赞和 star => GitHub stars
欢迎跟我探讨!!!