深度探索c++对象模型第五章笔记上

构造、解构、拷贝语意学(Semantics of Constuction,Destruction,and Copy)

假设有以下的代码:

class Abstract_base
{
public:
	virtual ~Abstract_base()=0;//pure virtual function
	virtual void interface() const=0;
	virtual const char*
		mumble() const{return _mumble;}
protected:
	char *_mumble;		
};

因为该class被设计为一个抽象的base class(因为有pure virtual function,使得Abstract_base不能拥有实体),但这个类仍然需要一个明确的构造函数来初始化它的成员变量:_mumble。如果没有初始化操作,那么这个base class的derived class中,作为局部性对象的_mumble将不能决定它自己的初值。即使,我们想要Abastract_base的derived class来提供_mumbel的初值,那么我们必须提供一个带有唯一参数的protected constructor:

Abstract_base::
Abstract_base(char *mumble_value=0):_mumble(mumble_value)
			{	}

一般来说,class的data member应该被初始化,并且只在constructor中或是在class的其他member functions 中指定初值。其他任何操作都将破坏封装性质。

纯虚函数的存在

在base class中,我们是可以为一个pure virtual functions 进行定义的,要不要定义全有class设计者自己决定。
唯一的例外就是pure virtual destructor,class设计者一定要定义它。因为每一个derived class destructor会被编译器加以扩展,以静态调用的调用方式调用其“每一个virtual base class”以及“上一层base class”的destructor。因此,只要缺乏任何一个base class destructor的定义,就会导致链接失败。
c++语言保证的一个前提就是:继承体系中的每一个class object 的destructor都会被调用

一个比较好的方案就是,不要把virtual destructor声明为pure

虚拟规格的存在

==如果一个函数不会对之后的derived class造成影响,那么这个函数就不应该设值为virtual ==。
一般而言,把所有的成员函数都声明为virtual function ,然后再靠编译器的优化操作把非必要的virtual function去除,并不是好的设计观念。

虚拟规格中const的存在

决定一个virtual function是否需要const,当我们真正面对一个abstract base class时,不容易做决定。因为这个决定意外着假设subclass实体可能被无穷次数地使用。不把函数声明为const,意味着该函数不能够获得一个const reference或const pointer。但声明一个函数为const时,之后可能会发现实际上其derived instance必须修改某一个data member,所以,简单点,不在用const就是。

重新考虑class的声明

class Abstarct_base
{
public:
	virtual ~Abstract_base() {}  //不再是pure virtual
	virtual void interface() = 0;  //不再是const
	const char* mumble() const { return _mumble; }//不再是virtual
protected:
	Abstract_base(char *pc = 0) :_mumble(pc) {}
	char *_mumble;
}

5.1“无继承”情况下的对象构造

(1)	Point global;
(2)
(3)	Point foobar()
(4)	{
(5)		Point local;
(6)		Point *heap=new Point();
(7)		*heap=local;
(8)		//..stuff....
(9)		delete heap;
(10)		return local;
(11)	}

L1,L5,L6表现出不同的对象产生方式:global(全局)内存配置,local(局部)内存配置和heap(堆)内存配置。
一个对象(object)的生存周期,是该Object的一个执行属性。local object的生命从L5的定义开始,到L10未知。global object的生命和整个程序的生命相同。heap object的生命从它被new 运算符配置出来开始,直到被delete运算符摧毁为止。
c++Standard有一种Plain old Data的声明形式:

typedef struct
{
	float   x,y,z; 
}Point;

当编译器遇到这种情况时,会为它贴上一个Plain Old Data卷标:然后他们会与在C中的表现一样。

再次强调的是,没有default constructor施行于new运算符所传回的Point object身上。L7对此object有一个赋值操作,如果local曾被适当初始化过,一切就没有问题。

(7)		*heap=local;

因为object是一个Plain Old Data,所以赋值操作只会向C这样的纯粹位搬移操作。
同样delete也是同样的结果。

抽象数据类型

以下是Point的第二次声明,在public接口之下多了private数据,提供完整的封装性,但没有提供任何virtual function:

class Point
{
public:
	Point(float x=0.0,float y=0.0,float z=0.0):_x(x),_y(y),_z(z){}
	//  no copy constructor ,copy operator
	//  or destructor defined...

	//......
private:
	float _x,_y,_z;
};

我们没有为Point定义一个copy constructor或copy operator,因为默认的位语意已经足够,同时也不需要提供一个destructor,因为程序默认的内存管理方法也已经足够。

为继承做准备

第三个Point声明,将为“继承性质”以及某些操作的动态决议做准备,当前我们限制对z成员进行存取操作:

class Point
{
public:
	Point(float x=0.0,float y=0.0):_x(x),_y(y){}
	// no destructor,copy constructor ,or 
	// copy operator defiend 
	
	virtual float z();
	//....
protectd:
	float _x,_y;	
};

在这里并没有定义copy constructor、copy operator、destructor。这个类中的所有members都以数值来储存,因此在程序层面的默认语意之下,运行良好。

virtual functions的引入促使每一个Point object拥有一个virtual table pointer。这个指针提供给我们virtual接口的弹性。
除了每一个class object 多负担一个vptr之外,virtual functions的引入也引发编译器对于Point class产生膨胀作用:

  • 我们所定义的constructor被附加了一些代码,以便将vptr初始化,这些代码必须被附加在任何base class constructors的调用之后,但必须在任何使用者编写的代码之前。
//c++ 伪代码 :内部膨胀
Point *
Point::Point(Point* this,float x,float y):_x(x),_y(y)
{
	//设定object的virtual table pointer(vptr)
	this->__vptr_Point=__vtbl__Point;
	
	//扩展member initialization list
	this->_x=x;
	this->_y=y;

	//传回this对象
	return this;
}
  • 合成一个copy constructor和一个 copy assignment operator,而且其操作不再是trivial。如果一个Point object 被初始化或以一个derived class object赋值。那么以位基础的操作(bitwise)可能给vptr带来非法设定
//c++ 伪代码
// copy constructor 的内部合成
inline Point*
Point::Point(Point *this,const Point &rhs)
{
	//设定object的virtual table pointer(vptr)
	this->__vptr_Point=__vtbl__Point;

	//将rhs 坐标中的连续位拷贝到this对象
	//或是经由member assignment 提供一个member

	return this;
}

编译器在优化状态下可能会把object的连续内容拷贝到另一个object身上,而不会精确地“以成员为基础(memberwise)” 的赋值操作。

如果我们设计的函数中有许多函数都是需要以传值方式(by value)传回一个local class object。那么提供一个copy constructor 就比较合理—即使default memberwise语意已经足够。它的出现可以出发NRV优化。NRV优化后将不需要copy constructor,因为运算结果已经将直接置于“将被传回的object”体内了。

5.2 继承体系下的对象构造

当我们定义object如下: T object;时,会发生什么事呢?
如果T有一个constructor(不论是user提供或是由编译器合成的),它都会被调用。那么constructor被调用时,会发生什么呢? Constructor内带有大量的隐藏码,因为编译器会扩充每一个constructor,扩充的程度视class T的继承体系而定。
一般而言编译器所做的扩充操作大约如下:

  • 1、记录在member initialization list中的data member初始化操作会被放进constructor的函数本身,并以members的声明顺序为顺序。
  • 2、如果有一个member并没有出现在member initialization list之中,但它有一个default constructor,那么该default constructor必须被调用。
  • 3、在那之前,如果class object有virtual table pointers,它们必须被设定初值,指向适当的virtual tables.
  • 4、在那之前,所有上一层的base class constructor 必须被调用,以base class的声明顺序为顺序(与member initialization list中的顺序没关联):
    • a、如果base calss 被列于 member initialization list中,那么任何明确指定的参数都应该被传递过去。
    • b、如果base class没有被列于member initialization list中,而它有default constructor(或default memberwise copy constructor),那么就调用它。
    • c、如果base class 是多重继承下的第二或后继的base class,那么this指针必须有所调整。
  • 5、在那之前,所有的virutal base class constructors必须被调用,从左到右,从最深到最浅。
    • a、如果class被列于member initialization list中,那么如果有任何明确指定的参数,都应该传递过去。若没有列于List之中,而class由一个default constructor,也应该调用它。
    • b、此外,class中的每一个virtual base class subobject的偏移量(offset)必须在执行可被存取。
    • c、如果class object是最底层(most-derived)的class,其constructors可能被调用;某些用以支持这个行为的机制必须被放进来。

再次扩充Point:

class  Point
{
public:
	Point(float x=0.0,float y=0.0);
	Point(const Point&);	//copy constructor
	Point& operator=(const Point&); //copy constructor
	
	virtual ~Point();	//virtual destructor
	
	virtual float z(){return 0.0;}
protected:
	float _x,_y;
};

在声明一个Line class,它由_begin和_end两个点组成:

class Line
{
	Point _begin,_end;
public:
	Line(float =0.0,float =0.0,float =0.0,float =0.0);
	Line(const Point& ,const Point&);
	draw();
	//........
};

每一个explicit constructor 都会被扩充以调用其他两个member class objects的constructors。如果我们定义constructors定义如下:

Line::Line(const Point &begin,const Point &end)
		:_end(end),_begin(begin){}

它会被编译器扩充并转换为:

// c++ 伪代码:Line constructor的扩充
Line*
Line:: Line(Line *this,const Point &begin,const Point &end)
{
	this->_begin.Point::Point(begin);
	this->_end.Point::Point(end);
	return this;
}

由于Point声明了一个Copy constructor、一个copy operator,以及一个destructor,所以Line class的implicit copy consturctor 、copy operator和destructor都将有实际功能(nontrivial):

虚拟继承

考虑下面这个虚拟继承,继承自Point

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 Point3d& rhs)
		:Point(rhs),_z(rhs._z){}
	
	~Point3d();
	
	Point3d& operator=(const Point3d& );

	virtual float z() {return _z;}
protected:
	float _z;
};

试想,如果有下面三种类派生情况:

class  Vertex: virtual public Point{.........};
class Vertex3d: public Point3d,public Vertex{......};
class PVertex : public Vectext3d{........};

在这里插入图片描述

下面就是Point3d中正确地constructor扩充内容:

//c++伪代码
//在virtual base class情况下的constructor扩充内容
Point3d*
Point3d::Point3d{Point3d *this,bool __moset_derived,float x,float  y,float z}
{
	if(__most_derived!=false)
		this->Point::Point(x,y);
	this->__vptr_Point3d=__vtbl_Point3d;
	this->__vptr_Point3d__Point=__vtbl_Point3d__Point;
	this->_z=rhs._z;
	return this;

}

在更深层的继承情况下,例如Vertex3d,当调用Point3d和Vertex的constructor时,总是会把__most_derived参数设为false,于是就压制了两个constructors中对Point constructor的调用操作:

//c++伪代码
//在virtual base class情况下的constructor扩充内容
Vertex3d*
Vertex3d::Vertex3d(Vertex3d *this,bool __most_derived,float x,float y,float z)
{
	if(__most_derived!=false)
		this->Point::Point(x,y);
	
	//调用上一层 base class
	//设定 __most_derived 为false

	this->Point3d::Point3d(false,x,y,z);
	this->Vertex::Vertex(false,x,y);

	//设定vptrs
	//安插user code

	return this;
}

这样的策略可以保持语意的正确无误,当我们定义

Point3d origin;

时,Point3d constructor可以正确地调用其Point virtual base class subobject。而当我们定义:

Vertex3d cv;

Vertex3d constructor正确地调用Point constructor。Point3d和Vertex的constructor会做每一件该做的事情—除了对Point的调用操作。

在这种状态下,“virtual base class constructors的被调用”有着明确的定义:只有当一个完整的class object被定义出来时,它才会被调用;如果object只是某个完整object的subobject(???),它就不会被调用。

vptr 初始化语意学

当我们定义一个PVertex object时,constructors的调用顺序是:

Point(x,y);
Point3d(x,y,z);
Vertex(x,y,z);
Vertex3d(x,y,z);
Pvertex(x,y,z);

假设这个继承体系中的每一个class都定义了一个virtual function size(),该函数负责传回class的大小。如果我们写:

PVertex pv;
Point3d p3d;

Point *pt=&pv;

那么调用操作:

pt->size();

将传回PVertex的大小。而

pt=&p3d;
pt->size();

将传回Point3d的大小。

c++中constructor的调用顺序是:由根源到末端,由内而外。当base class constructor执行时,derived 实体还没有被构造出来。在PVertex constructor执行完毕之前,PVertex并不是一个完整的对象;Point3d constructor执行之后,只有Point3d subobject构造完毕。

virtual table 是决定一个class的virtual functions名单的关键,通过vptr可以处理Virtual table。为了控制class中有所作用的函数,编译系统只要简单地控制住vptr的初始化和设定操作即可。
vptr初始化操作应该如何处理呢?在 base class constructors调用操作之后,但在其他程序或是==“member initialization list 中所列的members初始化操作”之前==。
如果每一个constructor都一直等待到其base class constructor执行完毕之后才设定其对象的vptr,那么每次它都能够调用正确地virtual function实体。
令每一个base class constructor设定其对象的vptr,使它指向相关的virtual table之后,构造中的对象就可以严格而正确地变成“构造过程中所幻化出来的每一个class”的对象。也就是说,一个PVertex对象会先形成一个Point对象,一个Point3d对象、一个Vertex对象、一个Vertex3d对象,然后才是一个PVertex对象。

constructor的执行算法通常如下:

  • 1、在derived class constructor 中,“所有virtual base classes”及“上一层base class”的constructors会被调用。
  • 2、上述完成后,对象的vptr(s)被初始化,指向相关的virtual table(s).
  • 3、如果有member initialization list 的话,将在constructor体内扩展开来。这必须在vptr被设定之后才进行,以免有一个virtual member function被调用
  • 4、最后,执行我们写的其他代码。

下面有两种vptr必须被设定的情况:

  • 1、当一个完整的对象被构造起来时,如果我们声明一个Point对象,Point construtor必须设定其Vptr.(????)
  • 2、当一个subobject constructor调用一个virtual function(不论是直接调用或间接调用)。

猜你喜欢

转载自blog.csdn.net/weixin_39116058/article/details/85064142