在C语言中,语言本身并没有建立数据和函数之间的关联性,所以C语言被称为是面向过程(procedural)的。C++有允许用户进行自行定义的抽象数据类型(ADT),这类语言被称为面向对象的语言。
那么第一个问题来了, 加上封装之后,布局成本有没有增加?
以以下一个类Point3d为例来分析。
class Point3d{
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
:_x(x), _y(y), _z(z) { }
float x() { return _x; }
float y() { return _y; }
float z() { return _z; }
void x(float xval) { _x = xval; }
// ... etc ...
private:
float _x, _y, _z;
};
答案是:跟C语言相比,Point3d的布局成本没有增加。三个data members直接在class object之中,就像C的struct一样。而member functions 虽然在class的声明之内,但是它不会出现在object之中。每一个non-inline member function 只会产生一个函数实例。至于inline function则会在其使用者处产生一个函数实例。这些跟C语言的一样,所以Point3d的封装并未给它带来任何空间或是执行期的不良后果。其实C++比C语言所付出的额外负担主要在virtual方面,具体是以下两个方面:
- virtual function 机制 一个用于支持执行期绑定的机制。
- virtual base class 用于保证一个base class多次在继承体系中出现,而只有一个且被共享的实例。
1.1 C++对象模型
在C++中,有两种class data members: static 和 nonstatic, 以及三种class member functions: static、nonstatic和virtual。
以下面的类声明来阐述类在机器上的表现。
class Point{
public:
Point(float xval);
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print(ostream &os) const;
float _x;
static int _point_count;
};
首先,nonstatic data members被配置在每一个class object之内,static data members被配置在class object之外。其次static function members 和 nonstatic function members都是被配置在class object之外。最后virtual function members以以下两个步骤支持:
1. 每一个 class 产生一堆指向 virtual functions 的指针,放在一个表格里,这个表格称为virtual table(简称是vtbl)。
2. 每一个 class object 被安插一个指针,指向该类的virtual table。这个指针通常被称为vptr。vptr 的设定(setting)和重置(resetting)都由每一个 class 的 constructor、destructor 和 copy assignment 运算符自动完成。每一个 class 所关联的 type_info object(用以支持runtime type identitication,RTTI)也经由virtual table 被指出来,通常放在表格的第一个slot。
该对象模型用图表示如下:
这种模型的优点主要在于它的空间和存取时间的效率。
加上继承之后模型会如何后续(3.4节)再做讨论。
对象模型如何影响程序
不同的对象模型会导致“现有的程序代码必须修改”以及“必须加入新的程序代码”两个结果。以下面的这个函数为例,看一下对象模型对程序的影响。首先 class X 定义了一个 copy constructor、一个 virtual destructor 和 一个 virtual function foo。
那么下面的这个函数会如何转化:
X fooobar()
{
X xx;
X* px = new X;
//foo 是一个 virtual function
xx.foo();
px->foo();
delete px;
return xx;
}
因为 class X 总共有三个函数,一个构造函数,一个虚析构函数,一个虚函数 foo,所以可以先画出 class X 的对象模型。
根据这个模型以上函数 foobar() 的代码可能做如下转化:
void foobar(X &_result){
//构造_result
//_result 用来取代函数内部的 local xx
_result.X::X();
//扩展X *px = new X;
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);
_delete(px);
}
//无须使用 named return statement
//无须摧毁 local object xx
return;
}
1.2 关键词所带来的差异
这点主要讨论struct在C++的使用必要。struct与class只是关键字,如无特别需要,建议全部用class代替struct。语言对接时除外。
1.3 对象的差异
有三种模型注意区分一下程序模型(procedural model), 抽象数据类型模型(abstract data type model, ADT),和面向对象模型(object-oriented model)。其中面向对象模型就是以范式在写程序,必须借助于指针或引用去操作完整的未知的对象,这样的方法才称为面向对象的编程。面向对象的编程的最小单位就是对象,就是说它在操作对象。摘抄原书中的两句话:虽然你可以直接或间接处理继承体系中的一个base class object,但只有通过pointer 或 reference 的间接处理,才支持OO程序设计所需的多态性质。 在OO paradigm中,程序员需要处理一个未知的实例,它的类型虽然有所界定,却有无穷的可能性。
C++以下列方法支持多态:
- 经由一组隐式的转化操作。例如把一个派生类的指针转化为一个指向其public base type 的指针。如:shape *ps = new circle(); //public circle : public shape
- 经由virtual function 机制。调用指针(或引用)对应的虚方法。
- 经由dynamic_cast 和 typeid 运算符。如:if( circle *pc = dynamic_cast< circle * >(ps) )。
多态的主要用途是经由一个共同的接口来影响程序的封装,这个接口通常被定义在一个抽象的base class 中。
现在再问一次:需要多少内存才能够表现一个class object?一般而言要有:
- 其nonstatic data members 的总和大小。
- 加上任何由于alignment(对齐)的需求而填补上去的空间。
- 加上为了支持 virtual 而由内部产生的任何额外负担(overhead)。
指针的类型
指针的类型会教导编译器如何解释某个特定地址中的内存内容及其大小。
加上多态之后,理解以下几句话:
- OO程序设计并不支持对object的直接处理。
- C++通过class的pointers和references来支持多态,这种程序设计风格称为多态。
- 区分OB(object-based)和OO(object-oriented)。OB就是具体的ADT程序风格,不含多态。