0%

深度探索C++对象模型之前言

前言

Simplifier是编译器的一部分,处于type checkingcode generation之间。它用来转换内部的程序表现。有3种转换是任何对象模型都需要的:

  1. 与编译器息息相关的转换(Implementation-dependent transformations

    例如,当parser看到这个表达式:

    1
    fct();

    它并不知道是否(a)这是一个函数调用操作,或者(b)这是overloaded calloperatorclass object fct上的一种应用。默认情况下,这个式子所代表的是一个函数调用,但是当(b)的情况出现,Simplifier就要重写并调换call subtree

  2. 语言语意转换(Language semantics transformations

    这包括constructor/destructor的合成和扩展、memberwise初始化、对于memberwise copy的支持、在程序代码中安插conversion operators、临时性对象,以及对constructor/destructor的调用。

  3. 程序代码和对象模型的转换(Code and object model transformations

    这包括对virtual functionsvirtual base classinheritance的一般支持、newdelete运算符、class objects所组成的数组、local static class instances、带有非常量表达式(nonconstant cxpression)之global object的静态初始化操作。

什么是C++对象模型?

  • 语言中直接支持面向对象程序设计的部分
  • 对于各种支持的底层实现机制

1章:关于对象

C语言中,“数据”和“处理数据的操作(函数)”是分开来声明的,也就是说,语言本身并没有支持“数据和函数”之间的关联性。

C++在布局以及存取时间上主要的额外负担是由virtual引起,包括:

  1. virtual function机制——用以支持一个有效率的“执行期绑定”
  2. virtual base class——用以实现“多次出现在继承体系中的base class,有一个单一而被共享的实体”
  3. 此外,还有一些多重继承下的额外负担,发生在“一个derived class和其第二或后继之base class的转换”之间。
1.1 C++对象模型

C++中,有两种class data membersstaticnonstatic,以及三种class member functionsstaticnonstaticvirtual

C++对象模型中,nonstatic data members被配置于每一个class object之内,static data members则被存放在所有的class object之外。staticnonstatic function members也被放在所有的class object之外。virtual functions则以两个步骤支持之:

  1. 每一个class产生出一堆指向virtual functions的指针,放在表格之中。这个表格被称为 vtbl
  2. 每一个class object被添加了一个指针,指向相关的virtual table。通常这个指针被称为vptrvptr的设定(setting)和重置(resetting)由每一个classconstructordestructorcopy assignment运算符自动完成。

在虚拟继承的情况下,base class不管在继承串链中被派生(derived)多少次,永远只会存在一个实体(称为subobject)。

C++最初采用的继承模型并不运用任何间接性: base class subobjectdata members被直接放置于derived class object中。这提供了对base class members最紧凑而且最有效率的存取。缺点就是: base class members的任何改变,包括增加﹑移除或改变类型等等,都使得所有用到“此base class或其derived classobjects”者重新编译。

virtual base class的原始模型是在class object中为每一个有关联的virtual base class加上一个指针。

对象模型如何影响程序?

不同的对象模型,会导致“现有的程序代码必须修改”以及“必须加人新的程序代码”两个结果。例如下面这个函数,其中class X定义了一个copy constructor,一个virtual destructor,和一个virtual function foo

image-20210219212057818
1
2
3
4
5
6
7
8
9
10
11
12
X foobar()
{
X xx;
X* px = new X;

// foo 是一个虚函数
xx.foo();
px->foo();

delete px;
return xx; // 这里会用到 copy constructor
}

这个函数有可能在内部被转化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void foobar(X& _result)
{
// 构造 _result
// _result 用来取代 local xx...
_result.X::X();

// 扩展 ×* px= new x;
px = _new(sizeof(X));
if(px != 0)
px->X::X();

// 扩展 xx.foo() 但不使用 virtual 机制
// 以 _result 取代 xx
foo(&_result);

// 使用 virtual 机制扩展 px->foo()
(*px->vtbl[2])(px);

// 扩展 delete px;
if(px != 0)
{
(*px->vtbl[1])(px); // destructor
_delete(px);
}

//不需使用 named return statement
//不需要摧毁 local object xx
return;
}
1.2 关键词所带来的差异
  • 掌握structclass关键字的差异

struct关键词的使用实现了C的数据萃取概念,而class关键词实现的是C++ADTAbstract Data Type)概念。

C程序员的巧计(C++中不可用)。例如把单一元素的数组放在一个struct的尾端,于是每个struct objects可以拥有可变大小的数组:

1
2
3
4
5
6
7
8
9
10
11
struct mumble
{
// stuff
char pc[1];
};

// 从档案或标准输入装置中取得一个字符串 str
// 然后为 struct 本身和该字符串配置足够的内存
struct mumble *pmumble = (struct mumble*)
malloc(sizeof(struct mumble) + strlen(str) + 1);
strcpy(&(pmumble->pc), str);

C++中凡处于同一个access section的数据,必定保证以其声明次序出现在内存布局当中。然而被放置在多个access sections中的各笔数据,排列次序就不一定了。组合(composition),而非继承,才是把CC++结合在一起的唯一可行方法(conversion运算符提供了一个十分便利的萃取方法):

1
2
3
4
5
6
7
8
struct C_point { ... };
class Point
{
public:
operator C_point() { return c_point_; }
// ...
private:
C_point c_point_;

C structC++中的一个合理用途,是当你要传递“一个复杂的class object的全部或部分”到某个C函数中去时,struct声明可以将数据封装起来,并保证拥有与C兼容的空间布局。然而这项保证只在组合的情况下才存在。

1.3 对象的差异

C++支持3种程序设计模型:

  1. 程序模型(面向过程)
  2. 抽象数据类型模型(基于对象模型)(封装)
  3. 面向对象模型(继承、多态)

C++,多态只存在于一个个的public class体系中。nonpublic的派生行为以及类型为void*的指针可以说是多态,但它们并没有被语言明白地支持,也就是说它们必须由程序员通过明白的转型操作来管理。C++以下列方法支持多态:

  1. 经由一组隐含的转化操作。例如把一个derived class指针转化为一个指向其public base type的指针

    1
    Shape *ps = new Circle();
  2. 经由virtual function机制

  3. 经由dynamic_casttypeid运算符

    1
    2
    if(Circle* pc = dynamic_cast<Circle*>(ps))
    ...

virtual function机制不只使得“当类型有所增加、修改、或删减时,我们的程序代码不需改变”。而且也使一个新的subtype的供应者不需要重新写出“对继承体系中的所有类型都共通”的行为和操作。

需要多少内存才能够表现一个class object

  • nonstatic data members的总和大小
  • 加上任何alignment(内存对齐)
  • 加上为了支持virtualfunctionbase class)而由内部产生的额外负担

转型(cast)其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式。

一个Base指针pb和一个Derived指针pd有什么不同?

1
2
3
Derived d;
Base* pb = &d;
Derived* pd = &d;

它们每个都指向Base object的第一个byte。其间的差别是,pd所涵盖的地址包含整个Derived object,而pb所涵盖的地址只包含Derived object中的Base subobject

除了Base subobject中出现的members,你不能使用pb来直接处理Derived的任何members。例外是通过virtual机制或转型操作。

当一个base class object被直接初始化为(或是被指定为)一个derived class object时,derived object就会被切割,以塞人较小的base type内存中,derived type将没有留下任何蛛丝马迹。

下面这一组定义,其可能的内存布局为:

image-20210220202640585
1
2
3
4
5
ZooAnimal za;
ZooAnimal *pza;
Bear b;
Panda* pp = new Panda;
pza = &b;
image-20210220153355200