Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

对象布局与方法调用(进阶)

复习

  • 语言运行时:提供对象模型等隐藏服务
  • 类型系统:类型约束运算
  • 程序在内存里的样子:数据如何布局

本章为进阶内容,零基础读者可以跳过,不影响后续阅读。

TL;DR

  • 对象在内存里由字段数据加类型信息组成
  • 方法调用需要根据对象的实际类型找到正确方法
  • 动态派发常借助类型信息(如虚表)实现
  • 布局设计影响性能与内存

正文

  面向对象语言里,我们写 对象.方法() 时,运行时到底做了什么?要回答它,先得知道对象在内存里长什么样。

对象里装了什么

  一个对象,除了它自己的字段数据,往往还要携带一点类型信息——因为它得知道自己“是什么类型”,才能在调用方法时找对地方。

  具体的布局,视语言而定,常见的组成部分有:

  • 各个字段的值
  • 一个指向类型信息的指针(有时叫虚表指针)
  • (在某些语言里)用于同步、哈希等的额外数据

方法调用:静态还是动态

  方法调用的“找方法”,有两种情形:

  • 静态派发:调用哪个方法,编译时就定死了(比如非虚方法)。直接生成调用,很快
  • 动态派发:调哪个方法,要看对象的实际类型,运行时才确定。比如父类引用指向子类对象,对象.方法() 该调子类重写的版本

  动态派发怎么实现?常见办法是虚表(vtable):类型信息里存一张方法地址表,每个类型一张。调用时,先从对象找到它的类型,再从这个类型的虚表里取出对应方法的地址,跳过去执行。

  这样,同一个调用点,随着对象实际类型的不同,就会跳到不同实现——这正是“多态”在机器层面的样子。

代价与设计

  动态派发很灵活,却也有成本:每次调用都要先查表、再跳转,比静态调用多几步;而且对象里要额外存类型信息,占一点空间。

  所以,语言和编译器会尽量在“能静态确定”的地方,就用静态派发,把动态派发留给真正需要多态的地方。能用确定信息时就用,省下运行时的开销——又是一次熟悉的权衡。

思考题 1

  对象在内存里通常包含哪些内容?

思考题 2

  “动态派发”为什么需要额外的类型信息?

小结

知识点

  • 对象通常由字段数据加类型信息组成
  • 方法调用分静态派发与动态派发
  • 动态派发常借助虚表按实际类型查找
  • 动态派发更灵活,但开销更大

参考资料

  1. Wikipedia(zh):虚函数表:支持动态派发的方法地址表
  2. Wikipedia(zh):动态分派:根据运行时类型选择方法

思考题答案(仅供参考)

思考题 1

  通常包含它各个字段的值,以及一份类型信息(常见形式是指向类型信息或虚表的指针),有的语言还会带上用于同步、哈希等的额外数据。这些使对象既保存数据,又能在运行时确定自己是什么类型。

思考题 2

  因为动态派发要在运行时根据对象的实际类型选择正确的方法实现。没有类型信息,就无从知道该调哪个版本,因此需要保存对象的类型(以及该类型对应的方法地址表),运行时据此查表跳转。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪