以下内容基于你上传的 Chapter14-1.pdf:Object-Oriented Languages, Chapter 14,按 第 1–18 页逐页整理:每页包含 原文翻译、页面描述、讲解;表格用 HTML,代码用 代码块,结构图尽量用 Mermaid。
Chapter 14 Object-Oriented Languages 逐页翻译与讲解
Page 1:Object-Oriented Languages / Chapter 14
1. 原文翻译
Object-Oriented Languages 面向对象语言
Chapter 14 第 14 章
2. 页面描述
这是章节封面页,只包含本章标题:Object-Oriented Languages。说明本章主题是面向对象语言,后续会讨论类、继承、字段布局、方法调用、动态分派等内容。
3. 讲解
这一章主要不是从“如何用面向对象写程序”的角度讲,而是从 编译器如何支持面向对象语言 的角度讲。也就是说,它关心的是:
- 类在语言语法中如何表示;
- 对象在内存中如何布局;
- 字段访问如何生成代码;
- 方法调用如何编译;
- 继承如何影响字段和方法;
- 动态方法为什么需要虚表,也就是 vtable。
可以把本章理解为: 面向对象语言特性在编译器和运行时中的实现机制。
Page 2:Object-Oriented Languages
1. 原文翻译
Object-Oriented Languages 面向对象语言
-
Class-based, object-oriented (OO) language 基于类的面向对象语言
-
All (or most) values are objects 所有值,或者大多数值,都是对象。
-
An object is an instance of a class 对象是类的一个实例。
-
Objects encapsulate state (fields) and behavior (methods) 对象封装状态和行为。状态对应字段,行为对应方法。
-
Some important features of OO languages 面向对象语言的一些重要特性:
-
Inheritance 继承
-
Encapsulation 封装
-
Polymorphism 多态
2. 页面描述
这一页介绍了面向对象语言的基本概念。重点有两个:
第一,面向对象语言通常是 class-based,也就是以类为核心组织程序。对象是类的实例。
第二,对象由两部分组成:
- fields:字段,用来保存状态;
- methods:方法,用来定义行为。
页面最后列出面向对象语言的三个核心特性:继承、封装、多态。
3. 讲解
这页是整章的基础。可以用下面这个模型理解:
flowchart TD
Class[Class 类] --> Object[Object 对象]
Object --> Fields[Fields 字段 / 状态]
Object --> Methods[Methods 方法 / 行为]
OO[OO Language 面向对象语言] --> Inheritance[Inheritance 继承]
OO --> Encapsulation[Encapsulation 封装]
OO --> Polymorphism[Polymorphism 多态]
其中:
| 概念 | 英文 | 含义 | 例子 |
|---|---|---|---|
| 类 | Class | 对象的模板,定义对象有哪些字段和方法 | Car, Vehicle, Truck |
| 对象 | Object | 类的实例,运行时真实存在的数据结构 | new Car 创建出的某辆车 |
| 字段 | Field | 对象内部保存的数据 | position, passengers |
| 方法 | Method | 对象可以执行的操作 | move(), await() |
| 继承 | Inheritance | 子类自动拥有父类的字段和方法 | Car extends Vehicle |
| 封装 | Encapsulation | 把状态和行为绑定到对象内部 | 对象自己管理自己的字段 |
| 多态 | Polymorphism | 同一个调用在运行时可能执行不同子类的方法 | v.move() 可能调用 Car_move 或 Truck_move |
这一章后面最关键的问题是: 如果一个变量静态类型是父类,但运行时指向子类对象,那么字段和方法到底怎么找?
Page 3:Outline
1. 原文翻译
Outline 大纲
-
Classes 类
-
Single Inheritance of Data Fields 数据字段的单继承
-
Multiple Inheritance 多继承
-
Testing Class Membership 测试类成员关系 / 判断对象是否属于某个类
-
Private Fields and Methods 私有字段和方法
2. 页面描述
这一页是本章大纲。它列出了本章要讲的五个主题:
- 类;
- 数据字段的单继承;
- 多继承;
- 类成员关系测试;
- 私有字段和方法。
3. 讲解
这页相当于路线图。整章逻辑可以概括为:
flowchart TD
A[Classes 类] --> B[Single Inheritance of Data Fields 单继承字段布局]
B --> C[Multiple Inheritance 多继承]
C --> D[Testing Class Membership 判断对象属于哪个类]
D --> E[Private Fields and Methods 私有字段和方法]
本 PDF 目前展示到第 18 页,主要覆盖了前两个部分:
- Classes
- Single Inheritance of Data Fields
- 以及动态方法调用的基本实现。
后面大纲中的多继承、类成员测试、私有字段方法,在这个 PDF 的后续部分可能会继续讲。
Page 4:Outline
1. 原文翻译
Outline 大纲
-
Classes 类
-
Single Inheritance of Data Fields 数据字段的单继承
-
Multiple Inheritance 多继承
-
Testing Class Membership 测试类成员关系
-
Private Fields and Methods 私有字段和方法
2. 页面描述
这一页和 Page 3 内容基本相同,但是 Classes 被加粗,表示接下来进入第一部分:Classes 类。
3. 讲解
这一页是章节切换页,用来告诉听众: 现在开始讲第一个主题 Classes。
从编译器角度看,“类”不仅仅是语法概念,它还会影响:
- 类型系统;
- 作用域规则;
- 对象创建;
- 字段访问;
- 方法调用;
- 继承关系;
- 运行时对象布局。
下面几页会用一种扩展语言 Object-Tiger 来说明如何给 Tiger 语言加入面向对象机制。
Page 5:Object-Tiger
1. 原文翻译
Object-Tiger
- Extend the Tiger language with new declaration syntax to create classes: 扩展 Tiger 语言,加入新的声明语法,用来创建类:
dec → classdec
classdec → class class-id extends class-id { {classfield } }
classfield → vardec
classfield → method
method → method id(tyfields) = exp
method → method id(tyfields) : type-id = exp
2. 页面描述
这一页开始介绍 Object-Tiger,也就是在 Tiger 语言基础上加入面向对象扩展。
新增的语法包括:
- 类声明;
- 类继承;
- 类字段;
- 方法声明;
- 带返回类型的方法声明。
3. 讲解
这一页讲的是 语法扩展。
原始 Tiger 语言中有变量声明、函数声明、类型声明等。现在为了支持面向对象,需要加入类声明:
class B extends A {
...
}
意思是:
- 声明一个类
B; B继承自类A;- 类体
{ ... }中可以包含字段和方法。
可以把语法结构拆开看:
| 语法规则 | 含义 |
|---|---|
dec → classdec |
声明可以是一种类声明。 |
classdec → class class-id extends class-id { {classfield } } |
类声明由 class 关键字、类名、extends、父类名和类体组成。 |
classfield → vardec |
类成员可以是变量声明,也就是字段。 |
classfield → method |
类成员也可以是方法。 |
method → method id(tyfields) = exp |
方法可以没有显式返回类型,由表达式决定。 |
method → method id(tyfields) : type-id = exp |
方法也可以显式声明返回类型。 |
用一个例子表示就是:
class Vehicle extends Object {
var position := 0
method move(x: int) =
position := position + x
}
这表示:
Vehicle是一个类;- 它继承自
Object; - 它有一个字段
position; - 它有一个方法
move。
Page 6:Object-Tiger
1. 原文翻译
Object-Tiger
-
class B extends A { ... } -
声明一个新类
B,它扩展了类A。 - 它必须位于声明
A的let-expression的作用域中。 A的所有字段和方法都会隐式属于B。A的某些方法可以在B中被重写,也就是有新的声明。参数类型和返回类型必须完全相同。-
但是字段不能被重写。
-
有一个预定义的类标识符
Object,它没有字段,也没有方法。 -
类
B中的每个方法都有一个隐式的形式参数self,类型为B。 -
self不是保留字,只是一个在每个方法中自动绑定的标识符。
2. 页面描述
这一页解释 Object-Tiger 中的继承规则。
重点包括:
- 子类继承父类的字段和方法;
- 子类可以重写父类方法;
- 子类不能重写父类字段;
- 所有类最终都继承自预定义类
Object; - 每个方法中都有隐式参数
self。
3. 讲解
这一页很重要,因为它定义了 Object-Tiger 的类语义。
3.1 类继承
class B extends A {
...
}
表示:
classDiagram
Object <|-- A
A <|-- B
B 会自动拥有 A 的字段和方法。
例如:
class A extends Object {
var x := 0
method f() = x := x + 1
}
class B extends A {
method g() = x := x + 2
}
这里 B 虽然没有显式声明字段 x,但它继承了 A.x,所以 B 的方法 g() 里可以使用 x。
3.2 方法可以重写
子类可以重新定义父类已有的方法:
class A extends Object {
method move(x: int) = ...
}
class B extends A {
method move(x: int) = ...
}
但是要求:
- 方法名相同;
- 参数类型相同;
- 返回类型相同。
也就是方法签名必须保持一致。
3.3 字段不能重写
如果父类有字段 position,子类不能再声明一个同名字段 position。
原因是:字段布局需要稳定。如果允许子类覆盖字段,字段偏移量会变复杂,编译器生成字段访问代码时就难以保证一致性。
3.4 self 的含义
每个方法都有一个隐式参数 self。例如:
class B extends A {
method f(x: int) =
self.g(x)
}
这里 self 表示当前对象,类似 Java 的 this。
注意这一页特别说:
self is not a reserved word
也就是说,self 不是语言关键字,而是编译器自动在每个方法中绑定的一个普通标识符。
Page 7:Object Tiger
1. 原文翻译
Object Tiger
-
新增表达式语法,用来创建对象和调用方法:
-
new B b.xb.f(x, y):这里的值b会作为方法f的隐式self参数。
exp → new class-id
→ lvalue . id()
→ lvalue . id(exp{, exp})
2. 页面描述
这一页介绍 Object-Tiger 中的新表达式语法:
new B:创建类B的对象;b.x:访问对象b的字段x;b.f(x, y):调用对象b的方法f,并传入参数x, y。
3. 讲解
这一页从“声明类”转到“使用类”。
3.1 创建对象
new B
表示创建一个类 B 的实例。
可以理解为:
flowchart LR
ClassB[Class B] --> NewB[new B]
NewB --> ObjB[Object of B]
3.2 字段访问
b.x
表示访问对象 b 的字段 x。
编译器要做的事情是:
- 找到
b指向的对象; - 确定字段
x在对象内存布局中的偏移量; - 从对应偏移位置取出字段值。
3.3 方法调用
b.f(x, y)
表面上看传入了两个参数 x 和 y,但实际上还隐式传入了 b:
f(self = b, x, y)
所以 b.f(x, y) 可以理解为:
B_f(b, x, y)
其中 b 是隐式的 self。
3.4 新表达式语法总结
| 表达式 | 含义 | 编译器需要做什么 |
|---|---|---|
new B |
创建 B 类对象 | 分配对象内存,初始化字段,设置类描述符 |
b.x |
访问对象字段 | 根据字段偏移量读取对象内存 |
b.f(x, y) |
调用对象方法 | 传入 self,并根据静态或动态方法规则找到目标代码地址 |
Page 8:An Object-Oriented Program
1. 原文翻译
An Object-Oriented Program 一个面向对象程序
let start := 10
class Vehicle extends Object {
var position := start
method move(int x) =
(position := position + x)
}
class Truck extends Vehicle {
method move(int x) =
if x <= 55
then position := position + x
}
class Car extends Vehicle {
var passengers := 0
method await(v: Vehicle) =
if (v.position < position)
then v.move(position - v.position)
else self.move(10)
}
...
in
...
end
页面右侧问题:
What are the variables in scope on entry to await?
进入 await 方法时,哪些变量在作用域中?
2. 页面描述
这一页给出一个 Object-Tiger 程序示例。
程序中有三个类:
Vehicle:父类,包含字段position和方法move;Truck:继承自Vehicle,重写了move方法;Car:继承自Vehicle,新增字段passengers,新增方法await。
页面提出问题:
进入 Car.await(v: Vehicle) 方法时,哪些变量在作用域中?
3. 讲解
3.1 类结构
classDiagram
Object <|-- Vehicle
Vehicle <|-- Truck
Vehicle <|-- Car
class Vehicle {
position
move(x)
}
class Truck {
move(x)
}
class Car {
passengers
await(v)
}
3.2 代码含义
Vehicle 类:
class Vehicle extends Object {
var position := start
method move(int x) =
(position := position + x)
}
说明:
Vehicle有字段position;- 初始值是外层变量
start; - 方法
move会让position增加x。
Truck 类:
class Truck extends Vehicle {
method move(int x) =
if x <= 55
then position := position + x
}
说明:
Truck继承Vehicle;- 它重写了
move; - 只有当
x <= 55时才移动。
Car 类:
class Car extends Vehicle {
var passengers := 0
method await(v: Vehicle) =
if (v.position < position)
then v.move(position - v.position)
else self.move(10)
}
说明:
Car继承Vehicle;- 它新增字段
passengers; - 它有方法
await,参数v的类型是Vehicle; - 如果
v.position < self.position,就让v追上自己; - 否则自己再移动 10。
3.3 进入 await 时作用域里有什么?
进入 await(v: Vehicle) 时,至少有:
| 变量 / 名称 | 来源 | 含义 |
|---|---|---|
self |
方法隐式参数 | 当前 Car 对象 |
v |
方法显式参数 | 传入的 Vehicle 对象 |
position |
从 Vehicle 继承来的字段 | 当前 Car 对象的 position,即 self.position |
passengers |
Car 自己声明的字段 | 当前 Car 对象的 passengers |
move |
从 Vehicle 继承的方法,也可能被动态分派 | 可以通过 self.move 调用 |
start |
外层 let 绑定 | Vehicle.position 初始化时使用的外层变量 |
不过需要注意:
在方法体中直接写 position,通常会被理解为 self.position。
所以:
if (v.position < position)
可以理解为:
if (v.position < self.position)
Page 9:An Object-Oriented Program (Cont)
1. 原文翻译
An Object-Oriented Program (Cont) 一个面向对象程序,续
let start := 10
...
class Car extends Vehicle {
var passengers := 0
method await(v: Vehicle) =
if (v.position < position)
then v.move(position - v.position)
else self.move(10)
}
var t := new Truck
var c := new Car
var v : Vehicle := c
in
c.passengers := 2;
c.move(60);
v.move(70);
c.await(t)
end
2. 页面描述
这一页继续上一页的程序,展示对象创建和方法调用。
关键代码包括:
var t := new Truck
var c := new Car
var v : Vehicle := c
以及执行部分:
c.passengers := 2;
c.move(60);
v.move(70);
c.await(t)
3. 讲解
这一页的核心是: 静态类型和运行时类型可能不同。
3.1 对象创建
var t := new Truck
var c := new Car
var v : Vehicle := c
含义:
| 变量 | 静态类型 | 运行时对象 |
|---|---|---|
t |
Truck | new Truck |
c |
Car | new Car |
v |
Vehicle | 同一个 Car 对象 |
也就是说:
var v : Vehicle := c
这里 v 的静态类型是 Vehicle,但是它实际指向的是一个 Car 对象。
可以画成:
flowchart LR
c[c : Car] --> carObj[Car object]
v[v : Vehicle] --> carObj
t[t : Truck] --> truckObj[Truck object]
3.2 执行语句分析
c.passengers := 2;
设置 Car 对象的 passengers 字段为 2。
c.move(60);
c 是 Car 类型,Car 没有重写 move,所以使用从 Vehicle 继承来的 move。
v.move(70);
这句最关键。
虽然 v 的静态类型是 Vehicle,但是运行时 v 指向的是 Car 对象。
如果 move 是动态方法,就需要根据运行时对象决定调用哪个方法。
因为 Car 没有重写 move,所以最终仍然调用 Vehicle.move。
c.await(t)
调用 Car.await,传入的参数是 Truck 对象 t。
由于 Truck 是 Vehicle 的子类,所以可以作为 Vehicle 类型参数传入。
3.3 这页想引出的问题
本页真正想引出的是:
v.position怎么访问?v.move(70)到底调用哪个方法?- 如果变量类型是父类,但对象实际是子类,编译器怎么生成代码?
这些问题对应后面几页的主题:字段布局和动态方法分派。
Page 10:Generate Code to Fetch Fields
1. 原文翻译
Generate Code to Fetch Fields 生成访问字段的代码
-
v.position -
v属于类Vehicle。 -
为了求值它,编译器必须生成代码,从
v指向的对象,也就是 record 中取出字段position。 -
怎么做?
-
一个简单想法:
-
从变量
v的环境条目中取得Vehicle的类描述符。 -
从这个描述符中取得
position的偏移量。 -
但是在运行时:
-
v可能包含一个指向Car或Truck的指针。 - 那么
position字段会在哪里?
2. 页面描述
这一页开始讨论编译器如何为字段访问生成代码。
例子是:
v.position
表面上看,v 的类型是 Vehicle,字段 position 属于 Vehicle。
但问题是:运行时 v 可能实际指向 Car 或 Truck 对象。
所以编译器必须保证无论 v 指向哪个子类对象,都能在相同偏移位置找到 position。
3. 讲解
这一页是从语义进入实现的关键页。
3.1 字段访问的核心问题
字段访问本质上是内存访问:
v.position
编译后可能类似:
MEM[v + offset(position)]
问题在于:
offset(position) 必须是多少?
3.2 如果没有继承,很简单
如果 Vehicle 对象布局固定:
Vehicle object:
+----------------+
| class desc ptr |
+----------------+
| position |
+----------------+
那么 position 的偏移就是固定的。
3.3 有继承后变复杂
如果:
class Car extends Vehicle {
var passengers := 0
}
那么 Car 对象既有 Vehicle 的字段,也有自己的字段:
Car object:
+----------------+
| class desc ptr |
+----------------+
| position |
+----------------+
| passengers |
+----------------+
只要 position 仍然放在和 Vehicle 一样的位置,字段访问就没问题。
这就引出下一节: single inheritance 中字段布局的 prefixing 方法。
Page 11:Outline
1. 原文翻译
Outline 大纲
-
Classes 类
-
Single Inheritance of Data Fields 数据字段的单继承
-
Multiple Inheritance 多继承
-
Testing Class Membership 测试类成员关系
-
Private Fields and Methods 私有字段和方法
2. 页面描述
这一页再次显示大纲,但这次加粗的是:
Single Inheritance of Data Fields
说明接下来进入第二部分: 数据字段的单继承实现。
3. 讲解
前面已经讲完类的语法和基本语义,现在开始讲编译器实现问题:
在单继承语言中,子类对象应该如何布局字段,才能让父类字段访问始终有效?
核心目标是: 如果一个变量静态类型是父类,那么它访问父类字段时,即使运行时对象是子类,也能用同一个偏移量找到字段。
这要求子类对象的前半部分必须和父类对象兼容。
Page 12:Single Inheritance
1. 原文翻译
Single Inheritance 单继承
-
Single-inheritance languages: 单继承语言:
-
每个类只扩展一个父类。
-
对于单继承语言,如何生成获取字段的代码?
2. 页面描述
这一页定义单继承语言,并提出问题:
如果每个类只有一个父类,那么字段访问代码该如何生成?
3. 讲解
单继承的结构是一棵树:
classDiagram
Object <|-- A
A <|-- B
A <|-- C
B <|-- D
每个类只有一个直接父类,因此对象字段布局可以沿着父类链逐步扩展。
这使得一个简单有效的策略成为可能:
子类对象开头部分完全复制父类字段布局,然后把自己的新字段追加到后面。
这个策略叫 prefixing,下一页会具体解释。
Page 13:Fields
1. 原文翻译
Fields 字段
-
Prefixing 前缀布局 / 前缀化
-
当
B继承A时,B中那些从A继承来的字段,会被放在B的 record 的开头,并且顺序和它们在Arecord 中出现的顺序相同。 -
B中没有从A继承来的字段,会被放在后面。
代码:
class A extends Object {
var a := 0
}
class B extends A {
var b := 0
var c := 0
}
class C extends A {
var d := 0
}
class D extends B {
var e := 0
}
2. 页面描述
这一页介绍单继承下字段布局的核心方法:prefixing。
页面下方的图展示了对象布局:
A: a
B: a, b, c
C: a, d
D: a, b, c, e
也就是说,子类对象的开头总是父类字段。
3. 讲解
3.1 prefixing 是什么?
如果 B extends A,那么 B 对象的字段布局必须以 A 的字段布局作为前缀。
例如:
class A extends Object {
var a := 0
}
class B extends A {
var b := 0
var c := 0
}
那么布局是:
A object:
+---+
| a |
+---+
B object:
+---+
| a | <- inherited from A
+---+
| b |
+---+
| c |
+---+
3.2 为什么要这样做?
因为如果一个变量的静态类型是 A,它可能指向 A、B、C、D 的对象。
只要所有子类对象中,字段 a 都在同一个位置,那么访问 a 的代码就不需要关心运行时对象到底是哪一个类。
3.3 本页例子的字段布局
| 类 | 继承关系 | 字段布局 | 说明 |
|---|---|---|---|
A |
A extends Object |
a |
A 自己声明字段 a。 |
B |
B extends A |
a, b, c |
先放继承自 A 的 a,再放自己的 b 和 c。 |
C |
C extends A |
a, d |
先放继承自 A 的 a,再放自己的 d。 |
D |
D extends B |
a, b, c, e |
先放继承自 B 的 a,b,c,再放自己的 e。 |
3.4 字段布局图
A object:
+-----+
| a |
+-----+
B object:
+-----+
| a |
+-----+
| b |
+-----+
| c |
+-----+
C object:
+-----+
| a |
+-----+
| d |
+-----+
D object:
+-----+
| a |
+-----+
| b |
+-----+
| c |
+-----+
| e |
+-----+
3.5 重点结论
使用 prefixing 之后:
- 父类字段在所有子类对象中的偏移量都不变;
- 编译器可以根据静态类型生成字段访问代码;
- 不需要运行时动态查找字段;
- 单继承下字段访问可以非常高效。
Page 14:Methods
1. 原文翻译
Methods 方法
-
一个方法实例的编译方式很像函数。
-
它会变成机器代码,存放在指令空间中的某个特定地址。
-
例如,方法实例
Truck_move在机器代码标签Truck_move处有一个入口点。 -
每个类描述符包含一个指向父类的指针,同时也包含一个方法实例列表。
2. 页面描述
这一页开始讨论方法的编译。
它强调:
- 方法最终会被编译成机器代码;
- 每个方法有一个入口地址;
- 类描述符中不仅有父类指针,还会保存方法实例列表。
页面右边画了一个 instruction space,里面有一个标记为 Truck_move 的代码区域,表示方法代码在指令空间中的位置。
3. 讲解
3.1 方法和函数的关系
方法和普通函数很像,都会被编译成机器代码。
比如:
class Truck extends Vehicle {
method move(int x) =
if x <= 55
then position := position + x
}
可能被编译成类似:
Truck_move:
...
machine instructions
...
3.2 方法和函数的区别
方法比函数多一个隐式参数 self。
所以:
t.move(10)
可以理解为:
Truck_move(self = t, x = 10)
3.3 类描述符
每个类在运行时通常有一个 class descriptor,也就是类描述符。
类描述符中包含:
- 指向父类描述符的指针;
- 方法列表;
- 可能还包含字段信息、类名、类型信息等。
这一页明确说:
Each class descriptor contains a pointer to its parent class, and also a list of method instances.
可以表示为:
Class Descriptor for Truck:
+------------------------+
| parent -> Vehicle desc |
+------------------------+
| methods list |
| move -> Truck_move |
+------------------------+
用 Mermaid 表示:
flowchart LR
TruckDesc[Truck class descriptor] --> VehicleDesc[Vehicle class descriptor]
TruckDesc --> MethodList[Method list]
MethodList --> TruckMove[Truck_move code]
3.4 重点
字段访问主要依赖 对象内存布局。 方法调用主要依赖 方法代码地址 和 类描述符中的方法列表。
Page 15:Static Methods
1. 原文翻译
Static Methods 静态方法
-
一些面向对象语言允许某些方法被声明为
static。 -
为了编译形如
c.f()的方法调用,编译器: -
找到
c的类,假设它是类C; - 在类
C中搜索方法f,假设没有找到; - 搜索
C的父类,假设它是类B,以此类推; - 假设在某个祖先类
A中找到了静态方法f,那么编译器可以把它编译为对标签A_f的函数调用。
图示:
c.f() ---> A_f
2. 页面描述
这一页介绍静态方法调用的编译。
如果方法是静态的,编译器可以在编译时确定目标方法地址。
因此 c.f() 可以直接编译为调用某个固定标签,比如 A_f。
3. 讲解
3.1 静态方法的核心特点
静态方法调用在编译时就能确定调用目标。
例如,如果编译器知道:
c的静态类型是C;C没有f;- 父类
B没有f; - 祖先类
A有静态方法f;
那么:
c.f()
可以直接编译成:
call A_f
3.2 静态方法查找过程
flowchart TD
Call["c.f()"] --> C["search class C"]
C -->|not found| B["search parent class B"]
B -->|not found| A["search ancestor class A"]
A -->|found static f| AF["compile to call A_f"]
3.3 静态方法为什么简单?
因为它不需要考虑运行时对象的真实类型。
编译器只根据静态类型和继承链就可以决定:
c.f() -> A_f
3.4 静态方法调用与动态方法调用对比
| 方法类型 | 目标地址何时确定 | 是否需要运行时查找 | 例子 |
|---|---|---|---|
| 静态方法 | 编译时 | 不需要 | c.f() 直接变成 call A_f |
| 动态方法 | 运行时 | 需要 | c.f() 需要根据对象真实类决定调用谁 |
Page 16:Dynamic Methods
1. 原文翻译
Dynamic Methods 动态方法
-
如果
A中的方法f是一个动态方法,那么我们能不能把c.f()转换成A_f()? -
f可能在某个类D中被重写,而D是C的子类。 -
在编译时无法判断
c是指向类D的对象,还是指向类C的对象。如果是D的对象,就应该调用D_f;如果是C的对象,就应该调用A_f。 -
如何解决这个问题?
代码:
Class A extends Object {
var x := 0
method f()
}
Class B extends A {
method g()
}
Class C extends B {
method g()
}
Class D extends C {
var y := 0
method f()
}
c.f()
2. 页面描述
这一页指出动态方法调用的问题:
虽然 c 的静态类型可能是 C,但是运行时它可能指向 C 对象,也可能指向 D 对象。
而 D 重写了 f,所以调用 c.f() 时到底应该执行 A_f 还是 D_f,编译时无法确定。
3. 讲解
3.1 类继承结构
classDiagram
Object <|-- A
A <|-- B
B <|-- C
C <|-- D
class A {
x
f()
}
class B {
g()
}
class C {
g()
}
class D {
y
f()
}
3.2 方法来源
| 类 | 字段 | 方法 | 说明 |
|---|---|---|---|
A |
x |
f() |
A 定义 f。 |
B |
继承 x |
g(),继承 f() |
B 新增 g。 |
C |
继承 x |
重写 g(),继承 f() |
C 覆盖 B.g。 |
D |
x, y |
重写 f(),继承 C.g() |
D 覆盖 A.f。 |
3.3 为什么不能直接编译成 A_f?
如果写:
c.f()
而 c 的静态类型是 C,那么从继承链看,C 没有定义 f,它继承的是 A.f。
于是编译器可能想直接生成:
call A_f
但问题是,运行时 c 可能实际指向 D 对象。
因为:
var c : C := new D
这是合法的,因为 D 是 C 的子类。
如果运行时对象是 D,那么 D 重写了 f,应该调用:
D_f
而不是:
A_f
3.4 核心问题
动态方法调用的问题就是:
编译时只知道静态类型,运行时才知道真实对象类型。
所以需要一种机制,让程序在运行时根据对象真实类型选择方法。
这个机制就是下一页的:
dispatch vector / virtual table / vtable
Page 17:Dynamic Methods
1. 原文翻译
Dynamic Methods 动态方法
-
类描述符必须包含一个向量,也叫:
-
dispatch vector;
- virtual table;
- vtable。
这个向量中,对于每一个非静态方法名,都有一个方法实例。
- Prefixing:当类
B继承自A时,方法表以A已知的所有方法名的条目开始,然后继续添加B声明的新方法。
代码:
Class A extends Object {
var x := 0
method f()
}
Class B extends A {
method g()
}
Class C extends B {
method g()
}
Class D extends C {
var y := 0
method f()
}
2. 页面描述
这一页给出动态方法调用的解决方案: 每个类描述符中维护一个 vtable。
图中展示了:
A的方法表有A_f;B的方法表有A_f和B_g;C的方法表有A_f和C_g;D的方法表有D_f和C_g。
这说明:
- 如果子类没有重写方法,就沿用父类方法;
- 如果子类重写方法,就在相同表项位置替换成新的方法实现;
- 新增方法则追加到方法表后面。
3. 讲解
3.1 vtable 是什么?
vtable,全称 virtual table,也叫 dispatch vector。 它是一个方法指针数组,用于支持动态分派。
可以理解为:
class descriptor:
+----------------+
| parent pointer |
+----------------+
| vtable |
+----------------+
vtable:
+----------------+
| method f ptr |
+----------------+
| method g ptr |
+----------------+
3.2 方法表也使用 prefixing
字段布局有 prefixing,方法表也有 prefixing。
如果 B extends A,那么:
B的方法表前面部分和A的方法表兼容;- 如果重写父类方法,就在同一个槽位替换方法指针;
- 如果新增方法,就追加到后面。
3.3 本页例子的 vtable
| 类 | 字段布局 | 方法表 / vtable | 说明 |
|---|---|---|---|
A |
x |
[A_f] |
A 定义了 f。 |
B |
x |
[A_f, B_g] |
B 继承 f,新增 g。 |
C |
x |
[A_f, C_g] |
C 继承 f,重写 g。 |
D |
x, y |
[D_f, C_g] |
D 重写 f,继承 C.g。 |
3.4 字符画表示
A object:
+----------------+
| class desc ptr |
+----------------+
| x |
+----------------+
A vtable:
+-------+
| A_f |
+-------+
B object:
+----------------+
| class desc ptr |
+----------------+
| x |
+----------------+
B vtable:
+-------+
| A_f | <- inherited f
+-------+
| B_g | <- new g
+-------+
C object:
+----------------+
| class desc ptr |
+----------------+
| x |
+----------------+
C vtable:
+-------+
| A_f | <- inherited f
+-------+
| C_g | <- override g
+-------+
D object:
+----------------+
| class desc ptr |
+----------------+
| x |
+----------------+
| y |
+----------------+
D vtable:
+-------+
| D_f | <- override f
+-------+
| C_g | <- inherited g from C
+-------+
3.5 为什么 vtable 能解决动态调用?
假设:
c.f()
f 在 vtable 中的偏移位置是固定的,比如第 0 个位置。
那么无论 c 指向什么对象:
- 如果
c指向C对象,那么C的 vtable 第 0 项是A_f; - 如果
c指向D对象,那么D的 vtable 第 0 项是D_f。
所以运行时只要:
- 从对象中取出 class descriptor;
- 从 class descriptor 中取出 vtable;
- 在固定偏移位置取出方法指针;
- 调用这个方法指针。
这样就实现了动态分派。
Page 18:Dynamic Methods
1. 原文翻译
Dynamic Methods 动态方法
-
为了执行
c.f(),其中f是一个动态方法,编译后的代码必须执行以下指令: -
从对象
c的偏移量 0 处取出类描述符d。 - 从
d中f的固定偏移位置取出方法实例指针p。 - 跳转到地址
p,并保存返回地址,也就是调用p。
2. 页面描述
这一页总结动态方法调用的底层执行流程。
核心是三步:
object c -> class descriptor d -> method pointer p -> call p
这就是典型的 vtable 动态分派过程。
3. 讲解
3.1 动态调用的执行流程
对于:
c.f()
如果 f 是动态方法,那么不能直接编译成:
call A_f
而应该编译成运行时查找:
d = c[0]
p = d[offset(f)]
call p
3.2 用伪代码表示
# c.f()
d := MEM[c + 0] # 取出对象 c 的 class descriptor
p := MEM[d + offset(f)] # 从 vtable 中取出 f 对应的方法地址
CALL p # 调用该方法
3.3 用 Mermaid 表示
flowchart LR
CObj[Object c] -->|offset 0| Desc[Class Descriptor d]
Desc -->|offset of f| Ptr[Method Pointer p]
Ptr --> Call[call p]
3.4 对应内存结构
Object c:
+---------------------------+
| class descriptor pointer | offset 0
+---------------------------+
| field 1 |
+---------------------------+
| field 2 |
+---------------------------+
Class descriptor d:
+---------------------------+
| parent class pointer |
+---------------------------+
| method table / vtable |
+---------------------------+
vtable:
+---------------------------+
| method f pointer | fixed offset for f
+---------------------------+
| method g pointer |
+---------------------------+
3.5 为什么 offset(f) 是 constant?
因为方法表使用 prefixing。 父类中的方法在所有子类方法表中的位置保持不变。
例如:
A vtable: [A_f]
B vtable: [A_f, B_g]
C vtable: [A_f, C_g]
D vtable: [D_f, C_g]
f 一直在第 0 个槽位。
所以编译器可以把 offset(f) 编译为常量。
但是槽位里的内容可能不同:
C的第 0 项是A_f;D的第 0 项是D_f。
这就是动态分派。
全章前 18 页核心总结
1. 本章讲什么?
这 18 页主要讲:
| 主题 | 核心问题 | 解决方法 |
|---|---|---|
| 类 | 如何在 Tiger 中加入 class、method、field? | 扩展语法,加入 class declaration 和 method expression。 |
| 对象创建 | 如何创建对象? | 使用 new B。 |
| 字段访问 | 父类变量可能指向子类对象,字段偏移如何保持一致? | 字段布局使用 prefixing。 |
| 静态方法 | 方法目标能否编译时确定? | 如果是 static method,直接编译为固定标签调用。 |
| 动态方法 | 运行时对象类型不同,方法实现可能不同,怎么办? | 使用 class descriptor 和 vtable。 |
2. 最重要的两种 prefixing
字段 prefixing
A fields: a
B fields: a, b, c
D fields: a, b, c, e
作用: 保证父类字段在子类对象中的偏移不变。
方法表 prefixing
A vtable: [A_f]
B vtable: [A_f, B_g]
C vtable: [A_f, C_g]
D vtable: [D_f, C_g]
作用: 保证同一个方法名在所有相关类的 vtable 中偏移不变。
3. 字段访问 vs 方法调用
| 操作 | 例子 | 依赖结构 | 是否动态 |
|---|---|---|---|
| 字段访问 | v.position |
对象字段布局 | 通常偏移静态确定 |
| 静态方法调用 | c.f() |
编译时类层次查找 | 否 |
| 动态方法调用 | c.f() |
class descriptor + vtable | 是 |
4. 最核心的一句话
这部分的核心可以概括为:
单继承面向对象语言通过 字段前缀布局 保证父类字段访问稳定,通过 方法表前缀布局与 vtable 支持动态方法分派。
英文关键词可以记成:
Single inheritance uses prefixing for field layout,
and vtables for dynamic method dispatch.
中文解释:
单继承语言中,子类对象以前缀方式保留父类字段布局;
动态方法调用则通过对象的类描述符找到虚表,再从虚表中取出真正要调用的方法地址。