考慮下面這個abstract base class 聲明:ios
class Abstract_base { public: virtual ~Abstract_base() = 0; virtual void interface() const = 0; virtual const char* mumble() const { return _mumble; } proteceted: char *_mumble; };
雖然這個class被設計爲一個抽象的base class(其中有pure virtual function,使得程序員
Abstract_base不可能擁有實例),但它仍然須要一個顯式的構造函數以初始化其data member算法
_mumble。若是沒有這個初始化操做,其局部性對象_mumble將沒法決定初值,例如:數組
class Concrete_derived : public Abstract_base { public: Concrete_derived(); // ... }; void foo() { // Abstract_base::_mumble 未被初始化 Concrete_derived trouble; // ... }
若是Abstract_base的設計者意圖讓其每個derived class提供_mumble的初值。然而若是是安全
這樣,derived class的惟一要求就是Abstract_base必須提供一個帶有惟一參數protected數據結構
constructor:ide
Abstract_base:: Abstract_base( char *mumble_value = 0 ) : _mumble( mumble_value ) { }
通常而言,class的data member應該被初始化,而且只在constructor中或是在class的其餘函數
member functions中指定初值。其餘任何操做都將被破壞封裝性質,使class的維護和修改更加測試
困難。 優化
pure virtual function能夠被定義和調用(invoke),不過它只能被靜靜地調用(invoked
statically),不能經由虛擬機制調用。例如:
// ok:定義pure virtual function // 但只可能被靜態地調用(invoked statically) inline void Abstract_base::interface() const // 先前聲明這是一個pure virtual const function { // ... } inline void Concrete_derived::interface() const { // 靜態調用(static invocation) Abstract_base::interface(); // 咱們居然可以調用一個pure virtual function // ... }
要不要這樣作,全由class設計者決定。惟一例外是pure virtual destructor:class設計者一
定得定義它。由於每個derived class destructor會被編譯器加以擴張,以靜態調用的方式調用
其「每個virtual base class」以及「上一層base class」的destructor。所以,只要缺少任何一個
base class destructors的定義,就會致使連接失敗。
這樣設計是以C++語言的一個保證爲前提:繼承體系中每個class object的destructor都會
被調用。因此編譯器不能壓抑這一調用。而編譯器也沒有足夠的知識合成一個pure virtual
destructor的函數定義。
#include <iostream> class Abstract_base { public: virtual ~Abstract_base() = 0; virtual void interface() const = 0; virtual const char* mumble() const { return _mumble; } protected: char *_mumble; }; class Concrete_derived : public Abstract_base { public: Concrete_derived() { } ~Concrete_derived() { } void interface() const; }; inline void Abstract_base::interface() const { std::cout << "mimiasd調用了Abstract_base::interface()" << std::endl; } Abstract_base:: ~Abstract_base() { std::cout << "mimiasd調用了Abstract_base::~Abstract_base()" << std::endl; } inline void Concrete_derived::interface() const { Abstract_base::interface(); } int main() { Concrete_derived concrete; concrete.interface(); }
一個好的代替方案是,不要把virtual destructor聲明爲pure。
考慮下面這個程序片斷:
(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) }
L一、L五、L6表現出三種不一樣的對象產生方式:global內存配置、local內存配置和heap內存配
置。L7把一個class object指定給另外一個,L10設定返回值,L9則顯式地以delete運算符刪除
heap object。
一個object的生命,是該object的一個執行期屬性。local object的生命從L5的定義開始,到
L10爲止。global object的生命和整個程序的生命相同。heap object的生命從它被new運算符配
置出來開始,到它被delete運算符摧毀爲止。
下面是Point的第一次聲明,能夠寫成C程序。C++ Standard說這是一種所謂的Plain OI‘ Data
聲明形式:
typedef struct { float x, y, z; } Point;
若是以C++來編譯這段代碼。觀念上,編譯器會爲Point聲明一個trivial default constructor、
一個trivial destructor、一個trivial copy constructor,以及一個trivial copy assignment
operator。但實際上,編譯器會分析這個聲明,併爲它貼上Plain OI’ Data標籤。
當編譯器遇到這樣的定義:
(1) Point global;
觀念上Point的trivial constructor和destructor都會被產生並調用,constructor在程序起始
(startup)處被調用而destructor在程序的exit()處被調用。然而,事實上那些trivial members要
不是沒被定義,就是沒被調用,程序的行爲一如它在C中的表現同樣。
只有一個小小的例外。在C中,global被視爲一個「臨時性的定義」,由於它沒有顯示的初始
化操做。一個「臨時性的定義」能夠在程序中發生屢次。那些實例會被連接器摺疊起來,只留下單
獨一個實例,被放在程序data segment中一個「特別保留給未初始化之global objects使用」的空
間。因爲歷史緣由,這塊空間被稱爲BSS,這是Block Started by Symbol的縮寫。
C++並不支持「臨時性的定義」,這是由於class構造行爲的隱式應用之故。雖然公認這個語言
能夠判斷一個class objects或是一個Plain OI' Data,但彷佛沒有必要搞得那麼複雜。因
此,global在C++中被視爲徹底定義(它會阻止第二或更多個定義)。C和C++的一個差別就在
於,BSS data segment在C++中相對地不重要。C++的全部全局對象都被以「初始化過的數據」來
對待。
foobar()函數中的L5,有一個Point object local,一樣也是既沒有被構造也沒有被析夠。當
然,Point object local若是沒有先通過初始化,可能會成一個潛在的程序「臭蟲」——萬一第一次
使用它就須要其初值的話(像L7)。至於heap object在L6的初始化操做:
(6) Point *heap = new Point;
會被轉換爲new運算符(由library提供)的調用:
Point *heap = _new( sizeof( Point ) );
並無default constructor施於new運算符傳回的Point object身上。L7對此object有個指派
(賦值,assign)操做,若是local曾被適當地初始化過,一切就沒有問題:
(7) *heap = local;
觀念上,這樣的指定操做會觸發trivial copy assignment operator作拷貝搬運操做。然而實際
上該object是一個Plain Ol‘ Data,因此賦值操做(assignment)將只是像C那樣的純粹位搬移操
做。L9執行一個delete操做。
(9) delete heap;
會被轉換爲對delete運算符(由library提供)的調用:
_delete( heap );
觀念上,這樣的操做會觸發Point的trival destructor。但一如咱們所見,destructor要不是沒
有被產生就是沒有被調用。最後,函數以傳值(by value)的方式將local當作返回值傳回,這
在觀念上會觸發trivial copy constructor,不過實際上return操做只是一個簡單的位拷貝操做,因
爲對象是一個Plain Ol’ Data。
如下是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 ) { } // mo copy constructor,copy operator // or destructor defined ... // ... private: float _x, _y, _z; };
這個通過封裝的Point class,其大小並無改變,仍是三個連續的float。不論private或
public存取層,或是member function的聲明,都不會佔用額外的對象空間。
並無爲Point定義一個copy constructor或copy operator,由於默認的位語意(default
bitwise semantics)已經足夠了。咱們也不須要提供一個destructor,由於程序默認的內存管理
方法也足夠了。
對於global實例:
Point global; // 實施Point::Point( 0.0, 0.0, 0.0 );
如今有了default constructor做用於其上。因爲global被定義在全局範疇中,其初始化操做將
延遲到程序啓動(startup)時纔開始。
若是要將class中的全部成員都設定常量初值,那麼給予一個explicit initialization list會比較有
效率些(比起意義相同的constructor的inline expansion而言)。甚至在local scope中也是如
此。例如:
void mumble() { Point local1 = { 1.0, 1.0, 1.0 }; Point local2; // 至關於一個inline expansion // explicit initialization會稍微快一些 // local2._x = 1.0 // local2._y = 1.0 // local2._z = 1.0 }
local1的初始化操做會比local2的有效率些。這是由於當函數的activation record被放進程序
堆棧時,上述initialization list中的常量就能夠被放進去local1內存中了。
Explicit initialization list帶來三項缺點:
1)只有當class members都是public,此法才奏效;
2)只能指定常數,由於它們在編譯時期就能夠被評估求值。
3)因爲編譯器並無自動施行之,因此初始化行爲的失敗可能性會高一些。
那麼,explicit initialization list所帶來的效率優勢,通常而言不可以彌補其軟件工程上的缺
點,然而在某些特殊狀況下又不同。例如,或許你以手工打造了一些巨大的數據結構如調色
盤(color palette),或是你正要把一堆常量數據傾倒給程序,那麼explicit initialization list 的
效率會比inline constructor好得多,特別是對全局對象(global object)而言。
在編譯器層面,會有一個優化機制用來識別inline constructors,後者簡單地提供一個
member-by-member的常量指定操做。而後編譯器會抽取出那些值,而且對待它們就好像是
explicit initialization list所供應的同樣,而不會把constructor擴展成爲一系列的assignment指
令。
因而,local Point object的定義:
{ Point local; // ... }
如今被附加上default Point constructor的inline expansion:
{ // inline expansion of default constructor Point local; local._x = 0.0; local._y = 0.0; local._z = 0.0; // ... }
L6配置出一個heap Point object:
(6) Point *heap = new Point;
如今則被附加一個「對default Point constructor的有條件調用操做」:
// C++僞碼 Point *heap = _new( sizeof( Point ) ); if( heap != 0 ) heap->Point::Point();
而後才被編譯器進行inline expansion操做。至於把heap指針指向local object:
(7) *heap = local;
則保持着簡單的位拷貝操做。以傳值方式傳回local object,狀況也是同樣:
(10) return local;
L9刪除heap所指的對象:
(9) delete heap;
該操做不會致使destructor被調用,由於咱們並無顯式提供一個destructor函數實例。
觀念上,咱們的Point class有一個相關的default copy constructor、copy operator、和
destructor。然而它們都是無用的(trivial),並且編譯器實際上根本沒有產生它們。
二、爲繼承作準備
第三個Point聲明,將爲「繼承性質」以及某些操做的動態決議(dynamic resoluton)作準備。
目前咱們限制對z成員作存取操做:
class Point { public: Point( float x = 0.0, float y = 0.0, float z = 0.0 ) : _x( x ), _y( y ), _z( z ) { } // mo copy constructor,copy operator // or destructor defined ... virtual float z(); // ... private: float _x, _y, _z; };
並無定一個copy constructor、copy operator、destructor。咱們的全部members都以數值
來存儲,所以在程序層面的默認語意之下,行爲良好。可能virtual functions的導入應該老是附
帶着一個virtual destuctor的聲明。但這樣作在這個例子中對咱們並沒有好處。
virtual functions的導入促使每個Point object擁有一個virtual table pointer。這個指針給我
們提供virtual接口的彈性,其成本是:每個object須要額外的一個word空間。具體影響視狀況
而定,這可能有意義,也可能沒有意義,必須視它對多態(ploymorphism)設計所帶來的實際
效益的比例而定。只有在實際完成以後,才能評估要不要避免之。
除了每個class object多負擔一個vptr以外,virtual function的導入也引起編譯器對於咱們
的Point class產生膨脹做用:
1)咱們所定義的constructor被附加了一些代碼,以便將ptr初始化。這些代碼必須被附加在
任何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; }
2)合成一個copy constructor和一個copy assignment operator,並且其操做再也不是
trivaial(但implicit destuctor仍然是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)」的賦值操做。C++ Standard要求編譯器儘可能延遲nontrivial
members的實際合成操做,直到真正遇到其使用場合爲止。
L1的global初始化操做、L6的heap初始化操做以及L9的heap刪除操做,都仍是和稍早的
Point版本相同,然而L7的memberwise賦值操做:
*heap = local;
頗有可能觸發copy assignment operator的合成,及其調用操做的一個inline expansion(行
內擴張):以this取代heap,而以rhs取代local。
最戲劇性的衝擊發生在以傳值方式傳回local的那一行(L10)。因爲copy constructor的出
現,foobar()頗有可能被轉化爲下面這樣:
// C++僞碼:foobar()的轉化, // 用以支持copy constructor Point foobar( Point &_result ) { Point local; local,Point::Point( 0.0, 0.0 ); // heap的部分與前面相同... // copy constructor的應用 _result.Point::Point( local ); // local 對象的destructor將在這裏執行。 // 調用Point定義的destructor: // local.Point::~Point(); return; }
若是支持named return value(NRV)優化,這個函數進一步被轉化爲:
// C++僞碼:foobar()的轉化, // 以支持named return value(NRV)優化 Point foobar( Point&_result ) { _result.Point::Point( 0.0, 0.0 ); // heap的部分與前相同...... return; }
通常而言,若是你的設計之中,有許多函數都須要以傳值方式(by value)傳回一個local
class object,例如:
T operator+( const T&, const T& ) { T result; // ...真正的工做在此 return result; }
那麼提供一個copyconstructor就比較合理——甚至即便default memberwise語意已經足夠。
它的出現會觸發NRV優化。然而,NRV優化後再也不須要調用copy constructor,由於運算結果已
經被直接計算於「將被傳回的object」體內了。
當咱們定義一個object以下:
T object;
若是T有一個constructor(不管是由用戶提供或是由編譯器合成的),它會被調用。這很明
顯,比較不明顯的是,constructor的調用真正伴隨了什麼?
Constructor可能內含大量的隱藏碼,由於編譯器會擴充每個constructor,擴充程度視
class T的繼承體系而定。通常而言編譯器所作的擴充操做大約以下:
1)記錄在member initialization list中的data members初始化操做會被放進constructor的函
數本體,並以members的聲明順序爲順序。
2)若是有一個member並無出如今member initialization list之中,但它有一個default
constructor,那麼該default constructor必須被調用。
3)在那以前,若是class object有virtual table pointer(s),它(們)必須被設定初值,指向適當
的virtual table(s)。
4)在那以前,全部上一層的base class constructors必須被調用,以base class的聲明順序
爲順序(與member initialization list中的順序沒關聯):
若是base class被列於member initialization list中,那麼任何顯示指定的參數都應該傳遞過
去。
若是base class沒有被列於member initialization list中,而它有default constructor(或
default memberwise copy constructor),那麼就調用之。
若是base class是多重繼承下的第二或後繼的base class,那麼this指針必須有所調整。
5)在那以前,全部virtual base class constructors必須被調用,從左到右,從最深到最淺:
若是class被列於member initialization list中,那麼若是有任何顯式指定的參數,都應該傳
遞過去。若沒有列於list之中,而class有一個default constructor,亦應該調用之。
此外,class中的每個virtual base class subobject的偏移位置(offset)必須在執行期可
被存取。
若是class object是最底層(most-derived)的class,其constructors可能被調用;某些用以
支持這一行爲的機制必須被放進來。
下面要從「C++語言對classes所保證的語意「這個角度,探討constructor擴充的必要性。再
次以Point爲例,併爲它增長一個copy constructor、一個copy operator、一個virtual
destructor,以下所示:
class Point { public: Point( float x = 0.0, float y = 0.0 ); Point( const Point& ); // copy constructor Point& operator=( const Point& ); // copy assignment operator virtual ~Point(); // virtual destructor virtual float z() { return 0; } // ... protected: float _x, _y; };
在開始介紹並一步步走過以Point爲根源的繼承體系以前,先很快地看看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的constructor。若是咱們定義constructor以下:
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(本例爲
virtual),因此Line class的implicit copy constructor、copy operator和destructor都將具備具體
效用(nontrivial)。
當程序員寫下:
Line a;
時,implicit Line destructor會被合成出來(若是Line派生自Point,那麼合成出來的
destructor將會是virtual。然而因爲Line只是內含Point objects而非繼承自Point,因此被合成出
來的destructor只是nontrivial而已)。其中,它的member class objects的destructor會被調用
(以其相反順序):
// C++僞碼:合成出來的Line destrutor inline void Line::~Line( Line *this ) { this->_end.Point::~Point(); this->_begin.Point::~Point(); }
固然,若是Point destructor是inline函數,則每個調用操做會在調用地點被擴展開來。雖然
Point destructor是virtual,但其調用操做(在containing class destructor之中)會被靜態地決議
出來。
相似的道理,當寫下:
Line b = a;
時,implicit Line copy constructor會被合成出來,成爲一個inline public member。
最後,當寫下:
a = b;
時,implicit copy assignment operator會被合成出來,成爲一個inline public member。
關於在產生copy operator的時候,要加入以下的條件過濾:
if( this == &rhs ) return *this;
防止自我指派(賦值),例如自我賦值以下的失敗:
// 使用者提供的copy assignment operator // 忘記提供一個自我拷貝時的過濾 String& String::operator= ( const String &rhs ) { // 這裏須要過濾(在釋放資源以前) delete [] str; str = new char[ strlen( rhs.str ) + 1 ]; }
考慮下面這個虛擬繼承:
class Point3d : public virtual Point { public: Point3d( float x = 0.0, float y = 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; };
傳統的「constructor擴充現象」並無用,這是由於virtual base class的「共享性」之故:
// C++僞碼 // 不合法的constructor擴充內容 Point3d* Point3d::Point3d( Point3d *this, float x, float y, float z ) { this->_vptr_Point3d = _vtbl_Point3d; this->_vptr_Point3d_Point = _vtbl_Point3d_Point; this->_z = rhs._z; return this; }
試着想如下三種類的派生狀況:
class Vertex : virtual public Point{ ... }; class Vertex3d : public Point3d, public Vertex { ... }; class PVertex : public Vertex3d{ ... };
Vertex的constructor必須也調用Point的constructor。然而,當Point3d和Vertex同爲Vertex3d
的subobjects時,它們對Point constructor的調用操做必定不能夠發生;取而代之的是,做爲一個
底層的class, Vertex3d有責任將Point初始化。而更日後的繼承,則由PVertex來負責完成「被共
享之Point subobject」的構造。
傳統策略若是要支持「初始化virtual base class」,會致使constructor中有更多的擴充內容,用
以指示virtual base classconstructors應不該該被調用。constructor的函數本體於是必須條件式地
測出傳進來的參數,而後決定調用或不調用相關的virtual base class constructors。下面就是
Point3d的constructor擴充內容:
// C++代碼 // 在virtual base class狀況下的consrtuctor擴充內容 Point3d* Point3d::Point3d( Point3d *this, bool _most_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,因而就壓制了兩個constructor中對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 classes // 設定_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被定義出來(例如origin)時,它纔會被調用;若是object的subobject,它就不會被調
用。
以此爲槓桿,咱們能夠產生更有效率的constructors。某些新進的編譯器把每個constructor
分裂爲二,一個針對完整的object,另外一個針對subobject。「完整object」版無條件地調用virtual
base constructors,設定全部ptrs等。「subobject」版則不調用virtual base constructors,也可能
不設vptrs等。
當咱們定義一個PVertex object時,constructor的調用順序是:
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的大小。
更進一步,咱們假設這個繼承體系中的每個constructor內含一個調用操做,像這樣:
Point3d::Point3d( float x, float y, float z ) : _x( x ), _y( y ), _z( z ) { if( spyOn ) cerr << "Within Point3d::Point3d()" << "size: " << size() << endl; }
當咱們定義PVertx object時,前述的5個constructors會如何會如何?每一次size()調用會被決
議爲PVertex::size()嗎?或者每次調用會被決議爲「目前正在執行之constructor所對應之class」的
size()函數實例?
C++語言規則告訴咱們,在Point3d constructor中調用size()函數,必須被決議爲
Point3d::size()而不是PVertex::size)。更通常性地說,在一個class(本例爲Point3d)的
constructor(和destructor)中,經由構造中的對象(本例爲PVertex對象)來調用一個virtual
function,其函數實例應該是在此class(本例爲Point3d)中有做用的那個。因爲各個
constructor的調用順序,上述狀況是必需的。Constructors的調用順序是:由根源而末端
(bottom up)、由內而外(inside out)。當base constructor執行時,derived實例尚未被構
造起來。
意思是,當每個PVertex base class constructors被調用時,編譯系統必須保證有適當的
size()函數實例被調用。怎樣才能辦到這一點?
若是調用操做限制必須在constructor(或destructor)中直接調用,那麼答案十分明顯:將每
一個調用操做以靜態方式決議,千萬不要用到虛擬機制。若是是在Point3d constructor中,就顯
式調用Point3d::size()。
然而若是size()之中又調用一個virtual function,會發生什麼事情?這種狀況下,這個調用也
必須決議爲Point3d的函數實例。而在其餘狀況下,這個調用是純正的virtual,必須經由虛擬機
制來決定歸向。也就是說,虛擬機制自己必須知道是否這個調用源自於一個constructor中。
另外一個咱們能夠採起的方法是,在constructor(或destructor)內設立一個標誌,用靜態方
式來決議。而後咱們就能夠用標誌值做爲判斷依據,產生條件式的調用操做。
這的確可行,雖然感受起來有點不夠優雅和有效率。
這個解法方法感受起來比較像是咱們的第一個設計策略失敗後的一個策略,而不是釜底抽
薪的辦法。根本的解決之道是,在執行一個constructor時,必須限制一組virtual function候選名
單。
virtual table是決定一個class的virtual function名單的關鍵。而Virtual table經過vptr。因此
爲了控制一個class中所做用的函數,編譯系統只要簡單地控制住vptr的初始化和設定操做即
可。固然,設定vptr是編譯器的責任,任何程序員都沒必要操心此事。
vptr初始化操做的處理,本質而言,這得視vptr在constructor之中「應該在什麼時候被初始化」而
定。咱們有三種選擇:
1)在任何操做以前。
2)在base class constructors調用操做以後,可是程序員供應的代碼或是」member
initialization list中所列的members初始化操做「以前。
3)在每一件事情發生以後。
答案是2。另兩個選擇沒有什麼價值。策略2解決了」在class中限制一組virtual functions名單
「的問題。若是每個constructor都一直等待到其base class constructors執行完畢以後才設定其
對象的vptr,那麼每次它都可以調用正確的virtual function實例。
令每個base class constructor設定其對象的vptr,使它指向相關的virtual table以後,構
造中的對象就能夠嚴格而正確地變成」構造過程當中所幻化出來的每個class「的對象。也就是
說,一個PVertex對象會先造成一個Point對象、一個Point3d對象、一個Vertex對象、一個
Vertex對象。在每個base class constructor中,對象能夠與constructor‘s class的完整對象作
比較。對於對象而言,」個體發生學「歸納了」系統發生學「。constructor的執行算法一般以下:
1)在derived class coinstructor中,」全部virtual base classes「及」上一層base class「的
constructor會被調用。
2)上述完成以後,對象的vptr(s)被初始化,指向相關的virtual table(s)。
3)若是有member initialization list的話,將在constructor體內擴展開來。這必須在vptr被設
定以後才作,以避免有一個virtual member function被調用。
4)最後,執行程序員所提供的代碼。
例如,已知下面這個由程序員定義的PVertex constructor:
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; }
它頗有可能被擴展爲:
// C++僞碼: // PVertex constructor 的擴展結果 PVertex* PVertex::PVertex( PVertex* this, bool _most_derived, float x, float y, float z ) { // 條件式地調用virtual base constructor if( _most_derived != false ) this->Point::Point( x, y ); // 無條件地調用上一層base this->Vertex3d::Vertex3d( x, y, z ); // 將相關的vptr初始化 this->_vptr_PVertex = _vtbl_PVertex; this->_vptr_Point_PVertex = _vtbl_Point_PVertex; // 程序員所寫的代碼 if( spyOn ) cerr << "Within PVertex::PVertex()" << "size: " // 經由虛擬機制調用 << ( *this->_vptr_PVertex[ 3 ].faddr )( this ) << endl; // 傳回構造的對象 return this; }
這真是個完美的解答嗎?假設咱們的Point constructor定義爲:
Point::Point( float x, float y ) : _x( x ), _y( y ) { }
咱們的Point3d constructor定義爲:
Point3d::Point3d( float x, float y, float z ) : Point( x, y ), _z( z ) { }
更進一步假設咱們的Vertex和Vertex3d constructors有相似的定義。
下面是vptr必須被設定的兩種狀況:
1)當一個完整的對象被構造起來時。若是咱們聲明一個Point對象,則Point constructor必
須設定其vptr。
2)當一個subobject constructor調用一個virtual function(不管是其直接調用或間接調用)
時。
若是咱們聲明一個PVertex對象,而後因爲咱們對其base class constructors的最新定義,其
vptr將再也不須要在每個base class constructor中被設定。解決之道是把constructor分裂爲一個
完整的object實例和一個subobject實例。在subobject實例中,vptr的設定能夠省略(若是可能
的話)。
知道了這些以後,你應該可以回答下面的問題了:在class的constructor的member
initialization list中調用該class的一個虛擬函數,安全嗎?就實際而言,將此函數施於其class’s
data member的初始化行動中,老是安全的。這是由於,正如咱們所見,vptr保證可以在
member initialization list被擴展以前,由編譯器正確地設定好。可是在語意上這多是不安全
的,由於函數自己可能還得依賴未被設立初值的members。因此並不推薦這種作法。然而,
從vptr的總體角度來看,這是安全的。
什麼時候須要供應參數給一個base class constructor?這種狀況下在」class的constructor的
member initialization list中「調用該class的虛擬函數,是不安全的。此時,vptr若不是還沒有被設
定好,就是被設定指向錯誤的class。更進一步地說,該函數所存取的任何class‘s data
members必定尚未被初始化。
當咱們設計一個class,並以一個class object指定另外一個class object時,咱們有三
種選擇:
1)什麼都不作,所以得以實施默認行爲。
2)提供一個explicit copy assignment operator。
3)顯式地拒絕把一個class object指定給另外一個class object。
若是選擇第3點,不許將一個class object指定給另外一個class object,那麼只要將copy
assignment operator聲明爲private,而且不提供其定義便可。把它設爲private,咱們就再也不允
許於任何地點(除了在member functions以及該class的friends之中)作賦值(assign)操做。
不提供其函數定義,則一旦某個member function或friend企圖影響一份拷貝,程序在連接時就
會失敗。通常認爲這和連接器的性質有關(也就是說並不屬於語言自己的性質),因此不是很
使人滿意。
這裏要驗證copy assignment operator的語意,以及它們如何被模塑出來。再次利用Point
class來幫助討論:
class Point { public: Point( float x = 0.0, float y = 0.0 ); // ... ( 沒有virtual function ) protected: float _x, _y; };
沒有什麼理由須要禁止拷貝一個Point object。所以問題就變成了:默認行爲是否足夠?如
果咱們要支持的只是一個簡單的拷貝操做,那麼默認行爲不但足夠並且有效率,咱們沒有理由
再本身提供一個copy assignment operator。
只有在默認行爲所致使的語意不安全或不正確時,咱們才須要設計一個copy assignment
operator(memberwise copy及其潛在陷阱)。默認的memberwise copy行爲對於咱們的Point
object不安全嗎?不正確嗎?不,因爲座標都內含數值,因此不會發生」別名化(aliasing)「或」
內存泄漏(memory leak)「。若是咱們本身提供一個copy assignment operator,程序反倒會執
行得比較慢。
若是咱們不對Point供應一個copy assignment operator,而關是依賴默認的memberwise
copy,編譯器會產生出一個實例嗎?這個答案和copy constructor的狀況同樣:實際上不會!由
於此class已經有了bitwise copy語意,因此implicit copy assignment operator被視爲毫無用處,
也根本不會被合成出來。
一個class對於默認的copy assignment operator,在如下狀況,不會變現出bitwise copy語
意:
1)當class內含一個member object,而其class有一個copy assignment operator時。
2)當一個class的base class有一個copy assignment operator時。
3)當一個class聲明瞭任何virtual functions(咱們必定不要拷貝右端class object的vptr地
址,由於它多是一個derived class object)時。
4)當class繼承自一個virtual base class(不論此base class 有沒有copy operator)時。
C++ Standard上說copy assignment operators並不表示bitwise copy semantics是
nontrivial。實際上,只有nontrivial instances纔會被合成出來。
因而,對於咱們的Point class,這樣的賦值(assign)操做:
Point a, b; ... a = b;
由bitwise copy完成,把Point b拷貝到Point a,其間並無copy assignment operator被調
用。從語意上或從效率上考慮,這都是咱們所須要的。注意,咱們仍是可能提供一個copy
constructor,爲的是把name return value(NRV)優化打開。copy constructor的出現不該該讓
咱們覺得也必定要提供一個copy assignment operator。
如今要導入一個copy assignment operator,用以說明該operator在繼承之下的行爲:
inline Point& Point::operator=( const Point &p ) { _x = p._x; _y = p._y; return *this; }
如今派生一個Point3d class(虛擬繼承):
class Point3d : virtual public Point { public: Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ); // ... protected: float _z; };
若是咱們沒有爲Point3d定義一個copy assignment operator,編譯器就必須合成一個(由於
前述的第二項和第四項理由)。合成而得的東西可能看起來像這樣:
// C++僞碼:被合成copy assignment operator inline Point3d& Point3d::operator=( Point3d* const this, const Point3d &p ) { // 調用base class的函數實例 this->Point::operator=( p ); // memberwise copy the derived class members _z = p._z; return *this; }
copy assignmentoperator有一個非正交性狀況(nonorthogonal aspect,意指不夠理想、不
夠嚴謹的狀況),就是它缺少一個member assignment list(也就是平行於member initialization
list的東西)。所以咱們不可以寫:
// C++僞碼,如下性質並不支持 inline Point3d& Point3d::operator=( const Point3d &p3d ) : Point( p3d ), z( p3d._z ) { }
咱們必須寫成如下兩種形式,才能調用base class的copy assignment operator:
Point::operator=( p3d );
或
( *( Point* )this ) - p3d;
缺乏copy assignment list,看起來或許只是一件小事,但若是沒有它,編譯器通常而言就
沒有辦法壓抑上一層base class的copy operator被調用。例如,下面是個Vertex copy
operator,其中Vertex也是虛擬繼承自Point:
// class Vertex : Virtual public Point inline Vertex& Vertex::operator=( const Vertex &v ) { this->Point::operator=( v ); _next = v._next; return *this; }
如今讓咱們從Point3d和Vertex中派生出Vertex3d。下面是Vertex3d的copy assignment
operator:
inline Vertex3d& Vertex3d::operator=( const Vertex3d &v ) { this->Point::operator=( v ); this->Point3d::operator=( v ); this->Vertex::operator=( v ); ... }
編譯器如何可以在Point3d和Vertex的copy assignment operators中壓抑Point 的copy
assignment operators呢?編譯器不可以重複傳統的constructor解決方案(附加額外的參數)。
這是由於,和constructor以及destructor不一樣的是,」取copy assignment operator地址「的操做是
合法的。所以下面這個例子是毫無瑕疵的合法程序代碼(雖然它也毫無瑕疵地推到了咱們但願
把copy assignment operator作得更靈巧的企圖):
typedef Point3d& ( Point3d::*pmfPoint3d )( const Point3d& ); pmfPoint3d pmf = &Point3d::operator=; ( x.*pmf )( x );
然而咱們沒法支持它,咱們仍然須要根據其獨特的繼承體系,安插任何可能個數的參數給
copy assignment operator。這一點在咱們支持由class objects(內含virtual base classes)所
組成的數組的配置操做時,也被證實是很是有問題的。
另外一個方法是,編譯器可能爲copy assignment operator產生分化函數(splict functions),
以支持這個class成爲most-derived class或成爲中間的base class。若是copy assinment
operator被編譯器產生的話,那麼」split function解決方案「可說是定義明確。但若是它是被class
設計者所完成的,那就不能算是定義明確。例如,一我的如何分化像下面這樣的函數呢(特別
當init_bases()是virtual時):
inline Vertex3d& Vertex3d::operator=( const Vertex3d &v ) { init_bases( v ); ... }
事實上,copy assignment operator在虛擬繼承狀況下行爲不佳,須要當心地設計和說明。
許多編譯器甚至並不嘗試取得正確的語意,它們在每個中間(調停用)的copy assignment
operator中調用每個base class instance,因而形成virtual base class copy assignment
operator的多個實例被調用。而C++ Standard的說法是:
咱們並無規定那些表明virtual base class的subobjects是否應該被」隱式定義(implicitly
defined)的copy assignment operator「指派(賦值,assign)內容一次以上。
若是使用一個語言爲基礎的解決辦法,那麼應該爲copy assignment operator提供一個附加
的」member copy list「。簡單地說,任何解決方案若是是以程序操做爲基礎,就將致使較高的復
雜度和較大的錯誤傾向。通常公認,這是語言的一個弱點,也是一我的應該老是當心檢驗其程
序代碼的地方(當他使用virtual base classes時)。
有一種方法能夠保證most-derived class 會引起(完成)virtual base class subobject的
copy行爲,那就是在derived class的copy assignment operator函數實例的最後,顯式調用那個
operator,像這樣:
inline Vertex3d& Vertex3d:operator=( const Vertex3d &v ) { this->Point3d::operator=( v ); this->Vertex::operator=( v ); // must place this last if your compiler does // not suppress intermediate class invocations this->Point::operator=( v ); ... }
這並不可以省略subobjects的多重拷貝,但卻能夠保證語意正確。另外一個解決方案要求把
virtual subobject拷貝到一個分離的函數中,並根據call path,條件化地調用它。
建議儘量不要容許一個virtual base class 的拷貝操做。甚至:不要在任何virtual base
class中聲明數據。
在如下的效率測試中,對象構造和拷貝所需的成本是以Point3d class聲明爲基準的,從簡單
形式逐漸到複雜形式,包括Plain Ol' Data、抽象數據類型(Abstract Data Type,ADT)、單一
繼承、多重繼承、虛擬繼承。如下函數是測試的主角:
Point3d lots_of_copies( Point3d a, Point3d b ) { Point3d pC = a; pC = b; // (1) b = a; // (2) return pC; }
它內含4個memberwise初始化操做,包括兩個參數、一個傳回值以及一個局部對象pC。它
也內含兩個memberwise拷貝操做,分別是標示爲(1)和(2)那兩行的pC和b。main()函數如
下:
main() { Point3d pA( 1.725, 0.875, 0.478 ); Point3d pB( 0.315, 0.317, 0.838 ); Point3d pC; for( int iters = 0; iters < 10000000; iters++ ) pC = lots_of_copies( pA, pB ); return 0; }
第一個程序中數據類型是一個struct:
struct Point3d { float x, y, z; };
第二個程序是擁有public數據的class:
class Point3d { public: float x, y, z; };
第三個測試,惟一改變的是數據的封裝以及inline函數的使用,以及一個inline constructor,
用以初始化每個object。class仍然展示出bitwise copy語意,因此常識告訴咱們,執行期的效
率應該相同。
class Point3d { public: inline Point3d( float _x, float _y, float _z ) : x( _x ), y( _y ), z( _z ) { } inline Point3d lots_of_copies( Point3d b ) { Point3d pC = *this; pC = b; // (1) b = *this; // (2) return pC; } private: float x, y, z; };
如今修改main()函數的初始化過程:
Point3d pA; pA.x = 1.725; pA.y = 0.875; pA.z = 0.478; Point3d pB; pB.x = 0.315; pB.y = 0.317; pB.z = 0.838;
封裝和未封裝過的兩種Point3d聲明之間,另外一個差別是關於下一行的語音:
Point3d pC;
若是使用ADT表示法,pC會以其default constructor的inline expansion自動進行初始化——
甚至雖然在此例而言,沒有初始化也很安全。從某一個角度來講,雖然這些差別實在小,但它
們扮演警告角色,警告說「封裝加上inline支持,徹底至關於C程序中的直接數據存取」。從另外一
個角度來講,這些差別並不具備什麼意義,所以也就沒有理由放棄「封裝」特性在軟件工程上的利
益。它們是一些你得記在心中以備特殊狀況下可以派上用場的東西。
下一個測試,把Point3d的表現法切割爲三個層次的單一繼承:
class Point1d{}; // x class Point2d : public Point1d{}; // y class Point3d : public Point2d{}; // z
下面的多重繼承,通常認爲是比較高明的設計。因爲其member的分佈,它完成了任務:
class Point1d{}; // x class Point2d{}; // y class Point3d : public Point1d, public Point2d{}; // z
因爲Point3d class仍然顯現出bitwise copy語意,因此額外的多重繼承關係不該該在
memberwise的對象初始化操做或拷貝操做上增長成本。
下面是單層的虛擬繼承:
class Point1d{}; // x class Point2d : public virtual Point1d{}; // y class Point3d : public Point2d{}; // z
再也不容許class擁有bitwise copy語意(第一層虛擬繼承不容許之,第二層繼承則更加復
雜)。合成型的inline copy constructor和copy assignment operator因而被產生出來,並派上用
場,這致使效率成本上的一個重大增長。
若是class沒有定義destructor,那麼只有在class內含的member object(抑或class本身的
base class)擁有destructor的狀況下,編譯器纔會自動合成出一個來。不然,destructor被視爲
不須要,也就不須要被合成(固然更不須要被調用)。例如,咱們的Point,默認狀況下並無
被編譯器合成出來一個destructor——甚至雖然它擁有一個virtual function:
class Point { public: Point( float x = 0.0, float y = 0.0 ); Point( const Point& ); virtual float z(); // ... private: float _x, _y; };
相似的道理,若是咱們把兩個Point對象組合成一個Line class:
class Line { public: Line( const Point&, const Point& ); // ... virtual draw(); // ... protected: Point _begin, _end; };
LIne也不會擁有一個合成出來的destructor,由於Point並無destructor。
當咱們從Point派生出Point3d(即便是一種虛擬派生關係)時,若是咱們沒有聲明一個
destructor,編譯器就沒有必要合成一個destructor。
不論Point仍是Point3d,都不須要destructor,爲它們提供一個destructor反而是低效率的。
應該拒絕那種被稱爲「對稱策略」的奇怪想法:「你已經定義了一個constructor,因此你覺得提供
一個destructor也是天經地義的事」。事實上,應該由於「須要」而非「感受」來提供destructor,更不
要由於不肯定是否須要一個destructor,因而就提供它。
爲了決定class是否須要一個程序層面的destructor(或是constructor),請想一想一個class
object的生命在哪裏結束(或開始)?須要什麼操做才能保證對象的完整?這是寫程序時比較需
要了解的(或是你的class使用者比較須要瞭解的)。這也是constructor和destructor何時其
做用的關鍵。例如:
{ Point pt; Point *p = new Point3d; foo( &pt, p ); ... delete p; }
咱們看到,pt和p在做爲foo()函數參數以前,都必須初始化爲某些座標值。這時候須要一個
constructor,不然使用者必須顯示提供座標值。通常而言,class的使用者沒有辦法檢驗一個
local變量或heap變量以知道它們是否被初始化。把constructor想象爲程序的一個額外負擔是錯
誤的,由於它們的工做有其必要性。若是沒有它們,抽象化(abstraction)的使用就會有錯誤
的傾向。
當咱們顯示地delete掉p,會如何?有任何程序上必須處理的嗎?是否須要在delete以前這
麼作:
p->x( 0 ); p->y( 0 );
固然不須要。沒有任何理由說在delete一個對象以前先得將其內容清除乾淨。也不須要歸還
任何資源。在結束pt和p的生命以前。沒有任何「class使用者層面」的程序操做是絕對必要的,因
此,也就不必定須要一個destructor。
然而請考慮咱們的Vertex class,它維護了一個由緊鄰的「頂點」所造成的鏈表,而且當一個
頂點的生命結束時,在鏈表上來回移動以完成刪除操做。若是這(或其餘語意)正是程序員所
須要的,那麼這就是Vertex destructor的工做。
當咱們從Point3d和Vertex派生出Vertex3d時,若是咱們不供應一個explicit Vertex3d
destructor,那麼咱們仍是但願Vertex destructor被調用,以結束一個Vertex3d object。所以編譯
器必須合成一個Vertex3d destructor,其惟一任務就是調用Vertex destructor。若是咱們提供一
個Vertex3d destructor,編譯器會擴展它,使它調用Vertex destructor(在咱們所供應的程序代
碼以後)。一個由程序員定義的destructor被擴展的方式相似constructor被擴展的方式,但順序
相反:
1)destructor的函數本體如今被執行,也就是說vptr會在程序員的代碼執行前被重設
(reset)。
2)若是class擁有member class objects,然後者擁有destructors。那麼它們會以其聲明數
怒的相反順序被調用。
3)若是object內含一個vptr,那麼首先重設(reset)相關的virtual table。
4)若是有任何直接的(上一層)nonvirtual base classes擁有destructor,它們會議其聲明
順序的相反順序被調用。
5)若是有任何virtual base classes擁有destructor,而目前討論的這個class是最尾端
(most-derived)的class,那麼它們會以其原來的構造順序的相反順序被調用。
就像constructor同樣,目前對於destructor的一種最佳實現策略就是維護兩份destructor實
例:
1)一個complete object實例,老是設定好vptr(s),並調用virtual base class destructor。
2)一個base class subobject實例;除非在destructor函數中調用一個vritual function,不然
它毫不會調用virtual base class destructors並設定vptr。
一個object的生命結束於其destructor開始執行之時。因爲每個base class destructor都輪
番被調用,因此derived object實際上變成了一個完整的object。例如一個PVertex對象歸還其內
存空間以前,會依次變成一個Vertex3d對象、一個Vertex對象,一個Point3d對象,最後稱爲一
個Point對象。當咱們在destructor中調用member functions時,對象的蛻變會由於vptr的從新設
定(在每個destructor中,在程序員所供應的代碼執行以前)而受到影響。