深度探索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不能擁有實體),但這個類仍然需要一個明確的建構函式來初始化它的成員變數
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(不論是直接呼叫或間接呼叫)。