对象布局与方法调用(进阶)
复习
- 语言运行时:提供对象模型等隐藏服务
- 类型系统:类型约束运算
- 程序在内存里的样子:数据如何布局
本章为进阶内容,零基础读者可以跳过,不影响后续阅读。
TL;DR
- 对象在内存里由字段数据加类型信息组成
- 方法调用需要根据对象的实际类型找到正确方法
- 动态派发常借助类型信息(如虚表)实现
- 布局设计影响性能与内存
正文
面向对象语言里,我们写 对象.方法() 时,运行时到底做了什么?要回答它,先得知道对象在内存里长什么样。
对象里装了什么
一个对象,除了它自己的字段数据,往往还要携带一点类型信息——因为它得知道自己“是什么类型”,才能在调用方法时找对地方。
具体的布局,视语言而定,常见的组成部分有:
- 各个字段的值
- 一个指向类型信息的指针(有时叫虚表指针)
- (在某些语言里)用于同步、哈希等的额外数据
方法调用:静态还是动态
方法调用的“找方法”,有两种情形:
- 静态派发:调用哪个方法,编译时就定死了(比如非虚方法)。直接生成调用,很快
- 动态派发:调哪个方法,要看对象的实际类型,运行时才确定。比如父类引用指向子类对象,
对象.方法()该调子类重写的版本
动态派发怎么实现?常见办法是虚表(vtable):类型信息里存一张方法地址表,每个类型一张。调用时,先从对象找到它的类型,再从这个类型的虚表里取出对应方法的地址,跳过去执行。
这样,同一个调用点,随着对象实际类型的不同,就会跳到不同实现——这正是“多态”在机器层面的样子。
代价与设计
动态派发很灵活,却也有成本:每次调用都要先查表、再跳转,比静态调用多几步;而且对象里要额外存类型信息,占一点空间。
所以,语言和编译器会尽量在“能静态确定”的地方,就用静态派发,把动态派发留给真正需要多态的地方。能用确定信息时就用,省下运行时的开销——又是一次熟悉的权衡。
思考题 1
对象在内存里通常包含哪些内容?
思考题 2
“动态派发”为什么需要额外的类型信息?
小结
知识点
- 对象通常由字段数据加类型信息组成
- 方法调用分静态派发与动态派发
- 动态派发常借助虚表按实际类型查找
- 动态派发更灵活,但开销更大
参考资料
- Wikipedia(zh):虚函数表:支持动态派发的方法地址表
- Wikipedia(zh):动态分派:根据运行时类型选择方法
思考题答案(仅供参考)
思考题 1
通常包含它各个字段的值,以及一份类型信息(常见形式是指向类型信息或虚表的指针),有的语言还会带上用于同步、哈希等的额外数据。这些使对象既保存数据,又能在运行时确定自己是什么类型。
思考题 2
因为动态派发要在运行时根据对象的实际类型选择正确的方法实现。没有类型信息,就无从知道该调哪个版本,因此需要保存对象的类型(以及该类型对应的方法地址表),运行时据此查表跳转。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪