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 如果有任何虚基类拥有析构函数,并且时最尾端的类,那么它们会以原来的顺序相反的顺序调用。
曾牧暗鲨,网名:大白鲨 2003-8-16