关于对象
c语言中,“数据”和“处理数据的操作(函数)” 是分开声明的,将这种程序方法称为程序性(procedural),比如声明一个struct Point3d:
struct Point3d { float x; float y; folat z; };
而操作该数据数据的函数例如打印函数,只能另外定义成:
void Point3d_print(const Point3d *pd) { print("(%f, %f, %f)", pd->x, pd->y, pd->z); }
或者定义成一个宏:
#define Point3d_print(pd) print("(%f, %f, %f)", pd->x, pd->y, pd->z);
而在C+中,可以采用独立的“抽象数据类型”(ADT,abstract data type)来实现:
class Point3d { private: float _x; float _y; float _z; 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; } }; inline osteream& operator<< (ostream& os, const Point3d &pt) { os << "(" << pt.x() << ", " << pt.y() << ", " << pt.z() << ")"; }
或者一个三层的class层次结构来完成,例如
此外利用模板template,可以将坐标类型和坐标个数都参数化:
template <typename type, int dim> class Point { private: type _coords[dim]; public: Piont(); Point(type corrds[dim]) { for(int i = 0; i < dim; ++i) { _corrds[i] = corrds[i]; } } type& operator[](int index) { return const_cast<type&> (static_cast<const type&>(*this)[index]); } type& operator[](int index) const { assert(index >= 0 && index < dim); return _corrds[index]; } }; inline template<typename type, int dim> ostream& operator<<(ostream& os, const Point<type, dim> &pt) { os << "("; for(int i = 0; i < dim - 1; ++i) { os << pt[i] << ","; } os << pt[dim - 1] << " )"; }
从软件工程的角度来看,C++中的一个ADT(尤其是使用template)比C中的程序性使用全局数据要更好,威力更大。
封装之后的布局成本:主要是由virtual引起的,包括:
- virtual function,用于支持一个有效率的执行期绑定(running binding)
- virtual base class,用于实现多继承体系中的base class,单一共享实例
一、C++对象模型(The C++ Object Model)
在C++中有两种成员数据:static data members(静态) 以及non-static data members(普通非静态);
三种成员函数:static member function(静态成员函数), non-static member function(一般非静态成员函数)以及virtual member function(虚函数)。
如下面这个类
class Point { public: Point(float xval); virtual ~Point(); //virtual member function(虚析构函数) float x() const; //non-static member function static int PointCount(); //static member function private: float _x; //static data member static int _point_count; //non-static data member }
简单对象模型
一个object都是一系列的slot, 每一个slot指向一个member,按照声明顺序,如下:
在简单模型下,members并不放在object中,只有指向members的指针才放在object中,这么做可以避免members有不同的类型,因而需要不同的存储空间,object中的members是以slot的索引来寻址的。slot的索引为3时,指向的是函数float Point::x();
很容易知道一个class object的大小为指针大小乘上class中声明的member的个数。
表格驱动的对象模型
这种模型抽取members 的信息,放在一个data member table 和一个 member function table中,而class object中则含有指向这两个table的指针,例如:
C++ 对象模型
C++对象模型将静态数据成员,静态成员函数和一般非静态成员函数均存放在个别的class object之外(单独存取,和对象无关),而非静态数据成员则被放在每一个class object内,虚函数则以下面两个步骤支持:
- 每一个class产生一堆指向虚函数的指针,并且按照顺序置于虚函数表(virtual table,vbtl)。
- 每一个class object安插一个指针(vptr)指向第一步的virtual table。vptr的设定以及重置都由每一个class的construtor, destrutor和copy assignment自动完成。(由于构造函数来设定vptr,故构造函数无法称成为虚函数)。每一个class 所关联的type_info object(用于支持RTTI)也通常放在虚函数表的第一个slot。
例如:
加上继承
在虚继承,virtual base class不管在继承链上被派生多少次,派生类中永远只会存在一个virtual base class的一个实例(有且仅有一个subobject),例如在以下iostream继承体系中,派生类iostream只有virtual ios base 的一个实例。
那么一个派生类是如何模塑其基类的实例呢?
base table模型:每一个class object内含一个bptr(pointer to base ctable),例如:
不管上述哪一种机制,“间接性”的级数因继承性的深度而增加。
c++最初的继承性模型不用任何的间接性,base class subobject的data members被直接放置于derived class object中,和提供了对base class members最紧凑而且最有效的存取,缺点是:base class members 任何改变,包括增加,移除或改变类型等,都使得此“base class 或其 derived class 之object”重新编译。
iostream类中的pbtr指向它的两个基类的table,此外,这种模型下存取的时间和继承的深度是密切相关的。例如:iostream object 要存取到ios subobject必然要经过两次。更加详细的讨论可以见第三章第四节。
对象模型是如何影响程序
举个例子:class X定义了copy constructor, virtual destructor和一个virtual function foo()。
X foobar() { X xx; X *px = new X; xx.foo(); px->foo(); delete px; return xx; }
内部的转化可能是:
void foobar(X& _result) { //NRV优化 //构造_result来代替返回的local xx _result.X::X(); //扩展X *px = new X; X *px = new(sizeof(X)); if(px != 0) px->X::X(); foo(&_result); //扩展px->foo(); (*px->vtbl[2])(px); //delete px; if(px != 0) { (*px->vtbl[1])(px);//destructor _delete(px); } return; }
类X的对象模型如下:
二、关键词所带来的差异
关键字struct和class:在C语言中,struct代表的是一个数据集合体,没有private data,member function;而在C++中,struct和class均是代表类,唯一的差别在于类的默认访问权限和默认继承类型是不同的:
关键字 | 默认访问权限 | 默认继承类型 |
---|---|---|
struct | public | public |
class | private | private |
template并不兼容C,因此template<struct Type>
是错误的。
策略正确的struct
把单一元素的数组放在一个struct的尾端,于是每个struct objects 可以拥有可变大小的数组:
struct mumble { //stuff char pc[1]; }; //从文件或标准输入中读一个字符串,然后为struct本身和字符串配置足够的内存 struct mumble *pmumb=(struct mumble*)malloc(sizeof(struct mumbleble)*strlen(string)+1); strcpy(mumble.pc,string);
如果改用class来声明,class该是:
- 指定多个access sections,内涵数据
- 从另一个class派生而来
- 定义了一个或多个virtual function
c++中处于同一access sections的数据,必定保证其声明顺序出现在内存当中,然而放在多个access sections中的数据,排列顺序就不一定。最好的排列顺序
struct mumble { public: //operations protected: //protected stuff private: //private stuff char pc[1]; };
同理:base classes和drivied classes的data members的布局也未有谁先谁后的规定,因而也不能保证前述的c伎俩一定有效,virtual function的存在也会使前述伎俩成为一个问号。
组合而非继承才是把c和c++结合在一起的唯一可行的办法(conversion运算符提供了一个十分便利的萃取方法)
struct C_point{...}; class point { public: operator C_point { return _c_point; } private: C_point _c_point; //... };
c struct在c++中的一个合理的用途是:当你要传递 一个复杂的class object的全部或部分 到某个c函数去时,struct声明可以将数据封装起来,并保证拥有与c兼容的空间布局。然而这项保证只有在组合(composition)的情况下才存在。如果是继承而不是组合,编译器决定是否有额外的data members被安插到base struct subobject中。
三、对象的差异
C++支持三种程序设计范式(programming paradigms)
1.程序模型
2.抽象数据类型模型(abstract data type model,ADT):所谓的“抽象”是和一组表达式(public接口)一起提供,那时的其运算定义任然隐而未明。如 string class
String girl="Anna"; String daughter; ... //String::operator=() daughter=girl; ... //String::operator==() if(girl==daughter) take_to_disneyland(girl);
3.面向对象模型:此模型中有一些彼此相关的类型,通过一个抽象的base class(用以提供接口)被封装起来。
虽然可以直接处理继承体系中的一个或多个base class。但是只有通过pointer或者reference的间接处理,才能支持多态(用基类对象的指针或者引用处理派生类接口),否则很可能产生切割(将派生类对象直接赋给基类对象)。
原则上,被指定的object真正类型在每一个执行点之前是无法被解析的,在C++中,只有通过pointers和reference的操作才能完成,相反的,ADT paradigm中,程序的处理是一个拥有固定而单一类型的实例。他在编译期已经确定。
//px && pr 不确定到底指向何种类型,可能是一个Librar_materials或者是其子类。 Librar_materials *px=retrieve_some_material(); Librar_materials &pr=*px; //dx只能是Librar_materials类型 Librar_materials dx=*px;
c++的多态只存在于一个个public class体系中。nonpublic的派生行为及类型为void*的指针可以说是多态的,但没有名曲的语言支持,必须通过程序员显示操作转换管理。
c++支持多态方法:
- 经由一组隐式的转化操作,例如把一个derived class 指针转化为一个指向public base type的指针
- 由virtual function机制
- 由dynamic_cast和typeid运算符。if(circle *pc=dynamic_cast<circle*>(ps))...
主要的一个用途是以公共的接口来影响类型的封装。
关于多态的动态类型和动态绑定参见c++中使用空指针调用成员函数的理解。
举个例子:class Z public继承自 class X,X中有个虚函数rotate(),考虑下面的代码:
void rotate(X datum, const X *pointer, const X& reference) { //下面这个操作总是调用X::rotate(); datum.rotate(); //下面这两个操作必须在运行期才能知道调用的是那个rotate()实例 (*pointer).rotate(); reference.rotate(); } main() { Z z; rotate(z, &z, z); return 0; }
在main()
函数中,rotate(z, &z, z);
中的第一个参数z不经过virtual机制,因此总是调用X::rotate()
,而另外两个参数由于一个是指针,一个是引用,因此都调用的是Z::rotate();
class object需要的内存大小:
一般情况下等于其non-static data member的总和大小加上为了支持virtual而内存产生的额外负担以及由于alignment的padding(内存对齐)。
指针的类型
指向不同的类型的指针之间的差异在于:其所寻地址出来的object的类型不同,也就是说,指针类型会教导编译器如何解释某个特定的地址中的内存内容与大小;另外转换cast其实是一种编译器指令,只影响地址中的内存内容与大小的解释方式,并不会改变指针所指向的地址。
例如,下面class ZooAnimal声明:
class ZooAnimal { public: ZooAnimal(); virtual ~ZooAnimal(); virtual rotate(); protected: int loc; string name; }; ZooAnimal za("Zoey"); ZooAnimal *pza = &za;
内存布局如下:
int在32位机器上一般是4bits,内存中的地址涵盖1000~1003,string通常是8bits(包括4bits的字符指针以及4bits的字符长度的整数),地址涵盖1004~1011,最后是4bits的vptr,地址涵盖1012~1015.
转换(cast)是一种编译器指令,大部分情况下它并不改变一个指针所含的真正地址,他只是影响“被指出之内存的大小和其内容”的解释方式。
加上多态之后
class Bear : public ZooAnimal { public: Bear(); ~Bear(); void rotate(); virtual void dance(); protected: enum Dances{...} Dances dances_known; int cell_block; }; Bear b("yogi"); Bear *pb = &b; Bear &rb = *pb; ZooAnimal *pz = &b;
pz在编译时期决定一下两点:
- 固定的接口,pz只能掉ZoonAnmainl的public接口
- 该接口的access level
b,pb,rb的内存需求是怎样的呢?
b是一个Bear Object,所需要的内存位24bytes,
包括ZooAnimal的16bytes以及Bear本身的8bytes,而指针pb以及引用rb只需要4bytes(在32位机器上)。具体见下图:
假设Bear object b放在地址1000处,那么Bear指针pb 和ZooAnima指针pz有什么区别呢?它们都是指向Bear Object的首地址(第一个byte即1000),差别在于pb所涵盖地址包括整个Bear Object即1000~1023,而pz所涵盖地址仅仅包括ZooAnimal Subobject即1000~1015.
Bear b; ZooAnimal *pz=&b; Bear *pb=&b;
他们每个都指向Bear object的第一个byte,差别是pb所指的地址包含整个bear object,pz所含的地址只包含Bear object中的ZooAnimal subobject。
Bear b; ZooAnimal za=b;//会引起切割 za.rotate();
初始化函数将一个object内容完整的拷贝到另一个object中去,为什么za的vptr不指向Bear的virtual table?
编译器在初始化及制定操作之间做出了仲裁,编译器必须保证如果某个object含有一个或一个以上的vptrs,那些vptrs的内容不会被base class object初始化改变
一个pointer或reference能一起多态,以为他们并不是引发内存中任何“与类型有关的内存委托操作”,受到改变的只是内存“大小和解释方式”
派生类和基类的类型转换
- 针对类对象,用基类对象为派生类对象赋值或者初始化,或者用类型转换符将基类对象转化为派生类对象,都是非法的;用派生类对象为基类对象赋值或者初始化,或者用类型转换符将派生类对象转化为基类对象,是可以的,但是部发生切割(sliced).
- 针对指针或者引用,将基类对象绑定到派生类对象的指针或者引用上,这是非法的;而将派生类对象绑定到基类的指针或者引用上,则是合法的,并且这是多态的基础条件。
不能讲基类转化为派生类。