1. 程式人生 > >深度探索c++物件模型第五章筆記上

深度探索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(不論是直接呼叫或間接呼叫)。