1. 程式人生 > >C++物件模型:構造、析構、拷貝語意學

C++物件模型:構造、析構、拷貝語意學

目錄

1、有關純虛擬函式

2、無繼承情況下的函式構造

2.1struct 構造

2.2抽象資料型別

2.3有虛擬函式的情況

3.繼承體系下的物件構造

3.1概述

3.2虛擬繼承的情況

3.3vptr的初始化

4.物件複製語意學

5.解構函式語意學


1、有關純虛擬函式

抽象基類的資料成員初始化

如果一個類被宣告為抽象基類(其中有 pure virtual function),則抽象基類不能例項化,但它仍需要一個顯示的建構函式以初始化其成員變數。如果沒有這個初始化操作,其 derived class 的區域性物件 _mumble 將無法決定初值。

class ABC
{
public:
    virtual void interface() = 0;                          //考慮儘量不用const

    const char* mumble() const { return _mumble; }        //不會被派生類改寫的不要用virtual

    virtual ~ABC() { };                              //不要將解構函式宣告為pure virtual
protected:
    char* _mumble;   
    ABC(char* mumble_value = 0);
}

//初始化其成員變數
ABC::ABC(char* mumble_value) : _mumble( mumble_value ) { };

這樣,可以由 derived class 的建構函式去初始化其 ABC 部分的成員變數。

關於虛解構函式,不要將 virtual destructor 宣告為 pure。因為每一個 derived class destructor 會被編譯器加以擴張,以靜態的方式呼叫其每一個 virtual base class 以及上一層 base class 的 destructor,只要缺乏任何一個基類解構函式的定義就會導致連結失敗。而此時如果此時是純虛擬函式,則不可能有解構函式來呼叫。

2、無繼承情況下的函式構造

2.1struct 構造

global 變數存放於全域性靜態區,區域性變數存放於棧,用 new 方法生成的物件存放於堆。

Point global;
Point foobar() {
    Point local;
    Point *heap = new Point;
    *heap = local;
    delete heap;
    return local;
}

C++中的Plain Ol' Data 宣告,類似於結構體,只有成員無相關操作。以C++來編譯這段程式碼的話,編譯器會為Point宣告:trivial預設建構函式,trivial解構函式,trivial拷貝建構函式,trival拷貝賦值運算子。 

1.第一行程式碼Point global;會在程式起始時呼叫Point的constructor,然後在系統產生的exit()之前呼叫destructor。 
在C++中,全域性物件都是被以“初始化過的資料”來對待,因此置於data segment。 
2.第四行程式碼 Point *heap = new Point;宣告一個堆上的物件,其中new運算子會被轉化為:Point *heap = __new(sizeof(Point));
此時並沒有default constructor施行於*heap object(無自身定義的default constructor,系統提供的建構函式會使初值不確定)。 
3.第五行程式碼 heap = local;由於local沒有初始化,因此會產生編譯警告”local is used before being initaalized”。 
接著delete heap;會被轉化為__delete(heap);這樣會觸發heap的trival destructor。 
4.最後函式已傳值的方式將local當作返回值傳回,這樣會觸發trival copy constructor,不過由於該物件是個POD型別,所以return操作只是一個簡單的bitwise copy。

2.2抽象資料型別

提供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) { }
private:
    float _x, _y, _z;
};

對於global例項: Point global;
現在有了預設建構函式作用在其身上,由於global定義在全域性範疇,其初始化操作會延遲到程式啟動(startup)時才開始。(統一構造一個_main()函式,該函式內呼叫所有global物件的預設建構函式), 具體見第六章。 

考慮Point *heap = new Point;與之前不同在於,Point 有自己的 nontrival 預設建構函式,所以會顯式呼叫這個預設建構函式如下:

Point *heap = __new(sizeof(Point));
if(heap != 0)
    heap->Point:Point():

觀念上,我們的 Point class 有一個相關的預設拷貝構造、拷貝運算子和解構函式會生成,然而他們都是不重要的(trivial),而且編譯器實際上根本沒有生成他們。

2.3有虛擬函式的情況

class Point {
public:
    Point(float x = 0.0, float y = 0.0) :
        _x(x), _y(y) { }
    virtual float z();
private:
    float _x, _y;
};

構造時,不僅會為每個物件多引入一個4 bytes 的 vptr 外,虛擬函式的匯入也會引發編譯器對類的膨脹作用:

Point::Point(Point *this, float x, float y) : 
    _x(x), _y(y) {
    this->__vptr_Point = __vbtl_Point;//設定虛表
    this->_x = x;  //擴充套件初值列表
    this->_y = y;
    return this;//返回this
}

人為合成一個copy constructor和一個copy assignment operator,其操作是 nontrival,但是 implicit destructor 仍然是trival。如果以一個子類物件初始化父類物件,且是以位運算為基礎,那麼vptr的設定有可能是非法的。

//拷貝建構函式的內部合成
inline Point*
Point::Point(Point *this, const Point& rhs) {
    this->__vptr_Point = __vbtl_Point;
    //將rhs座標的連續位拷貝到this物件
    return this;
}

同樣在foobar()中, *heap = local 的操作很有可能觸發拷貝賦值運算子函式的生成,及其呼叫操作的一個 inline expansion :以 this 取代 heap 。以 rhs 取代 local 。

3.繼承體系下的物件構造

3.1概述

當我們定義一個object如:T object  實際上會發生什麼呢?如果T有一個constructor(不管是trival還是nontrival),它都會被呼叫,建構函式可能含有大量的隱藏程式碼。根據 class T 的繼承體系,編譯器會擴充每一個 constructor 。

1.如果有 virtual base class,虛基類的建構函式必須被呼叫,由淺到深,從左往右:

- 如果class位於成員初始化列表,有任何顯示指定的引數都應該傳遞過去;若沒有位於初始化列表,而class含有一個預設建構函式,也應該呼叫。

- class中的每一個virtual base class subobject的偏移量必須在執行期可存取。

- 如果class是最底層的class,其constructors可能被呼叫。 

2.如果有base class,基類的建構函式必須被呼叫; 

- 如果class位於成員初值列,有任何顯示指定的引數都應該傳遞過去。 
- 若沒有位於初值列,而class含有一個預設構造(拷貝)函式,也應該呼叫。 
- 如果class是多重繼承下的第二或者後繼的base class,那麼this指標應該有所調整。 

3. 如果有虛擬函式,必須設定vptr指向適當的虛表; 

4. 如果一個member沒有出現在成員初值列表中,但是該成員有一個預設建構函式,那麼這個預設建構函式必須被呼叫; 

5. 成員初值列表中的member初始化操作放在constructor的函式體內,且順序和宣告順序一致。

繼承情況下的物件構造順序為:虛基類 -> 基類 -> vptr與虛表 -> 類成員初始化 -> 自定義的程式碼。

再次以 Point 為例,增加拷貝構造、拷貝賦值運算子、虛解構函式,Line class 在 Point 基礎上擴充。

class Point {
public:
    Point(float x = 0.0, float y = 0.0);
    Point(const Point&);
    Point& operator=(const Point&);
    virtual ~Point();
    virtual float z() { return 0.0; }
private:
    float _x, _y;
};

class Lines {
    Point _begin, _end;
public:
    Lines(float = 0.0, float = 0.0, float = 0.0, float = 0.0);
    Lines(const Point&, const Point&);
    draw();
};

來看 Line 建構函式的定義與內部轉化:

Lines::Lines(const Point& begin, const Point& end) :
    _begin(begin), _end(end){} //定義

//下面是內部轉化,將初值列中的成員構造放入函式體,呼叫這些成員的建構函式
Lines* Lines::Lines(Lines *this, const Point& begin, const Point& end) 
{
    this->_begin.Point::Point(begin);
    this->_end.Point::Point(end);
    return this;
};

如果使用了Lines b = a; 這個時候呼叫合成的拷貝建構函式,合成的拷貝構造在內部可能如下:

inline Lines&
Lines::Lines(const Lines& rhs) {
    if(*this = rsh) return *this;   
    //證同測試,或者可以採用copy and swap,具體見effective C++
    //還要注意深拷貝和淺拷貝
    this->_begin.Point::Point(rhs._begin);
    this->_end.Point::Point(rhs._end);
    return *this;
}

3.2虛擬繼承的情況

考慮下面這個虛擬繼承(繼承自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; }
private:
    float _z;
}

虛擬繼承時,由於virtual base class的共享性的原因,傳統的 constructor 擴張並沒有用。例如以下繼承關係:

實際Point3d的constructor的擴充如下:

//c++偽碼
Point3d*
Point3d::Point3d(Point3d* this, bool __most_derived,
                float x = 0.0, float y = 0.0, float z = 0.0) {
    if(__most_derived != false) this->Point::Point();
    //虛擬繼承下兩個虛表的設定
    this->__vptr_Point3d = __vtbl_Point3d;
    this->__vptr_Point3d__Point = __vtbl_Point3d__Point;
    this->z = rhs.z;
    return this;
}

_most derived 在自己(最底層派生類) 構造時為 true ,在派生路徑上的任一父類呼叫時設為 false

在更加深層次的繼承情況下,例如Vextex3d,呼叫Point3d和Vertex的constructor時,總會將該引數設定為false,於是就壓制了兩個constructors對Point constructor的呼叫操作。 例如:

//c++偽碼
Vextex3d*
Vextex3d::Vextex3d(Vextex3d* this, bool __most_derived,
                float x , float y , float z ) 
{
    if(__most_derived != false) 
        this->Point::Point();         //只由Vertex3d構造一次Point

    //設定__most_derived為false
    //呼叫上一層 base classes
    this->Point3d::Point3d(false, x, y, z);
    this->Vertex::Vertex(false, x, y);

    //設定vptrs
    return this;
}

從而保證了子類中只有一個虛擬繼承的 subobject 。

3.3vptr的初始化

vptr的初始化操作:在base class constructor呼叫操作之後,但是在程式設計師供應的程式碼或”成員初值列中所列出的成員初始化操作”之前。 

不能放在任何操作之前:基類中有 virtual function 呼叫,基類構造完成時,派生類構造才能覆蓋重寫的 virtual function。

不能在所有初始化過程之後:當一個 subobj constructor 呼叫一個 virtual function 時,希望呼叫的是自己類對應的 virtual function 而不是派生類的 virtual function 。

之前的PVertex constructor可能被擴張成:

PVertex*
PVertex::Pvertex(PVertex*this, bool __most_derived,
            float x, float y, float z) 
{
    //有條件呼叫virtual base class constructor
    if(__most_derived != false) this->Point::Point();
    //無條件呼叫上一層base class constructor
    this->Vertex3d::Vertex3d(x, y, z);

    //設定vptr
    this->__vptr_PVertex = __vtbl_PVertex;
    this->__vptr_Point3d__PVertex = __vtbl_Point3d__PVertex;

    //執行程式設計師的程式碼
    if(spyOn)
        cerr << "Within Pvertex::PVertex()"
             << "size: "
             //虛擬機制呼叫
             << (*this->__vptr_PVertex[3].faddr)(this)
             << endl;
    return this;
}

在 class 的 constructor 的 成員初始化列表中呼叫該 class 的一個 virtual function 安全嗎?

實際而言,vptr 保證能夠在成員初始化之前由編譯器設定好,總是安全的。但在語意上這可能是不安全的,因為 virtual 函式本身可能會依賴未被設定初值的成員變數。

4.物件複製語意學

在不涉及虛擬繼承只有一個子物件的情況下,編譯器合成的派生類的賦值運算子函式會呼叫所有即時基類的 operator = 函式:

inline Point&
Point::operator=(const Point& p) {
    _x = p._x;
    _y = p._y;
    return *this;
}

//Point3d虛擬繼承自Point
class Point3d : virtual public Point {
public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0);
private:
    float _z;
};

//如果沒有為Point3d設定一個copy assignment operator,編譯器就會為其合成一個
inline Point3d&
Point3d::operator=(point3d* this, const Point3d& p) {
    this->Point::operator=(p);  //base class operator=
    _z = p._z;  //memberwise copy
    return *this;
}

同樣考慮之前的繼承體系,類Vertex虛擬繼承自Point,並且從Point3d和Vertex派生出Vertex3d。則編譯器生成的 copy operator 如下:

inline Vertex&
Vertex operator=(const Vertex& v) {
    this->Point::operator=(v);
    _next = v._next;
    return *this;
}
inline Vertex3d&
Vertex operator=(const Vertex3d& v) {
    this->Point::operator=(v);
    this->Point3d::operator=(v);    //在 Point3d 的複製運算子函式抑制 Point 的複製運算子函式
    this->Vertex::operator=(v);     //在 Vertex 的複製運算子函式抑制 Point 的複製運算子函式
    return *this;
}

編譯器如何在Point3d和Vertex的copy assignment operator抑制 Point 的 copy assignment operator 呢?constructor 中的解決辦法是新增一個額外的引數 _most_derived。

編譯器生成預設 copy assignment operator 的解決辦法是:為copy assignment operator提供 分化函式( split functions )以支援這個 class 稱為 most-derived class 或者成為中間的base class。( 即分情況特殊處理 ) 

也可以將虛基類的copy assignment operator 呼叫放在最後,雖然也會造成重複拷貝,但是語意正確。
建議:儘可能不要允許一個virtual base class的拷貝操作,更進一步,不要在任何virtual base class中宣告資料。

5.解構函式語意學

解構函式也是根據編譯器的需要才會合成出來,兩種情況: 
1. class中內含的某個object擁有解構函式; 
2. 繼承自某個base class,該base class含有解構函式

例如,在 Point 類中,預設情況下並沒有被編譯器合成出一個 destructor ,甚至它擁有一個 virtual function 。如果把兩個 Point 物件 _begin 和 _end 組合成一個 Line 類,也不會合成出一個 destructor。當我們從 Point 派生出 Point3d(即使是虛擬派生),如果沒有宣告一個 destructor ,編譯器就沒有必要合成一個 destructor 。使用 new 與 delete 管理的物件在非必要情況下也不會生成 destructor 。

與建構函式相比,即使擁有虛擬函式或者虛擬繼承,不滿足上述兩個條件,編譯器是不會合成解構函式的。

如果說 Vertex 解構函式牽扯從連結串列中刪除一個節點(或者其他語意),那麼這時候定義 destructor 就是必要的。而且其派生的類Vertex3d 如果我們不定義一個 explicit destructor ,編譯器也會合成一個,其唯一任務是呼叫 Vertex destructor。若自己定義了解構函式,則編譯器會擴充套件它使它呼叫 Vertex destructor。

在繼承體系中,由我們定義的destructor的擴充套件方式和constructor類似,只是順序相反,順序如下: 
1. destructor的函式體首先執行。 
2. 如果class擁有member class object,且該class含有destructor,那麼它們會以宣告順序相反的順序依次被呼叫。 
3. 如果object內含一個vptr,重新被設定指向適當的base class的virtual table(即物件在析構的過程中,依次蛻變為其基類)。 
4. 如果有任何上層的nonvirtual base classes擁有destructor,那麼它們會以宣告順序相反的順序依次被呼叫。 
5. 如果有任何virtual base classes擁有destructor,那麼它們會以原來構造順序相反的順序依次被呼叫。

 

 

 

主要參考:
作者:幸福的起點_ 
來源:CSDN 
原文:https://blog.csdn.net/qq_25467397/article/details/80451635 
版權宣告:本文為博主原創文章,轉載請附上博文連結!