天天看點

深度探索c++對象模型第五章筆記上構造、解構、拷貝語意學(Semantics of Constuction,Destruction,and Copy)

構造、解構、拷貝語意學(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{........};
           
深度探索c++對象模型第五章筆記上構造、解構、拷貝語意學(Semantics of Constuction,Destruction,and Copy)

下面就是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(不論是直接調用或間接調用)。