深度探索C++对象模型 1
第一章:关于对象
加上封装后的布局:
如果C++class和C struct含有相同数据成员,那么C++class并没有增加成本。因为,C++ class中的成员函数不出现在object中(non-inline 成员函数只会诞生一个实例,而inline函数则会在调用该C++ Class的文件模块中产生一个实例),所以C++支持的封装性质并未带来任何空间或执行期的不良后果。
C++在布局以及存取时间上主要的额外负担是由virtual引起的(包括虚函数机制和虚基类),此外多重继承下也会有额外负担(发生在一个子类和它的第二个或后继基类之间的转换时)。
1.1C++对象模型(书中写的是模式)
C++包含两种数据成员(static和nonstatic)和三种成员函数(static,nonstatic和virtual)。
C++对象模型:
Bjarne Stroustrup博士(C++之父,C++语言的创始人,著有《The C++ Programming Language》,《The Design and Evolution of C++》)将Nonstatic data members放在每一个classobject内,static data members放在class object之外,static和nonstatic function members也放在class object之外。virtualfunctions则以两个步骤支持:1.每个class产生出一堆指向virtual functions的指针,放在表格之中,这个表格成为 virtual table(简称vtbl)。2.每个class object被安插一个指针(称为vptr),指向相关的virtual table。vptr的设定和重置都由每一个class的constructor、destructor和copy assignment运算符自动完成(第五章讨论)。每一个class所关联的type_info object(用以支持runtime type identification, RTTI)也经常由virtual table被指出来,通常放在表格的第一个slot。
例如有以下一个类Point:
class Point{
public:
Point(floatxval);
virtual~Point();
floatx() const;
staticint PointCount();
private:
virtualostream&
print(ostream &os ) const;
float _x;
static int_point_count;
} ;
则下图表示C++ 对象模型应用在Point类上。
这个模型的主要优点在于空间和存取时间的效率;缺点是如果应用程序代码本省未曾该表,但所用到的class object的nonstatic data member有所改变,那么那些应用层序也得重新编译。
加上继承:
C++最初采用的继承模型并不运用任何间接性:base class subobject的data members被直接放置于derived classobject中。优点:提供base class members最紧凑而且最有效率的存取;缺点:baseclass members的任何改变,任何用到该base class或该base class的derivedclass object都需要重新编译。
C++2.0之后导入virtualbase class,需要一些间接的base class表现方法:virtual base class的原始模型在classobject中为每一个有关联的virtual base class加上一个指针,或者在classobject中导入virtual base class table,又或者扩充原已存在的virtual table,以便维护每一个virtualbase class的位置。
对象模型如何影响程序:
不同的对象模型会导致“现有的程序代码必须修改”以及“必须加入新的程序代码”两个结果。
例如下面这个函数,其中class X定义了一个copy contructor、一个virtualdestructor和一个virtual function foo:
X foobar(){
X xx;
X *px = new X;
//foo()是一个虚函数
xx.foo();
px->foo();
delete px;
return xx;
};
这个函数有可能在内部转化为:
Void foobar( X &_result ){ //此处&为引用
//构造_result
_result.X::X();
//扩展X *px = new X;
px = _new( sizeof( X ) );
if( px != 0 )
px->X::X();
//扩展xx.foo() 但不使用virtual机制
foo(& _result); //此处&为取地址符
//使用virtual扩展px->foo()
( *px->vtbl[ 2 ] )( px );
//扩展delete px;
( *px->vtbl[ 1 ] )( px );
_delete( px );
return;
}
下图可解决 *px->vtbl[ 1 ] 和 *px->vtbl[2 ] 的含义。
1.2 关键词所带来的差异
略
1.3对象的差异:
C++程序支持三种模型
1.程序模型:像C程序一样
2.抽象数据类型模型(也被称为object-based OB):有封装,但没有多态和继承。比如string
3.面向对象模型(object- oriented OO):有封装,多态,继承
C++以下列方式支持多态:
1.由一组隐式的转化操作:父类指针指向子类对象,或父类引用类型引用子类对象
2.经由虚函数机制
3.经由dynamic_cast和typeid运算符
一个class object需要的内存一般而言由三部分组成:
1.其nonstatic data members的总和大小(包括父类的nonstaticdata members)
2.加上任何由于alignment而需要的填充空间。(alignment就是将数值调整到某数的倍数。在32位计算机上,alignment为4bytes,以使运算量达到最高效率)
3.加上为了支持virtual而由内部产生的任何额外负担
指针的类型:
“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其大小。
如:有一个对象有两个数据成员,类型分别为int和string,还有一个指向该对象的指针,则该指针将涵盖12bytes内存空间。(因为int占四个,string占8个包括一个字符指针和一个表示长度的整数);如果该对象有虚函数,则指向该对象的指针还要加4bytes,因为该对象会含有一个vptr指针。
我们不知道void*指针将涵盖怎样的地址空间,这就是void*只能够支持一个地址,而不能够通过它操作所指object的原因。
加上多态之后:
用一个父类指针指向一个子类对象,则这个指针所涵盖的地址只包含子类对象中的父类部分。可以用dynamic_cast和static_cast将父类指针转换为子类指针。
一个指针或引用之所以支持多态,是因为它们并不引发内存中任何“与类型有关的内存委托操作”;会受到改变的,只有它们所指向对象的内存的“大小和内容的解释方式”。
然而,如果将子类对象赋值给父类对象,子类对象的子类部分会被切割,父类部分保留。于是多态不在呈现。而一个严格的编译器可以在编译时期解析虚函数,但编译时期的虚函数为静态虚函数,也就是父类的虚函数,所以回避了virtual机制(在运行期解析的虚函数为指针所指对象的虚函数)。