Lippman早期在贝尔实验室,和C++发明者Bjarne Stroustrup设计了全世界第一套C++编译器cfront,还著有经典的C++入门书Ensential C++和C++ Primer。
全书基本以cfront的设计方法为基础,讨论编译器如何处理C++代码语意,看完C++ Primer只能学会C++语法,读完这本则可以了解C++面向对象的底层实现原理,简直刷新对C++的全新认识。
详细见: Inside the C++ Object Model, Stanley B. Lippman
1. 关于对象
封装成class后的空间布局成本:
- 成员函数在class内声明,但不出现在object中。
- 每一个非内联的成员函数只会诞生一个函数实例。
- 每一个拥有“拥有零个或一个定义”的内联函数会在每一个使用者身上产生一个函数实例。
因此封装没有带来空间或运行时的不良后果,实际上C++在layout以及access time上的额外代价是由virtual引起:
- 虚函数机制:实现运行时绑定。(需要保存vtbl和通过vtbl找函数地址)
- 虚基类机制:实现多次出现在继承体系中的基类,有一个单一而被共享的实例。(????)
简单对象模型:
- object内存放指向成员的指针,不存放成员。
- 牺牲空间和运行时的效率。解决成员类型不一致带来的存储空间不一致问题。
表格驱动对象模型:
- 需要两个表,数据成员表和成员函数表,数据成员表直接存放数据本身,成员函数表存放每个函数的地址。
- object存放这指向这两个表的指针。
- 成员函数表的思想可以支持虚函数实现。
C++对象模型
- Stroustrup设计,从简单对象模型派生而来。
- 非静态数据成员存放在每一个class object之内,静态数据成员存放在个别的class object之外。
- 静态和非静态的成员函数也存放在个别的class object之外。
- 虚函数则通过虚函数表vtbl和指向虚函数表的指针vptr实现:
- 每个class object有一个vptr,指向相关的vtbl。
- vptr的设定由类的构造/析构/拷贝函数完成。
- vtbl表第一项是每个class关联的type_info object(用来支持运行时类型识别RTTI),其他的每一项存放着指向虚函数的地址。
关于指针和引用:
- 一个指针,无论指向哪一种数据类型,指针本身的所需内存大小是固定的(一个机器地址大小)。
- 不同指向类型的指针的差异,在于其寻址出来的object类型不同。指针类型告诉编译器如何解释某个特定地址中的内存内容从及其大小。
- 因此指针的转换,本质上是编译器指令,并不改变一个指针的真正地址,改变的是编译器对被指内存的解释方式。
- 引用通常是编译器用一个指针实现的,但这个实现在语言层面对程序员做了透明化处理,所以指针和引用并没有本质的区别。
一个class object的大小包括:
- 非静态数据成员的总和大小。
- 由于对齐需要而填补的空间。
- 为了支持virtual而由内部产生的额外开销。
多态只能由指针或引用(而不能通过实例对象)来实现,根本原因在于:
- 指针和引用(通常以指针来实现)的大小是固定的(一个 word),而对象的大小却是可变的。其类的指针和引用可以指向(或引用)子类,但是基类的对象永远也只能是基类,没有变化则不可能引发多态。
- 一个指针或引用绝不会引发任何“与类型有关的内存委托操作”,在指针类型转换时会受到的改变的只有它们所指向内存的解释方式而已。(例如指针绝不会引发空间的slice,因为它们大小相同)