你真的了解 C++ 对象模型吗?(对自己提问)
在 C 语言中,“数据”和“函数”是分开申明的,而在 C++ 中,数据和函数是一同申明在一个 class 中,称为抽象数据类型(ADT)。
C++ 面向对象模型被提出了很多中,最终最采纳的是如下这种:
- Nonstatic data members 放置在每一个 class object(类对象)内;
- static data members 放置在 class object(类对象)外,具体应该存放在全局静态区;
- static function members 和 non static members 都应该存放在代码段,不要因为 function 是 static 就误认为它存放于全局静态区;
- 如果有 virtual function,那么每个 class 都会产生一堆指向虚函数的指针,放在虚函数表中(virtual table),每一个 class object 都会被安插一个 vptr ,指向 virtual table。vptr 的设定都是由构造函数、拷贝构造函数、赋值运算符自动完成的。每一个 class 所关联的 RTTI(runtime type identification)也是由 virtual tabel 指出的(我所知道的 RTTI 信息在使用 dynamic_cast 的时候会用到),且通常放置在 virtual table 的第一个 slot 中。 具体的图片参考《深入理解C++对象模型》P10
对于继承来说,也有一种类似的 base table 模型:
- 每一个 class object 都拥有一个 bptr ,指向其 base class table;
- 每一个 base class table 的每一个 slot 都内含一个相关的 base class 地址。 具体图片参考《深入理解C++对象模型》P11
对象模型如何影响程序? 这就要从静态类型和动态类型谈起了,如果一个类型是静态类型,那么在编译期就可以确定它的行为,而如果一个类型是动态类型,那么只有在运行期才能知道它的行为。动态绑定是通过 base class ptr 来实现的。
比如下面这个例子:
x foobar() {
X xx;
X* px = new X;
// foo 是一个虚函数
xx.foo();
px->foo();
delete px;
return xx;
}
编译器会根据其类型处理,判断所访问静态类型的函数还是查虚函数表,对于 px ,就需要查寻函数表调用对应的函数。可以发现,C++对象模型影响了编译器对C++对象的解析。
C++ 对象模型支持三种程序范式:
- 程序模型,和 C 语言一样,我想这是继承 C 库所表现出来的形式;
- 抽象数据模型,由所谓的“抽象”和 public 接口提供,如为一个 class 实现 opeartor== ;
- 面向对象模型,通过一个抽象基类(提供接口),其子类继承基类的成员以及接口(可以直接继承,也可以扩展)
C++ 如何支持多态?
首先需要明白一点,C++ 的多态只存在于 public class 的继承体系之中,为什么?因为其余继承方式,会导致基类 public 部分变得不可访问,这就违背了多态的初衷,多态是想让子类继承其接口,再扩展其接口。
- 通过一组隐式的转换操作。如:将一个 derived class 指针转化成一个指向 public base type 的指针:
share* ps = new circle();
- 使用虚函数;
- 通过 dynamic_cast 和 typeid 来实现:
if (circle* pc = dynamic_cast<circle*>(ps) != nullptr)
...
老生常谈的问题:需要多少内存才能表示一个 class object ?
一个 class object 只包含 nonstatic members、vptr。其余的 member function存储在代码段,static data members 存放在静态存储区。所以一个 class object 占用多少内存取决于 nonstatic member 的数量以及类型,再加上一个 vptr 大小,最后还得考虑内存对齐(提高 CPU 的处理效率)。 具体图片参阅《C++对象模型》P28
最后我还想了解一下为什么可以通过指针(or 引用)就能在运行时找到正确的类型?
其实在内存结构上看,指针并无区别,都是指向地址,且指针本身的大小都是 4 Byte(32 bit),那为什么说多态可以依赖 base class type ptr 来实现呢?这就涉及到了编译器如何解释某个特定地址的内容和大小,一切都取决于编译器!
以上就是我读了《深入理解C++对象模型》的感悟。