第一章:关于对象

时间:2022-05-03 05:43:14

(1)引言

正如我们所知,C是过程(procedural)语言,“数据”和“处理数据的操作(函数)”是分开声明的,语言本身没有支持数据和函数之间的关联性。换句话说,C语言由一组“分布在各个以功能为导向的函数中的算法所驱动,它们处理的是共同的外部数据”。

如自定义数据:

1 typedef struct point3d
2 {
3     float x;
4     float y;
5     float z;
6 }Point3d

那么可以围绕该数据定义许多操作该数据的函数,这些函数与数据之间的关联性并不由语言本身支持,而是由用户进行关联。

在C++中,可以通过抽象将数据和操作该数据的函数封装在一个类中:

 1 class Point3d
 2 {
 3   public:
 4      Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
 5         :_x(x),_y(y),_z(z)
 6      { }
 7      //...
 8   private:
 9      float _x;
10      float _y;
11      float _z;
12 };

甚至class类还可以通过双层或多层的类继承体系实现上面的数据和函数的封装。对此,我们也许该考虑一下:为什么我们愿意用“ADT或class hierarchy”的数据封装来代替“在C程序中过程性地使用全局数据”?很多情况下,程序员们会在”被要求快速让一个应用程序上马应战,并且执行起来又快又有效率“而选择精瘦和简易的C程序。相比于C的精瘦和简易,C++的好处我们在后面会依依引述出来,下面我们先讲讲C++封装带来的布局成本。

(2)封装带来的布局成本

上面C程序中的struct和C++中抽象出的class,虽然有很大的不同,但具有封装性的class其实并没有增加布局成本。这是因为三个data members直接内含在每一个class object对象之中,就像C struct的情况一样。而member functions虽然含在class的声明之内,却不出现在object之中,每一个非inline成员函数只会诞生一个函数实体;而inline成员函数则会在每一个使用者(模块)身上产生一个函数实体。上面的类Point3d支持封装,但并未带来任何空间或执行期的不良回应。

C++在布局以及存取时间上的主要额外负担是由virtual引起,包括:virtual function 和 virtual base class,以及多重继承下的额外负担。

(3)简单对象模型和表格驱动模型

在C++中,有两种class data members:static 和 nonstatic,以及三种class member functions:static, nonstatic 和 virtual。

如果定义如下类:

第一章:关于对象第一章:关于对象
 1 class Point
 2 {
 3 public:
 4     Point(float xval);
 5     virtual ~Point();
 6 
 7     float x() const;
 8     static int PointCount();
 9 
10 protected:
11     virtual ostream& print(ostream& os) const;
12     float _x;
13     static int_point_count;
14 };
View Code

那么对应的对象模型有:

a. 简单对象模型

第一章:关于对象

即一个class object是由一系列的slots组成的,每一个slot指向一个member。这种模型中,members本身不放在object之中,object中的members以slot的索引值来寻址,只有”指向member的指针“才放在object内,一个class object的大小由指针大小乘以class中声明的members数量确定,同时也可以避免由于members的不同类型而带来的不同存储空间的问题。

这个简单模型的简单之处在于降低了编译器设计的复杂性,但也赔上了时间和空间上的效率。这个模型并没有被应用于实际产品上,但是关于索引或slot数目的观念被应用到了C++的”指向成员的指针“的观念之中。

b. 表格驱动对象模型

第一章:关于对象

表格驱动模型如上,该模型将所有与members相关的信息抽出来,放在一个data member table 和 一个member function table之中,class object本身则内含指向这两个表格的指针。其中data member table内含data本身,member function table则是一系列的slots,每一个slot指向一个member function。

表格驱动模型也没有实际应用于真正的C++编译器中,但是member function table的观念却有助于我们理解virtual functions机制。

(4) C++ 对象模型

第一章:关于对象

如上,最终使用的C++对象模型中,non-static data members被配置于每一个class object之内,static data members则被存放在所有的class object之外;static和non-static function members都被放在class object之外;object中还含有一个vptr指针指向一个virtual table。virtual functions如下实现:

  • 每一个class 产生一堆指向virtual functions的指针,放在表格之中。此表格被称为 virtual table(vtbl)。
  • 每一个class object被添加了一个指针vptr,指向相关的virtual table。vptr的设定和重置都由每一个class的constructor、destructor和copy assignment运算符自动完成。

这个模型的优点是提高了空间和存取时间的效率;缺点则是如果应用程序代码本身未改变,但所用到的class objects的non-static data members有所修改,那么应用程序代码就需要重新编译。对此,表格驱动模型通过将data members放在object之外而避免了这个问题,但是带来了空间和执行效率的代价。

(5)多态实现

通过虚函数实现多态,调用虚函数的静态类型必须是基类类型的指针或引用,而不能是对象。首先看如下代码:

第一章:关于对象第一章:关于对象
 1 class ZooAnimal
 2 {
 3 public:
 4 ZooAnimal();
 5 virtual ~ZooAnimal();
 6 virtual void rotate();
 7 
 8 protected:
 9 int loc;
10 string name;
11 };
12 
13 class Bear : public ZooAnimal
14 {
15 public:
16 Bear();
17 ~Bear();
18 
19 void rotate();
20 virtual void dance();
21 
22 protected:
23 enum Dances{...};
24 Dances dances_known;
25 int cell_block;
26 };
27 
28 Bear b("yuanyuan");
29 Bear *pb = &b;
30 Bear &rb = *pb;
View Code

根据上面的代码,得到的内存布局:
第一章:关于对象

Bear b;
ZooAnimal *pz = &b;
Bear *pb = &b;

如果上面定义的对象b的地址是1000,那么指针pz,pb都将指向这个对象的第一个byte,其区别是:pb所涵盖的地址包含整个Bear对象,而pz所涵盖地址只包含Bear对象中的ZooAnimal subobject。所以,除了ZooAnimal中的members,不能使用pz直接处理Bear的任何members,唯一例外是通过virtual机制。例如下面的代码:

pz->cell_block;//error,即使pz指向一个Bear object
((Bear*)pz)->cell_block;//ok,明白的通过downcast转换
if(Bear* pb2 = dynamic_cast<Bear*>(pz))
    pb2->cell_block;//better,但是run-time转换,成本较高

对于函数调用:

pz->rotate();

pz所指向的object类型将决定rotate()所调用的实体,类型信息的封装不是维护在pz之中,而是维护在link之中,次link存在于“object的vptr”和“vptr所指的virtual table”之间。不同于指针或引用的转换机制,如果将一个派生类对象直接赋给一个基类对象,那么会发生切割。否则一个为一个基类对象分配的内存空间是不足以存放一个派生类对象的。

延伸:对于不支持多态的OB(object-based)类,如string class,由于所有函数引发操作都在编译时期解析完成,对象构建不需要设置virtual机制,因而具有更快的速度;另一方面,由于每一个class object不需要负担传统上为了支持virtual机制而需要的额外负荷,因而空间上更紧凑。其缺点是不如支持多态机制的类更具有弹性。