对象模型概述
C语言中,“数据”和“处理数据的操作(函数)”是分开定义的,也就是说语言本身并没有支持“数据与函数”之间的关联性。
C++中,一个类可能采用独立的“抽象数据类型(ADT)”来实现,或者以一个几层的类继承层次结构完成。
- 加上封装后的布局成本
加上了封装之后,布局成本增加了多少?答案是并没有增加成本。数据成员直接内含在每一个类对象之中,就像在C语言的struct的情况一样。
而成员函数虽然在类的声明之内,却不出现在对象中。每一个非内联函数只会生成一个函数实例。
C++在布局以及存取时间上主要的额外负担是由virtual引起的。包括virtual函数和virtual基类。
此外还有一些多重继承下的额外负担,发生在“一个派生类和其第二或后继之基类的转换”“之间。
C++对象模式
一个简单对象模型
这个模型是为了尽量减低C++编译器的设计复杂度而开发的,赔上的是空间和运行时期的效率。
在这个简单模型下,成员本身并不放在对象之中。只有”指向成员的指针”才放在对象内。
这么做可以避免”成员有不同的类型,因而需要不同的存储空间”所招致的问题。
虽然这个模型并没有被应用于实际产品中,不过关于索引或slot个数的观念,倒是被应用到C++的”指向成员的指针(pointer-to-member)”观念之中。
表格驱动对象模型
另一种对象模型是把所有与members相关的信息抽离出来,放在一个data member table和一个member function table之中,类对象本身则内含指向这两个表格的指针。
虽然这个模型也没有实际应用于真正的C++编译器身上,但member function table这个观念却成为支持virtual functions的一个有效方案。(CRE:也许是说虚函数表VTable)
C++对象模型
在此模型中,non-static数据成员被配置于每一个类对象之内,static数据成员则被存放在个别的类对象之外。static和non-static函数成员也被放在个别的类对象之外。
virtual函数则由两个步骤支持:
- 每个class产生出一堆指向虚函数的指针,存放在表格中,称为virtual table(vtbl)。
- 每个类对象被安插一个指针,指向相关的virtual table,通常称为vptr。
- 继承:
基类可以使用slot或者bptr来指向,但是这些方法的缺点是间接性导致的空间和时间负担。
C++最初采用的继承模型并不运用任何间接性,基类subobject的数据成员被直接放置于派生类object中。这提供了对基类成员最紧凑而且最有效率的存取。
自C++2.0起才新导入的virtual基类,需要一些间接的基类表现方法。virtual基类的原始模型是在类对象中为每一个有关联的virtual基类加上一个指针。(其他演化出来的模型则要不是导入一个虚基类表,就是扩充原已存在的virtual表)
关键字带来的差异
如果不是为了努力维护与C之间的兼容性,C++远可以比现在更简单些。
如果C++并不需要支持C原有的struct
,那么class
的观念可以借由关键字class
来支持。
关键字的困扰
GPT:技术上,
struct
和class
是完全相同的,除了默认访问修饰符。结构体是默认公开,类是默认私有的。
GPT:当你需要一个主要用来存储数据的简单容器或者POD时,使用
struct
是合适的。这样可以清楚地向使用者表明这是一个数据结构,而非一个完整的对象。而class
通常用于具有行为的复杂数据结构。
策略性正确的struct
一些C语言使用的小伎俩可能在C++中或许可能无效化。注意,C++中凡是处于同一个访问控制部分的数据,必定保证以其声明顺序出现在内存布局中。然而被放置在多个访问控制部分中的各笔数据,排列顺序就不一定了。
C struct在C++中的一个合理用途,是当你要传递一个“复杂的类对象的全部或部分”到某个C函数中去,struct声明可以将数据封装起来,并保证与C兼容的空间布局。然而这项保证只在组合而非继承的情况下才存在。
对象的差异
- 编程范式
C++程序设计模型直接支持三种编程范式(programming paradigms):
- 程序化模型(procedual model)。
- 抽象数据类型(ADT)。
- 面向对象模型(object-oriented model)。
纯粹以一种范式写程序,有助于整体行为的良好稳固。
- ADT范式和OOP范式
ADT和OOP范式区别是:OOP范式中,程序员往往需要处理一个未知实例,它的类型虽然有所界定,但是有很多可能。相反地在ADT范式中,程序员处理的是一个拥有固定而单一类型的实例,他在编译时期就已经完全定义好了。
CRE:OOP范式往往会使用指针/引用以及里氏替换,来实现多态。而ADT往往会使用对象的切割。
- 多少内存可以表现一个类对象
- non-static数据成员的总大小。
- alignment的需求而padding的空间。
- 为了支持virtual的额外负担。
指针的类型
“指向不同类型的指针”之间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的object类型不同。也就是说,“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其大小。
所以转换(cast)其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式。
加上多态之后
CRE:MSVC中只有带有虚函数时,才会在对象开头存一个4字节的vptr。
OOP不支持”直接的对象存取”。(CRE:也许是说对象切割会导致OOP失效)
- 对象直接赋值/初始化中的切割:
Bear bear;
Animal animal;
animal = bear;
animal.Do();
(&animal)->Do();
//输出(MSVC):
//Animal Do
//Bear Do
Bear bear;
Animal animal;
memcpy(&animal, &bear, sizeof(Animal));
animal.Do();
(&animal)->Do();
//输出(MSVC):
//Animal Do
//Animal Do
CRE:在MSVC中,对象的复制构造或者赋值,貌似不会复制虚函数表指针。但是memcpy可以复制虚函数表指针。
(END)