深刻探索C++對象模型(五)

構造、解構、拷貝語意學(Semantics of Construction,Destruction, and Copy)

通常而言,class的data member應該被初始化,而且只在constructor中或是在class的其餘member functions中指定初值。其餘任何操做都將破壞封裝性質,使class的維護和修改更加困難。程序員

純虛函數的存在(Presence of a Pure Virtual Function)

C++ 新手經常很驚訝地發現,一我的居然能夠定義和調用(invoke)一個pure virtual function:不過它只能被靜態地調用(invoked statically),不能經由虛擬機制調用。例如,你能夠合法地寫下這段代碼:算法

//定義pure virtual function但只能被靜態地調用(invoked statically)
inline void Abstract_base::interface() const{
    //請注意,先前曾聲明這是一個pure virtual const
function
    //...
}

inline void Concrete_derived::interface() const{
    //靜態調用(static invocation) 
    Abastract_base::interface();
          //請注意,咱們居然可以調用一個pure virtual
function
    //...
}

要不要這樣作,全由class設計者決定。惟一的例外就是pure virtual destructor: class設計者必定要定義它。爲何? 由於每個derived class destructor會被編譯器加以擴展,以靜態調用的方式調用其「每個virtual base class」以及「上一層base class」的destructor。所以,只要缺少任何一個base class destructor的定義,就會致使連接失敗。ide

「無繼承」狀況下的對象構造

考慮下面的程序片斷:函數

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

L1,L5,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 old Data聲明形式:this

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

若是以C++ 來編譯這段碼,會發生什麼事? 觀念上,編譯器會爲Point聲明一個trivial default constructor、一個trivial destructor、一個trivial copy constructor,以及一個trivial copy assignment operator。但實際上,編譯器會分析這個聲明,併爲它貼上Plain of Data標籤lua

當編譯器遇到這樣的定義:設計

(1) Point global;

時,觀念上Point的trival constructor和destructor都會被產生並被調用,constructor在程序起始(startup)處被調用而destructor在程序的exit()處被調用。然而,事實上那些tirvial members要不是沒被定義,就是沒被調用,程序的行爲一如它在C中的表現同樣。3d

只有一個小小的例外,在C中,global被視爲一個「臨時性的定義」,由於它沒有明確的初始化操做。一個「臨時性的定義」能夠在程序中發生屢次,那些實例會被連接器摺疊起來,只留下單獨一個實體,被放在程序data segment中的一個「特別保留給未初始化之global objects使用」的空間,因爲歷史的緣故,這段空間被稱爲BSS,這是Block Started by Symbol的縮寫。

C++ 並不支持「臨時性的定義」,這是由於class構造行爲的隱含應用之故。所以,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運算符的調用:

Point *heap = _new(sizeof(Point));

再一次強調,並無default constructor施行與new運算符所傳回的Point object身上。L7對此object有一個賦值(賦值,assign)操做,若是local曾被適當地初始化過,一切就沒有問題:

(7)  *heap = local;

事實上這一行會產生編譯警告以下:

warning,line 7, local is used before being initialized

觀念上,這樣的指定操做會觸發trivial copy assignment operator進行拷貝搬運操做。然而實際上此object是一個Plain old data,因此賦值操做(assignment)將只是像C那樣的純粹位搬移操做。L9執行一個delete操做:

(9) delete heap;

會被轉換爲對delete運算符(由library提供)的調用:

_delete(heap);

觀念上,這樣的操做會觸發Point的trivial destructor。可是一如咱們所見,destructor要不是沒有被產生就是沒有被調用。最後,函數以傳值(by value)的方式將local看成返回值傳回,這在觀念上會觸發trivial copy constructor,不過實際上return操做只是一個簡單的位拷貝操做,由於對象是一個Plain old data。

抽象數據類型(Abstract Data Type)

如下是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(y) { }
        
    //no copy constructor, copy operator or destructor defined
private:
    float _x, _y, _z;
};

這個通過封裝的Point class,其大小並無改變,仍是三個連續的float。是的,不論private、public存取層,或是member function的聲明,都不會佔用額外的對象空間。

對於一個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};
    
    Plint 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. 只能指定常量,由於它們在編譯時期就能夠被評估求值(evaluated)
  3. 因爲編譯器並無自動施行之,因此初始化行爲的失敗可能性會高一些

在編譯器層面,會有一個優化機制用來識別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的有條件調用操做」:

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 resolution)作準備,當前咱們限制對z成員進行存取操做:

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

再次強調,沒有定義一個copy constructor、copy operator、destructor。咱們全部的memebers都以數值來存儲,由於在程序層面的默認語意之下,行爲良好。

virtual function的引入促使每個Point object擁有一個virtual table pointer。這個指針提供給咱們virtual接口的彈性,代價是:每個object須要額外的一個word空間。

除了每個class object多負擔一個vptr以外,virtual function的引入也引起編譯器對於咱們的Point class產生膨脹做用:

  • 咱們所定義的constructor被附加了一些碼,以便使vptr初始化。這些碼必須附加在任何base class constructors的調用以後,但必須在任何由使用者(程序員)供應的碼以前。如:
    ```cpp
    Point* Point::Point(Point *this, float x, float y)
    : _x(x), _y(y){

    //設定object的virtual table pointer
      this->_vptr_Point = _vtbl_Point;
    
      //擴展member initialization list
      this->_x = x;
      this->_y = y;
    
      //傳回this對象
      return this;

    }
    ```

  • 合成一個copy constructor和一個copy assignment operator,並且其操做再也不是trivial(但implicit destructor仍然是trivial)。若是一個Point object被初始化或以一個derived class object賦值,那麼以位爲基礎(bitwise)的操做可能會給vptr帶來非法設定。
    ```cpp
    //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的實際合成操做,直到真正遇到其使用場合爲止。

通常而言,若是你的設計之中有許多函數都須要以傳值方式(by value)傳回一個local class object,例如像以下形式的一個算術運算:

T opeartor+(const T&, const T&){
    T result;
    //真正的工做在此...
    return result;
}

此時提供一個copy constructor就比較合理——甚至即便default memberwise語意已經足夠,它的出現會觸發NRV優化。NRV優化後就再也不須要調用copy constructor,由於運算結果已經被直接置於「將被傳回的object」體內了。

繼承體系下的對象構造

當咱們定義一個object以下:

T object;

時,實際上會發生什麼事情呢? 若是T有一個constructor(不管是由user提供或是由編譯器合成),它會被調用。這很明顯,比較不明顯的是,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 functions, 它們必須被設定初值,指向適當的virtual tables.
  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所保證的語意」這個角度來探討constructors擴充的必要性。我再次以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.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。若是咱們定義constructor以下:

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

它會被編譯器擴充並轉換爲:

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都將有實際功能。(nontrival)

當程序員寫下:

Line a;

時,implicit Line destructor會被合成出來(若是Line派生自Point,那麼合成出來的destructor將會是virtual。然而因爲Line只是內帶Point objects而非繼承自Point,因此被合成出來的destructor只是nontrivial而已)。在其中,它的member class objects的destructor會被調用(以其構造的相反順序):

inline Line::~Line(Line *this){
    this->_end.Point::~Point();
    this->_begin.Point::~Point();
}

固然,若是Point destructor是inline函數,那麼每個調用操做會在調用地點被擴展出來。請注意,雖然Point destructor是virtual,但其調用操做(在containing class destructor之中)會被靜態地決議出來(resolved statically)。

虛擬繼承(Virtual Inheritance)

考慮下面這個虛擬繼承:

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;
};

傳統的「constructor擴充現象」並無用,這是由於virtual base class的「共享性」之故:

//不合法的constructor擴充內容
Point3d* Point3d::Point3d(Point3d *this, float x, float y, float z)
{
    this->Point::Point(x, y);
    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同爲Vertetx3d的subobjects時,它們對Point constructor的調用操做必定不能夠發生,取而代之的是,做爲一個最底層的class,Vertex3d有責任將Point初始化,而更日後(往下)繼承,則由PVertex(再也不是Vertex3d)來負責完成「被共享之Point subobject」的構造。

constructor的函數自己於是必須條件式地測試傳進來的參數,而後決定調用或不調用相關的virtual base class constructors,下面就是Point3d的constructor擴充內容:

//在virtual base class狀況下的constructor擴充內容
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 = _vpbl_Point3d_Point;
    this->_z = rhs._z;
    return this;
}

在更深層次的繼承狀況下,例如Vertex3d,當調用Point3d和Vertex的constructor時,老是會把_most_derived參數設爲flase。因而就壓制了兩個constructors中對Point constructor的調用操做:

//在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的constructors會作每一件該作的事情——對Point的調用操做除外。

「virtual base class constructors的被調用」有着明確的定義:只有當一個完整的class object被定義出來時,它纔會被調用;若是object只是某個完整object的subject,它就不會被調用

vptr初始化語意學(The Semantics of the vptr Initialization)

當咱們定義一個PVertex object時,constructors的調用順序是:

Point(x, y);
Point(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++ 語言規則告訴咱們,在Point3d constructor中調用的size()函數,必須被決議爲Point3d::size()而不是PVertex::size()。更通常地,在一個class(本例爲Point3d)的constructor(和destructor)中,經由構造中的對象(本例爲PVertex)來調用一個virtual function,其函數實例應該是在此class(本例爲Point3d)中有做用的那個。因爲各個constructors的調用順序,上述狀況是必要的。

Constructors的調用順序是:由根源而末端(bottom up)、由內而外(inside out)。當base class constructor執行時,derived實例尚未被構造起來。在PVertex constructor執行完畢以前,PVertex並非一個完整的對象:Point3d constructor執行以後,只有Point3d subobject構造完畢。

若是調用操做限制必須在constructor(或destructor)中直接調用,那麼答案十分明顯:將每個調用操做以靜態方式決議之,千萬不要用到虛擬機制。

vptr 初始化操做應該如何處理? vptr初始化操做在base class constructors調用操做以後,可是在程序員供應的代碼或是「memeber initialization list中所列的members初始化操做」以前。

令每個base class constructor設定其對象的vptr,使它指向相關的virtual table以後,構造中的對象就能夠嚴格而正確地變成「構造過程所幻化出來的每個class」的對象。也就是說,一個PVertex對象會先造成一個Point對象、一個Point3d對象、一個Vertex對象、一個Vertex3d對象,而後才成爲一個PVeretex對象。在每個base class constructors中,對象能夠與constructors's class 的完整對象做比較。對於對象而言,「個體發生學」概況了「系統發生學」。constructor的執行算法一般以下:

  1. 在derived class constructor中,「全部virtual base classes」及「上一層base class」的constructors會被調用
  2. 上述完成以後,對象的vptrs被初始化,指向相關的virtual tables
  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;
    }
}

它可能被擴展爲:

//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()"
                Point3d::Point3d(),
             << "size: " 
             << (*this->_vptr_PVertex[3].faddr)(this) 
             << endl;
    }
    
    //傳回被構造的對象
    return this;
}

下面是vptr必須被設定的兩種狀況:

  1. 當一個完整的對象被構造起來時,若是咱們聲明一個Point對象,Point constructor必須設定其vptr。
  2. 當一個subobject constructor調用了一個virtual function(無論是直接調用仍是間接調用時)。

若是咱們聲明一個PVertex對象,而後因爲咱們對其base class constructors的最新定義,其vptr將再也不須要在每個base class constructors中被設定。解決之道是把constructor分裂爲一個完整的object實體和一個subobject實體。在subobject實體中,vptr的設定能夠省略(若是能夠的話)。

對象複製語意學(Object Copy Semantics)

一個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並不表示bitwist copy semantics是nontrival。實際上,只有nontrivial instances纔會被合成出來

對於Point class定義以下:

class Point{
public:
    Point(float x = 0.0, float y = 0.0);
    //... 沒有virtual function
protected:
    float _x, _y;
};

當有以下賦值(assign)操做:

Point a, b;
a = b;

由bitwise copy完成,把Point b拷貝到Point a,其間並無copy assignment operator被調用。從語意或效率上考慮,這都是咱們所須要的,注意,咱們仍是可能提供一個copy constructor,爲的是把name return vale(NRV)優化打開,copy constructor的出現不該該讓咱們也必定要提供一個copy assignment operator。

如今我要導入一個copy assignment operator,用以說明該opeartor在繼承之下的行爲:

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 opeartor,編譯器就必須合成一個(由於前述的第二項和第四項理由),合成而得的東西可能看起來像這樣:

//被合成的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;
}

下面是個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;
}

這部分太難了,摸了半天沒摸清楚,等下次再啃吧。

析構語義學(Semantics of Destruction)

若是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。

爲了以爲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)的使用就會有錯誤的傾向。

一個由程序員定義的destructor被擴展的方式相似constructors被擴展的方式,但順序相反:

  1. destructor的函數本體如今被執行,也就是說vptr會在程序員的代碼執行前被重設(reset)
  2. 若是class擁有member class objects。然後者擁有destructors,那麼它們會以其聲明的順序的相反順序被調用
  3. 若是object內含一個vptr,那麼首先重設(reset)相關的virtual table
  4. 若是有任何直接的(上一層)nonvirtual base classes擁有destructors,它們會以其聲明順序的相反順序被調用
  5. 若是有任何virtual base classes擁有destructor,而目前討論的這個class是最尾端(most-derived)的class,那麼它們會以其原來的構造順序的相反順序被調用

就像constructor同樣,目前對於destructor的一種最佳實現策略就是維護兩份destructor實體:

  1. 一個complete object實體,老是設定好vptr(s),並調用virtual base class destructors
  2. 一個base class subobject實體;除非在destructor函數中調用一個virtual function,不然它毫不會調用virtual base class destructors並設定vptr。

一個object的生命結束於其destructor開始執行之時。因爲每個base class constructor都輪番被調用,因此derived object實際上變成了一個完整的object。例如一個PVertex對象歸還其內存空間以前,會依次變成一個Vertex3d對象、一個Vertex對象、一個Point3d對象,最後成爲一個Point對象。當咱們在destructor中調用member functiions時,對象的蛻變會由於vptr的從新設定(在每個destructor中,在程序員所供應的碼執行以前)而受到影響。

相關文章
相關標籤/搜索