前言
<<Inside The C++ Object Model>>Stanley B.Lippman著;中文版《深度探索C++对象模型》侯捷翻译。看到大家都认为是本好书,所以从Chinapub中购买会来。看了一遍后觉得昏昏沉沉,主要是很琐碎,其中此书有点过时感觉,中文出版时在2001年,英文原版写的时后大概是1994年到1998年之间;所举的列子关于编译器如何解决,有点给人过时感觉。Stanley B.Lippman对写作看来不太在行,侯捷翻译时纠正了很多错误。但是侯捷风格喜欢中文夹带英文的习惯,好比是“你今天shopping了吗?”;书中有很多小刮号,小刮号里面的话很多是废话。所翻译的中文有些很不流畅。以上几个方面对阅读和理解带来很大的不便,所以本鲨做做笔记啦!!
第一章关于对象
1 C++对象模型 Stroustrup当初设计C++时对象模型是从简单对象模型派生而来的,并对内存空间和存取时间做了优化,在此模型中非静态数据成员被配置于每个对象之内;静态数据成员,静态成员函数,非静态成员函数被配置在对象之外;虚函数也被配置在对象之外,而且与虚函数表(vbtl)一一对应,对象中有个虚函数表指针指向虚函数表(vptr),虚函数表第一个位置放的是对象类型信息(RTTI)
class Point
{
public:
Point(float xval);
Float x() const;
Static int PointCount();
~Point();
private:
float x;
static int pointcount;
}
class Point
{
public:
Point(float xval);
Float x() const;
Static int PointCount();
Virtual ~Point();
Virtual double x() const;
private:
float x;
static int pointcount;
}
在单继承,多继承及虚继承后的对象模型是 基类的对象直接加到派生类对象的头部,如果是虚基类的话就是采用指针(bptr)指向虚基类
2 C++对象的大小
由其 非静态数据成员的大小; 内存对齐的需求而填补上去的空间大小;支持虚函数而内部产生的额外负之和。(内存对齐:将大小调整到某个数的倍数,在32位机器上是4的倍数)
独立类
class zoo
{
public:
zoo();
virtual ~zoo();
virtual void rate();
private:
int loc
string name;
};
main()
{
zoo za(“zoey”);
zoo *pza=&za;
}
单继承
class Bear :public Zoo
{
public:
Bear();
~Bear();
void rtate();
virtual void dance();
private:
enum Dances{….};
dance dances_know;
int CellBlock;
};
多继承和虚继承跟单继承一样的。注意点类中有char a 的数据成员 a只占一个字节,内存对齐后补上了3个空字节,在以后继承中派生类不会对基类重新进行内存对齐。也就是说派生类中如果有char b,c,d 不会和基类中a 共用4个字节空间。
C++对象模型之二 构造语句笔记
构造函数
你是不是这样认为:
1. 任何类如果没有定义默认构造函数,编译器就会合成一个;
2. 编译器合成的会明确设定类中每个数据成员的默认值;
事实并非如此的,只有C++编译器需要的时候才会合成个默认构造函数。
并且在4种情况下会那么做。
第一种情况:类中带有对象成员
class A{……};
class B
{
private:
A a; //对象成员
int x;
}
合成的构造函数
B::B ( )
{
a.A::A();
}
假如你定义了个构造函数
A::A ( )
{
x=0;
}
编译器会追加代码到里面去
A::A ()
{
a.A::A();
x=0;
}
第二种情况 基类中有默认构造函数(包括被合成的)
和第一种一样,你定义了很多构造函数,编译器会追加代码到每个构造函数中
第三种情况 带有虚函数的类 (包括纯虚函数)
编译器的必要的操作
1. 一个虚函数表将产生,内放虚函数的地址;
2. 一个虚函数指针将产生,指向虚函数表地址;
所以编译器会为每个对象设定vptr的初始值,放置适当的虚函数表地址,对于类所定义的构造函数追加代码完成这些事。
第四种情况 虚继承
class X{publci: int i ; };
class A :public virtual X { public : int j ;};
class B: public virtual X { public: double d;};
class C: public A,public B{ public: int k;};
void foo(const A *pa) { pa->i=1024;} //无法在编译期决定出pa->X::i 的位置
main() {foo(new A); foo(new C);} // 可能转变为 void foo (const A*pa) { pa->_vbcx->i=1024} vbcx是对象指向类X的指针,当然要在构造函数中确定。
拷贝构造函数
拷贝构造函数: 以一个对象的内容去初始化另个对象。 (关键在于初始化)
有三种情况下会调用拷贝构造函数
class x {……};
X x; X xx=x //不是赋值操作而是拷贝构造函数
Void foo (X x); // 对象参数
FooBar() { X x; return x;} // 返回对象
如果类没有提供显示的拷贝构造函数,编译器采用的是位逐次拷贝。也就是把对象数据成员值拷贝到另个对象上,如果遇到对象成员就会递归进去。
class String
{ public: //类没有提供显示的拷贝构造函数
private:
char *str ;
int len;
};
class word
{
public: //类没有提供显示的拷贝构造函数
private:
int occurs;
String str: //类含了对象成员
}
位逐次拷贝时先位逐次拷贝int occurs 遇到word后递归进 class String 位逐次拷贝。
当以下情况下编译器不会采用位逐次拷贝,而是生成默认的拷贝构造函数
1. 当类内含一个对象成员,而后者的类中声明了一个拷贝构造函数时 (包括程序员写的,编译器合成的)
2. 当类继承一个基类时,而后者存在一个拷贝构造函数时
3. 当类声明了虚函数时
4. 当类派生自一个继承链中有虚继承时
1.当类内含一个对象成员,而后者的类中声明了一个拷贝构造函数时
class String
{ public:
String (const char *);
String (const String &) //类提供显示的拷贝构造函数
~String();
private:
char *str ;
int len;
};
class word
{
public: //类没有提供显示的拷贝构造函数
private:
int occurs;
String str:
}
//就会合成一个 : word::word(const word &wd) { str.String::String( wd.str); occurs=wd.occurs;}
2 当类继承一个基类时,而后者存在一个拷贝构造函数时
class word :public String
{
public: //类没有提供显示的拷贝构造函数
private:
int occurs;
String str:
} //就会合成一个
3 当类声明了虚函数时
class word
{
public: //类没有提供显示的拷贝构造函数
virtural cout(); //类声明了虚函数
private:
int occurs;
String str:
} // 同理与构造函数一样的理由,拷贝构造函数要处理虚函数指针和虚函数表。
4 当类派生自一个继承链中有虚继承时
class Zoo{…}; class Racconn :public virtual Zoo {….} ; class ReadPanda : public Racconn {…};
这个时候不是一个类对象要另个类对象做初始化而是基类对象要派生类对象来初始化
Raccoonn rocky; Racconn little=rocky; // 简单的位逐次拷贝就行了
RedPanda litteRed; Racconnn littecritter = rocky ;
// 简单的位逐次拷贝就不行了,编译器必须明确地初始化littercritter 虚继承的指针。
C++对象模型之三 数据成员笔记
首先纠正第一章中关于静态成员在对象之外,对象用指针连接,实际上对象没有用指针去连接他们,而是通过类存取的. 下面是空类的虚继承.
Class x {};
class y :public virtual x{};
lass z:public virtual x{};
class A: public y,public z {};
其大小是 X =1 Y=4 Z=4 A=8;虚继承基类 X 隐藏1字节字符使的类两个对象在内存中有不同的地址。
数据成员的布局
非静态数据成员在类对象中的排列顺序和其被声明的顺序一样,任何中间介入的静态数据成员都不会放进去。在同一个存取节中各个成员并不一定连续排列,符合较晚出现的成员在类对象中有较高的地址。编译器可能合成些内部使用的数据成员以支持整个对象模型。编译器把多个相同的存取节合并在一起。
数据成员的存取
1 静态数据成员:在对象之外,并做为全局变量看,每一个对象的存取许可及类的关联并不会导致空间和时间的负担。
Point3d origin, *pt=&origin;
Origin.chunSize=250;
Pt->chunSize=250;
被内部转化为:
Point3d::chunkSize=250; Point3d::chunkSize=250; 无论是对象还是指针都一样。
如果静态数据成员经函数调用:foobar ( ).chunkSize=250; 被转化为:(void) foobar(); Point3d::chunkSize=250;
取一个静态数据成员的地址,得到的是指向该数据类型的指针,而不是一个指向其类成员的指针。
&Point3d::chunkSize; è const int *
2 非静态数据存取
欲要对非静态数据存取,编译器需要把类对象的起始地址加上数据成员的偏移量。
Origin.y=0.9; è &Origin+(&Point3d::y-1);
虚继承比其它方式存取数据成员会比较慢点。用对象和指针存取虚继承而来的数据成员时指针存取操作必须延迟到执行期,而对象在编译期就固定了。
继承与数据成员
在C++模型中派生类对象所表现出来的东西,是自己的成员们加上其基类们的成员的总和,基类成员总是先出现,但是虚继承类除外。
1 单继承: 类Point3d单继承于Point2d
2 单继承含虚函数:Point3d单继承于Point2d,Point2d有virtual void foobar();
3 多继承:Point3d继承于Point2d; Vertex3d 多继承于 Point3d 和Vertex
4虚继承:Point3d virtual Point2d ; Vertex :virtual Point2d; Vertex3d:public Point3d,public Vertex;
1 每个对象会多一个指针指向虚继承基类 (Point2d);
2 每次继承时存取时间会增加,因为要调整一些指针;
所以有两种对象模型:一种就是增加指针继承时调整指针;另一种是把虚继承基类的偏移地址放在虚函数表第一位值。
对象成员的效率
我使用BCB5 在塞扬A466 128SDRAM 下
void __fastcall TForm1::Button1Click(TObject *Sender)
{ //单个变量
float pA_x,pB_x,pA_y,pB_y,pA_z,pB_z;
pA_x=1.725;pA_y=0.875;pA_z=0.478;pB_x=0.315;pB_y=0.317;pB_z=0.838;
long StartTime=GetTickCount();
for(int i=0;i<10000000;i++)
{
pB_x=pA_x-pB_z;
pB_y=pA_y+pB_x;
pB_z=pA_z+pB_y;
}
long EndTime=GetTickCount();
Label1->Caption=EndTime-StartTime;
}
void __fastcall TForm1::Button2Click(TObject *Sender)
{
class Point3d
{
public:
Point3d(float xx=1.725,float yy=0.838,float zz=0.315): _x(xx),_y(yy),_z(zz){}
float & x() {return _x;}
float & y() {return _y;}
float & z() {return _z;}
private:
float _x,_y,_z;
};
Point3d A,B;
long StartTime=GetTickCount();
for(int i=0; i<10000000;i++)
{
B.x()=A.x()-B.z();
B.y()=A.y()+B.x();
B.z()=A.z()+B.y();
}
long EndTime=GetTickCount();
Label2->Caption=EndTime-StartTime;
}
剩余的代码没有给出,大家自己可以做做,对象不会很慢,纳闷结构体比单个变量快?
|
未优化 |
优化 |
单个变量 |
0.530 (秒) |
0.521 (秒) |
结构体 |
0.260 (秒) |
0.200 (秒) |
数组 |
0.531 (秒) |
0.521 (秒) |
对象 |
2.083 (秒) |
0.521 (秒) |
书上还做了各种继承的测试结论:
1 关闭优化后内联函数存取比较慢,所以程序员应该实际测试下,不要光凭推理或常识判断。
2 有时侯优化操作并不一定总是有效运行。
3 虚继承在优化后同样效率令人失望。
指向数据成员的指针
class Point3d
{
public:
virtual ~Point3d();
protected:
static Point3d origin;
float x,y,z;
};
因为 vptr 占4个字节 float 占4个字节 所以 vptr放在头部三个数据成员的偏移量是 4,8,12;vptr在尾部0,4,8。
如果你取数据成员的地址 &Point3d::x 返回的总是多1个字节,为什么?
因为
float Point3d::*p1=0;
float Point3d::*p2=&Point3d::x;
if (p1==p2) //无法区分 所以不得不加上1个字节
而 &Point3d::x; 和 &origin.Z 是不同的 前者得到的是在类中的偏移量,后者得到是真正的内存地址。
指向成员的指针效率
|
未优化 |
优化 |
直接存取 |
1.42 |
0.8 |
指针指向已绑定的成员 |
3.04 |
0.8 |
指针指向已数据成员 |
5.34 |
0.8 |
指向数据成员的指针
|
未优化 |
优化 |
没有继承 |
5.34 |
0.8 |
单一继承(三层) |
5.34 |
0.8 |
虚继承(一层) |
5.44 |
1.6 |
虚继承(二层) |
5.51 |
2.14 |
虚继承它妨碍了优化,每一层中导入了额外的层次间接性
pB.*bx 转化为 &pB->__vbtr+(bx-1) 而不是 &pB+(bx-1)
C++对象模型之四 成员函数笔记
成员函数的调用方式
非静态成员函数:C++设计准则之一就是非静态成员函数至少和一般的非成员函数一样的效率。
所以编译器内化成非成员函数,其步骤 1改写函数的原型以安插一个额外的参数到成员函数中,用以提供一个存取管道,使类对象得以调用该函数。 这个参数为 this 指针。
Point3d::Mangitude( Point3d *const this);
2 将每个对非静态数据成员的存取操作改为经由this指针来存取。
{ return sqr( this->_x * this->_x+ this->_y * this->_y+ this->_z * this->_z); }
3 将成员函数重新写成一个外部函数,对函数名称进行 mangling 处理。
Extern mangitude __7Point3dFv ( regiester Piont3d *const this)
虚成员函数 register float mag = Magnitude(); è register float mag = ( *this->vptr[2]) (this);
1 编译器生成vptr指针;2 虚函数表中的索引值;3就是this 指针
静态成员函数 obj.Magnitude(); ptr->Magnitude(); 都会转化成 Magnitude__7Point3dSFv();
虚成员函数
如果类中有虚函数,就会有虚函数表指针(vptr),表格的地址,大小和内容都是编译掌握住了。
执行时要做的是在特定的虚函数表槽激活虚函数。虚函数包括:1 派生类改写基类的虚函数,2 继承基类的虚函数,3 纯虚函数。
Class Pont
{
public:
virtual ~Point();
virtual Point& mult(float)=0;
float x() const {return _x;}
virtual float y() const { return 0;}
virtual float x() const { return 0;}
protecetd:
point (float x = 0.0);
float _x;
};
当一个类派生自Point时,会发生什么?
1 可能它继承基类所声明的虚函数的函数实体,正确地说是该函数实体的地址会被拷贝到派生类的虚函数表相对应的槽中
2 可能 它可以使用自己的函数实体,者表示它自己的函数实体地址必须放在对应的槽中
3 可能它可以加入一个新的虚函数表,这时候虚函数表的尺寸会增大一个槽,而新的函数实体地址会被放进该槽中。
多重继承下得虚函数
复杂度围绕在第二个及后继的基类身上,以及必须在执行时调整this指针
class Base1{
public: Base1();
virtual ~Base1();
virutal void speckclearly();
virutal Base1 *clone() const;
protected: float data_Base1;
};
class Base2{
public: Base1();
virtual ~Base2();
virutal void mumble();
virutal Base1 *clone() const;
protected: float data_Base2;
};
class Derived :public Base1,public Base2
{
public:
Derived();
Virtual ~Derived();
Virtual Derived *clone() const;
Protected : float data_Derived;
};
Base2 *pbase2 = new derived;
è 调整: Derived *temp = new Derived; Base2 *pbase2 = temp ? temp + sizeof(Base1):0;
当第二个基类的指针调用派生类的虚函数时,必须在执行中完成调整。也就是offset
this+=sizeof(base1);
Derived::~Derived(this);
虚函数在虚继承下
class Point2d{
public: Point2d( float = 0.0, float = 0.0);
virtual ~Point2d();
virtual void mumble();
virtual float z();
protected: float _x , _y ;
};
class Point3d :public virtual Point2d{
public: point3d (float = 0.0 , float = 0.0 );
~Point3d();
float z();
protected: float _z;
};
由于Point2d和Point3d的对象不再相符,两者之间的转换也就需要调整this指针.
函数效率
|
未优化 |
优化 |
内联成员 |
4.70 |
0.08 |
友元函数 |
6.13 |
4.43 |
静态成员 |
6.13 |
4.43 |
非静态成员 |
6.13 |
4.43 |
虚函数 |
6.90 |
4.76 |
虚函数多继承 |
7.06 |
4.90 |
虚函数虚继承 |
7.07 |
5.44 |
指向成员函数的指针
取个非静态成员函数的地址,如果它不是虚函数,则得到的是在内存中真正的地址。不过需要绑定在某个对象地址上。使用一个成员函数指针,如果并不用于虚函数,多重继承,虚继承等情况不会比一般的指针成本高。
指向虚函数的指针
float (Point:: *pmf) () = &Point::z; //取虚函数的地址得到是在虚函数表的索引值
( * ptr->vptr[ (int) pmf ] ) (ptr); //通过调用会被内部转化为一个编译时期的式子
区分PMF调用的是虚函数还是非虚函数方法:
( ( ( int ) pmf ) & ~127 ) ? (* pmf ) ( ptr ) : ( *ptrèvptr[ ( int) pmf ] ( ptr ) );
指向多重继承和虚继承的指针
采用结构体
struct _mptr
{
int delta; //表示this指针的offset
int index;
union {
ptrtofunc faddr;
int v_offset; //虚函数在虚函数表的索引
};
};
( ptr-> *pmf) (); 会变成 (pmf.index < 0 )? ( pmf.faddr ) (ptr ): ( ptr->vptr[ pmf.index ]( ptr ) );
指向成员函数指针的效率
|
未优化 |
优化 |
非成员函数指针 |
6.12 |
4.30 |
成员函数指针 |
6.38 |
4.30 |
多重继承非虚函数指针 |
6.32 |
4.30 |
虚继承非虚函数指针 |
6.84 |
4.70 |
虚函数指针 |
7.39 |
4.70 |
多重继承虚函数指针 |
8.72 |
5.90 |
虚继承虚函数指针 |
8.80 |
5.84 |
内联函数
一般而言处理内联函数有两个阶段
1 分析函数定义,以决定函数的内联的本资。如果判断其不可内联就会转化成静态函数。
2 内联函数的展开是在调用点上,这样会带来参数求值和临时对象管理。
形式参数
inline int min( int I,int j) { return I<j ? I:j ;}
main()
{
int minval ;
int val1=1024;
int val2=2048;
minval = min (val1,val2);
minval = min (1024,2048);
minval = min (foo(),bar()+1);
}
第一调用的 minval = val1 <val2 ? val1 : val2;
第二调用的 minval = 1024;
第三调用的 int t1; int t2; minval = (t1 =foo() ),(t2 = bar()+1) t1<t2 ? t1: t2; / / 临时对象
在inline扩展时,每一个形式参数都会被实际参数取代。如果会导致对于实际参数多次求值就会引入临时对象。
局部变量
inline int min(int i,int j)
{
int minval = i < j ? i : j ;
return minval ;
}
main()
{ int loval _var,minval;
minval = min (val1,val2);
}
就会被扩展为:
{ int local _var, minval ;
int__min_lv_minval;
minval = (__min_lv_minval = val1 < val2 ? val1: val2), __min_lv_minval;
}
一般而言,内联函数中的每个局部变量都必须被放在函数调用的一个封闭区间中,拥有独一无二的名称。
如果内联函数以单一表达式扩展多次,那么每次都需要一组局部变量。以分离多个式子被扩展多次,那么只需要一组局部变量,就可以重复使用了。
以上是多个同类对象共享成员函数,每个对象只有数据不同而已
C++对象模型之五 构造 析构 赋值笔记
1 无继承下的构造
用C++来编译
typedef struct
{
float x,y,z;
} point
编译器会声明一个:无效的默认构造,无效的析构,无效的拷贝,无效的赋值操作。然而并不实现它们。
当写成类时体积并没有改变
class point
{
public:
point (flaot x=0.0,float y = 0.0, float z = 0.0):_x(x),_y(y),_z(z){}
//没有构造,析构,拷贝
private:
float _x, _y, _z;
};
明显初始化列表带来的三项缺点
1. 只有当类成员都是public时,才有效。
2. 只能指定常量,因为它们在编译时期可以被评估求值
3. 由于编译器并没有自动化施行之,所以初始化行为的失败可能性会比较高
2 为继承做准备
class point
{
public:
point (flaot x=0.0,float y = 0.0):_x(x),_y(y){}
//没有构造,析构,拷贝
virtual float z();
private:
float _x, _y;
};
体积因有了vtbl所以多vptr 4个字节的空间。
1 构造函数被附加了些代码以便初始化vptr.被加在基类构造函数调用之后,程序员写的代码之前,可能是
point * point ::point( point *this,float x,flaot y):_x(x),_y(y)
{
this->_vptr_point = _vtbl_point; //初始化vptr
this->_x = x; //扩展成员初始化列表
this->_y = y;
return this;
}
2 合成一个拷贝函数和一个拷贝赋值操作
inline point * point::point ( point *this, const point &rhs)
{
this->_vptr_point = _vtbl_point ;
return this;
}
赋值操作采用位拷贝技术。
继承体系下的构造
一般而言编译器所做的扩充操作大约如下
1. 记录在成员初始化列表中的数据成员,初始化操作会被放进构造函数本身,以声明为序。
2. 如果有个成员没有出现在初始化列表之中,但它有个默认构造函数,将被调用。
3. 如果类对象中有vptr它将被设定初始值,指向vbtl。
4. 所有上一层基类构造必须被调用,以声明为序。
1. 如果基类被放在初始化列表中,那么任何明确指定的参数都应该传递过去。
2. 如果基类没有被放在初始化列表中,而它有个默认构造函数将被调用。
3. 如果基类是多重继承下的第二或后继的基类,那么this指针必须调整。
5. 所有虚基类构造函数必须调用,从左到右,从最深到最浅:
1. 如果类被放在初始化列表中,那么参数将传过去,没有放在,而类有构造将调用。
2. 此外,类中的每个虚基类子对象的偏移量必须在执行期可被存取。
3. 如果基类是最底层的类,其构造可能被调用,某些机制将会放进来。
class point
{
public:
point (flaot x=0.0,float y = 0.0)
point (const point &);
point & operator = (const point &);
virtual ~point();
virtual float z(){return 0.0;}
private:
float _x, _y;
};
3虚继承下
class point3d:public virtual point
{
public:
point3d ( float x = 0.0, float y=0.0, float z =0.0):point (x,y),_z(z){}
point3d( const poin3d& rhs):point(rhs),_z(rhs.z){}
~point3d();
point3d & operator = (const point3d &);
virtual float z() {return _z;}
protected:
float _z;
};
下面就是point3d的构造函数被扩充的内容
point3d *point3d::point3d( piont3d *this ,bool _most_derived,float x, float y, float z)
{
if ( _most_derived != false) //在C++中虚基类的构造函数由最底层的类负责调用,所以要判断自己有没有派this->point::point(x,y); //所以要判断自己有没有派生类
this->_vptr_point3d = _vtbl_point3d; // 初始化自己的vptr
this->vptr_point3d_point = _vtbl_point3d_point; //初始化基类的vptr
this->_z = rhs.z;
return this;
}
4vptr初始化
class Vertex:virtual public point{….};
class Vertex3d:public point3d,public Vertex {…..};
class Pvertex:public Vertex3d{….};
它们中都有个virtual size()
构造调用顺序是:由根源而末端,由内而外。在构造函数中调用虚函数被评估为本类的函数而不是派生类的,多态特性在对象构造中无效。
1. 在派生类中所有的虚基类及上一层基类的构造函数被调用。
2. 上述完成后对象的vptr被初始化,指向vbtl
3. 如果有成员初始化列表的话,将在构造内展开,必须在vptr被设定后进行
4. 最后执行程序员提供的代码。
Pvertex ::Pvertex ( float x, float y, float z):_next(0),Vertex3d(x,y,z),point(x,y)
{
if (spyOn)
cerr<<”within pvertex::pvertex()”<<”size:”<,size()<<endl;
}
被扩充为:
Pvertex *Pvertex::Pvertex( Pvertex *this ,bool _most_derived, float x, float,y,float z)
{
if ( _most_derived != false)
this->point::point(x,y);
this->vertex3d::vertex3d(x,y,z);
this->_vptr_Pvertex = _vtbl_Pvertex;
this->_vptr_point_Pvertex = _vbtl_point_Pvertex;
if ( spyOn)
cerr<<”within Pvertex::Pvertex()”<<”size:”<<(*this->_vptr_Pvertex[3].faddr)(this)<<endl;
return this;
}
对象赋值
赋值操作将在以下情况下需要
1. 当类内带有一个对象成员,而其类有个赋值操作。
2. 当本类的基类有个赋值操作时。
3. 当本类声明了任何虚函数。
4. 当本类继承自一个虚继承类时,无论基类是否有赋值操作。
Inline point & point::operator = ( const point &p)
{
_x = p._x;
_y=p._y;
return this;
}
class point3d :: virtual public point
{…..}
编译器将为point3d合成一个赋值操作
inline point3d & point3d::operator = (point3d *const this,const point3d &p)
{
this->point::operator=(p);
_z=p._z;
return *this;
}
因为C++标准没有赋值列表,所以造成派生类赋值操作中将会重复调用基类的赋值操作
inline vertex3d& vertex3d::operator=(const vertex3d &v)
{
this->point::operator=(v);
this->point3d::operator=(v); //内含有this->point::operator=(v);
this->vertex::operator=(v); //内含有this->point::operator=(v);
return this;
}
建议尽可能不要允一个虚基类的赋值操作,不要在任何虚基类中声明数据。
对象功能
测试对象的构造和赋值操作成本
struct point3d {float x,y,z;};
class point3d {public : float x,y,z;};
|
优化 |
未优化 |
结构体 |
5.84 |
7.22 |
内联构造逐位 |
6.00 |
7.33 |
内联构造带虚函数非逐位 |
7.67 |
13.05 |
单一继承内联逐位 |
6.26 |
7.33 |
单一继承内联非逐位 |
10.35 |
17.74 |
单一虚继承内联非逐位 |
17.29 |
23.93 |
多重继承内联逐位 |
6.26 |
7.33 |
多重继承内联非逐位 |
12.39 |
20.39 |
多重虚继承内联非逐位 |
19.31 |
26.80 |
析构
如果类没有定义析构函数,那么只有在类内带的对象成员并且对象成员有析构或者基类拥有析构的情况下,编译器才会自动合成出一个来,否则是不需要的。
class point
{
public:
point (float x = 0.0 ,float y=0.0);
point (const point &);
virtual float z();
….
Private :
Float _x,_y;
};
既是类point拥有虚函数,编译器也不会合成一个析构函数。既是类lines数据成员是point beging,end; 也没有必要因为point本身没有析构函数。
你应该拒绝被称为对称策略的想法:“你已经定义了构造就必须定义个析构”。
决定为类是否在程序层面提供析构函数要看: 类对象生命在哪里结束?需要什么操作才能保证对象的完整性。这也是构造和析构函数什么时候起关键作用。
Main()
{
point pt;
point *p=new point3d;
foo( &pt,p);
….
Delete p;
}
pt和p在函数foo()前要初始化,否则使用者必须明确提供坐标,一般而言类的使用者这没有办法检验一个本地变量或者堆变量是否已经初始化了,所以这时构造函数工作是有必要的。同样在delete p之前对p是否要进行处理保证对象的完整性 比如:p->x=0;p->y=0;
目前对于析构的一种最佳策略就是维护两份析构实体
1 一个完整对象实体,总是设定好vptrs,并调用虚基类析构函数。
2 一个基类子对象实体,除非在析构函数中调用一个虚函数,否则不会去调用虚基类的析构函数并设定vptr
一个对象的生命结束于其析构函数开始执行之前时,由于每个基类析构函数都轮番被调用,所以派生类实际上变成一个完整的对象。如Pvertex对象归还内存空间时,会依次变为一个Vertex3d,Vertex,Point3d,Point.
析构顺序:
1 析构函数本身首先执行
2 如果类有成员对象,而后者拥有析构函数,那么它们会以声明的顺序相反的顺序调用。
3 如果对象带有个vptr,则现在被重新设定,指向适当的基类vtbl.
4 如果有任何直接上一层非虚基类拥有析构函数,那么它们会以声明的顺序相反的顺序调用。
5 如果有任何虚基类拥有析构函数,并且时最尾端的类,那么它们会以原来的顺序相反的顺序调用。
C++对象模型之六 运行期笔记
1 对象的构造和析构
尽可能地把对象的声明放在使用它的那个程序区段附近。
foo()
{
Point pointa; //马上构造
if ( cache)
return 1; // 这里会析构是多余的,而且是浪费的
pointa.run()
return 0; //这里也会析构是正常的。
}
foo()
{
if ( cache)
return 1; // 这里就不会了
Point pointa; //马上构造
pointa.run()
return 0; //这里也会析构是正常的。
}
2 全局对象
一般会对没有初始化的对象自动初始化为零。在主程序里最前面初始化这些全局对象,在程序退出之前将会自动地释放全局对象。
3 对象数组
point konts[10];
假如point 有构造和析构函数的话,将轮流构造或析构每个对象。假如有虚基类将采用vec_vnew()来构造否则是vec_new().
Void *vec_new ( void *array, size_t elem_size,int elem_count, void(*构造)(void*),void(*析构) (void * char))
Void * vec_delete(void *array,size_t elem_size,int elem_count,void(*析构)(void *,char));
point konts[10];
Vec_new( &kontes,sieof ( point ),10,&point::point,0);
假如:point konts[10]={point(),point(1.0,1.0,0.5),-1.0};
对于那些明显获得初值的元素,vec_new不再有必要了,但是对于那些没有初始化的vec_new还是有必要。
4 new 和 delete
int *pi = new int (5);
delete pi;
变为:
int *pi;
if ( pi = _new ( sizeof (int) ) )
*pi = 5;
if ( pi !=0)
_delete ( pi );
假如是对象话
point3d *orgin = new point3d; è if( origin = _new (sizeof(point3d))) origin = point3d::pointed(origin);
异常下:
if ( origin = _new (sizeof(point3d)))
{ try { origin = point3d::point3d ( origin ); }
catch (…) { _delete (origin); throw; }
}
delete origin;è if (origin !=0) { point3d::~point3d(origin); _delete (origin);}
数组new
int * p_array = new int [5];è int *p_array=(int*)_new(5*sizeof(int));
假如它不是类对象,或者类对象没有构造函数的话 vec_new不会调用,只是单纯地获得释放内存。
Point3d *p_array = new point3d [10]; è
Point3d *p_array;
P_array=vec_new(0,sizeof(point3d),10,&point3d::point3d,&point3d::~piont3d);
预先定义好的重载new
void * operator new ( size_t, void *p ) { return p; }
调用: point *ptw = new ( arena) pont; // 其中arena 是指向内存的一个区块。
è point *ptw=(point*) arena; if(ptw != 0) ptw->point::point();
arena所表现的真正指针类型,必须指向相同类型的类,要不就是一块新鲜内存,足够容纳下该类型的对象.
派生很显然不在支持之内.
新鲜内存这样配置而来: char *arena = new char [ sizeof ( point ) ];
相同类型的对象可以这样获得: point *arena = new point;
无论如何 新的pont的存储空间的确是覆盖了arena的位置. 一般而言预先定义好的重载new不支持多态.
5临时对象
C++标准容许编译器对于临时对象的产生有完全的自由度
临时对象的被摧毁,应该是对完整表达式求值过程中最后一个步骤.该完整表达式造成临时对象的产生.
String v= s+a+b+c; 临时对象有: temp1=s+a; temp2=temp1+b; temp3=temp2+c; 在临时对象3时 临时对象1 虽然已经没有什么用了,但是要等到整个表达式完成后才可以释放内存.
凡是含有表达式执行结果的临时对象,应该保留到对象的初始化操作完成为止.
上面的表达式产生了3个临时对象,在整个表达式结束后,temp1,temp2 将被释放,temp3 要等到 v=temp3 后方可释放.
如果一个临时对象被绑定于个引用后,对象将保留,直到被初始化引用的生命结束,或直到临时对象的生命范畴结束-----要看哪种情况先来而定
const string &space = a+b; 这里的临时对象将和space结束而结束,或者超过了生命范围之内.
Const string &s= foo(); // point foo() { return point;}这里的临时对象超过了生命范围之内.s将是错误的
这个条没有对指针绑定临时对象作出明确的解释
const char * a=b+c+d;
C++对象模型之七 模板 异常 RTTI笔记
下面关于模板的三个问题:
1 模板的声明时会发生什么
2 如何实例化出类对象内联成员
3 如何实例化出成员,模板成员函数,模板静态成员.
Template <class T>
Class point
{
public:
enum Status { unallocated, normalized };
point (T x=0.0,T y=0.0, T z=0.0);
~point ();
void * operator new ( size_t );
void * opertaor deleted (voide * , size_t );
private:
static point<T> * freeList;
static int ChunSize;
T _x, _y, _z;
};
当编译器看到template class 声明时,它什么反应也没有,上述的静态数据成员,嵌套枚举 不可用. 虽然enum status 的真正的类型在所有的point实例中都一样.但是它们必须通过模板point类的实例来存取和操作.
Point <float >:: Status S; // OK
Point ::Status S;//Error
指针: const point < float > * ptr = 0; 不会实例化;
引用: const point < float > & ref = 0; 会实例化
è point < float > temp ( floa t (0 ) ); const point < float > & ref = temp;
模板的错误报告: 所有的语法错误被检测出来,但不做类型检查.
模板中的名称解决方式
extern double foo ( double ); //类S的定义
template < class T>
class S
{
public:
void invariant () { member = foo ( val); }
T dependent() { return foo ( member) ;}
private:
int val;
T member;
};
extren int foo (int );
S<int> S1;
S1.invariant();
S1.dependent();
模板中对于成员名称的解决结果时根据这个名称的使用是否与”用以实例化出该模板的参数类型有关” 而决定.没有关就采用类的定义来解决, 有关就采用实例化的参数类型.
因此: S1.invariant();中的foo 调用的 extern double foo ( double ); 因为void invariant () { member = foo ( val); }中的val 是int val; 类型无关.而 S1.dependent(); 中的foo 调用的 extern int foo ( int );因为T dependent() { return foo ( member) ;}中的member 是T member; 类型有关.
成员函数的实例化
只有在成员函数使用时后C++标准才要它们被实例化出来.但是并不精确遵循.愿意如下:
1 空间和时间的考虑. 如果类中有100个成员函数,但你的程序只针对某个类型的7个函数,那么其他的193个函数都要被实例化将花费大量的时间和空间.
2 尚未实现的机制,并不是一个模板实例化出来的所有类型就一定能够完整支持一组成员函数所需的所有运算符.
Point < float > *ptr = new point < float >;
用到了 void * operator new ( size_t ); point (T x=0.0,T y=0.0, T z=0.0);
目前编译器有两个策略: 1是编译时期程序代码必须在程序文本文件中备妥当,2 是连接时用工具引导编译器实例化行为.
1 编译器如何找出函数的定义?
1是包含在模板程序文本中,2 要求一个文件命名规则.
2 编译器如何只实例化程序中用到的成员函数?
1 根本忽略,把一个已经实例化的类所有的成员函数都生产出来. 2仿真连接操作: 检测哪个函数真正需要.
3 编译器如何阻止成员定义在多个obj文件中都被实例化?
1 产生多个实例在连接时候只留下一个. 2 由使用者来引导”仿真连接阶段”的实例.
Borland 支持所有问题的第一中方法.
异常处理
一般而言异常处理机制需要与编译器所产生的数据结构以及执行期的一个异常库精密合作.
编译器在程序速度和大小方面做出选择
1 为了维持运行速度,编译器可以在编译时期建立起用于的数据结构.但编译器可以忽略这些结构直到异常
2 为了维护程序大小,编译器可以在执行期建立起用于的数据结构, 但编译器只有在必要的时候才建立数据结构.
异常可以不程序划分为多个区域: try block 以外 和try block以内以及整个区域.
void mumble ( void * arena )
{
point *p = new point ;
try
{
smlock ( arena );
}
catch (…)
{
smunlock ( arena );
delete p;
throw ;
}
smunlock (arena);
delete p;
}
为什么 point *p = new point ;不在try block以内? 因为它之前没有什么要释放的东西.虽然它自己也会抛出异常,但是它会自己处理调资源释放的问题.
支持异常会使成员对象或基类子对象的类的构造函数复杂化. 比如: 类X有成员对象 A,B,C.它们都有一对的构造和析构函数. 但A的构造函数抛出异常,那么A,B,C的析构不用调用. 但 B的构造函数抛出异常,那么A的析构要调用,C的析构不用调用. 一样的是 point3d *ptr = new point3d [512]; 假如第100个元素抛出异常,那么前99个要调用析构,后面的就不用调用.
当一个异常发生时,编译器要完成以下事情:
1 检验throw 操作函数
2 决定 throw 操作是否发生在try 区域中
2.1 如果是 编译器把异常类型和每一个catch比较
2.2 如果比较吻合,流程控制权交到catch中
3 如果 throw 操作不发生在try 区域中 或者没有一个catch吻合 那么系统会做 1 释放所有本地存在的对象, 2 从堆栈中将当前的函数展开掉 3 进行到程序堆栈中的下一个函数,然后重复2-3
决定 throw 操作是否发生在try 区域中 函数中会生成区域统计表
如果是 编译器把异常类型和每一个catch比较 系统会采用类型描述器(RTTI)
当异常被丢出时,异常对象会放在相同形式的异常数据堆栈中,从throw传给catch 的是 异常对象地址和类型描述器.一般catch( ex &p) 基本上采用引用 如果是对象,有能发生切割.
对象大小 |
没有异常 |
有异常 |
百分比 |
Borland |
86.822 |
89.510 |
3% |
Microsoft |
60.146 |
67.071 |
13% |
Symantec |
69.786 |
74.826 |
8% |
执行速度 |
|
||
Borland |
78 |
83 |
6% |
Microsoft |
83 |
87 |
5% |
Symantec |
94 |
96 |
4% |
运行期类型识别(RTTI)
向下兼容:
RTTI是后来引进的,并没有使用新的关键字,而是如果声明的虚函数就会有RTTI。
动态类型转换:dynamic_cast可以在运行期决定类型,如果成功将传回指针否则是0 比静态转换成本要贵的多,但是也安全多了。Dynamic_cast < Type > ( &pt) 如果是引用失败后返回的是bad_cast异常。
TYPEID:
使用它可以同样达到引用的目的
if ( typeid (rt)== typeid (fct))
fct &rf=static_cast<fct&>(rf);
typeid 转会来的const 引用 类型为type_info : bool type_info ::operator==(const type_info&) cosnt;
class type_info
{
public:
virtual ~type_info();
bool operator==(const type_info&) const;
bool operator!=(const type_info&) const;
bool before (const type_info &) const;
const char *name() const;
private:
type_info (const type_infor&);
type_info & operator = (const type_info &);
};
RTTI 适合与多态 type 适合于普通类和一般类型。
作者:曾牧暗鲨 完成时间:2003-7-26,27,29,8-3,16,18,19 http://blog.csdn.net/ZengMuAnSha