C++對象模型-構造函數語意學

1.默認構造

因爲編譯器會盡量的爲全部的警告和錯誤作出解釋。但也所以致使了部分狀況下的過分解析。
書中給的例子是編譯器因爲過分解析,使用了類型轉換運算符的解析代碼,致使隱藏了真正的錯誤。html

cin << intval;
int temp = cin.operator int();
temp << intval;

分析一下:ios

  • 程序員的目的是實現讀取輸入,可是誤將 >> 寫成了 <<, 而istream並無重載 << 運算符,編譯器一看這條路不通啊,因而只好按照 << 左移位來解析;
  • but,要想實現左移位,又必須將cin轉成整型。那麼編譯器就去找istream有沒有類型轉換函數好將cin轉成整型後再進行移位操做。若是找到了,那麼,cin << intval; 正常執行了,並不報錯。但不符合程序員的本意。
  • 爲了不這種轉換髮生,istream中使用了operator void*()來替換operator int()。

要注意因爲隱式轉換可能形成的不良後果。程序員

(1) 隱式類型轉換

隱式類型轉換雖然會"暗地裏"作一些轉換操做,但這種機制的好處也是顯而易見的。並且C++爲了讓這個"暗地裏"的隱式操做可以被程序員察覺顯形,提供了一個修飾符"explicit",類型轉換構造函數和類型轉換函數聲明前加上explicit關鍵字將阻止編譯器隱式類型轉換操做。任未嘗試隱式轉換的操做都會報錯。

以下:函數

#include<iostream>
using namespace std;
class A
{
public:  
    //explicit
    A(int a):m_a(a)
    {
        cout << "construct A from int" << endl;
    }
    //explicit
    operator int()
    {
        cout <<"convert A to int " << end; 
        return m_a;
    }
public:    
    int m_a;
};
int main(void)
{
    A a(5);//顯式類型轉換構造
    A b = 5;//隱式類型轉換構造
    int i = a;//隱式類型轉換函數
    return 0;
}
  • A a(5);老是正常的。
  • 第一個explicit註釋掉,A a(5);運行正常;打開該註釋,編譯提示:類型轉換失敗
  • 第二個explicit註釋掉,int i = a;運行正常。 打開該註釋 編譯提示:非法的存儲類,即賦值失敗。

(2) 默認構造函數

討論trivial和notirvial其實就是討論構造函數存在的必要性。構造本質上要爲對象的生成作一些輔助操做。
但若是對象生成的需求僅僅是分配空間就夠了,那麼構造函數其實也沒有什麼意義。
構造函數確實不是必須的,甚至於某些狀況下,編譯器連繫統默認構造函數都不會提供
好比有一個類就像C中結構體的同樣,struct A* pa = new (sizeof(struct A));就足夠,即不須要初始化成員變量,也不須要負責成員變量的構造,更不須要初始化虛函數表,或者虛基類表。那麼編譯器也沒有必要提供一個默認構造畫蛇添足了。優化

  • 構造時先構造基類,再構造子類的非基類部分,析構時先析構子類的非基類部分,再析構基類。
  • 初始化表用於指導基類子對象或者成員變量如何初始化,也包括類類型的成員變量。
  • 基類子對象按照繼承順序必須在初始化表中指定構造方式,且必須在子類的非基類成員以前被構造出來
  • 對於類類型成員變量,既能夠按照初始化表中指示進行構造,也能夠以後在函數體中按照聲明順序依次構造對象成員。
  • 子類構造函數執行時會先按照繼承順序插入調用各基類的構造函數的代碼。再調用用戶定義的子類的構造部分。
  • 無論子類是否默認構造,其基類子對象或者是子類中的類類型成員即便是默認構造,子類至少要提供默認構造來調用基類的構造函數。除非基類連默認構造都不須要。
  • 基類若是是有參構造,則子類必須顯式指定構造函數,由於要指示基類如何構造。提供的默認構造沒法作到調用基類有參構造。故而須要自定義構造函數來指導基類構造方式。對於類的類類型成員變量也是如此。

綜合來看,做者想要說明的是:在基類或者類成員沒有自定義構造函數時默認構造函數的做用,在如下幾種條件下會體現即:spa

  • 有類類型成員變量須要被構造時,須要調用類類型成員的默認構造函數來調用該成員對象的默認構造。
  • 有基類子對象須要被構造時,須要調用子類的默認構造函數來調用基類的默認構造
  • 帶有虛函數的類,須要其默認構造來初始化每一個對象的vptr,注意:虛函數表和函數指針覆蓋是在編譯期完成的。
  • 虛繼承的類,須要其默認構造來初始化每一個對象的虛基類表,注意:虛基類初始化表是在編譯期完成的。

看做者總結的四種狀況(沒有自定義構造函數的狀況下):指針

public class B
{
    B(){}
};
class A
{
    class B b;
};
  • 類類型的成員變量須要調用默認構造,即B沒有自定義構造時;
public class B
{
    B(){}
};
class A:public class B
{
};
  • 基類須要調用默認構造,即B沒有自定義構造時
class A
{
virtual fun(){}
};
  • 存在虛函數時須要經過默認構造函數來,即A::fun須要動態尋址時
class A : virtual public class B
{
};
  • 存在虛基類時須要經過默認構造函數來,即A中的B子對象須要動態尋址時

2.默認拷貝構造

(1) 默認拷貝構造

類須要執行拷貝構造的三種情形code

  • 明確使用 = ;
  • 對象做爲實參;
  • 做爲返回值,不考慮編譯器優化(部分編譯器會把臨時對象直接做爲有名對象返回,從而減小一次拷貝構造)

注意:拷貝構造的本質是仍是構造,本質操做是初始化操做,而非拷貝操做。
拷貝構造一樣被分紅了trivial和notirvial,是trivial仍是notrivial和默認構造解釋差很少。先理解下幾個名詞。htm

bitwise copy是編譯器默認提供的位拷貝,即memcpy系列,以bit爲單位。加個semantics(語意)引伸意義後面講。
memberwise init即基本類型成員的賦值,以成員爲單位。對象

bitwise copy semantics:位拷貝語意,即一個類的拷貝構造過程當中的初始化操做應該是固定且連續的memwise init,不能被安插子對象或者類類型的成員變量的拷貝構造(儘管子對象或者類類型的成員變量內部也是遞歸mmwise init的),另外也不能因爲存在虛函數或者虛基類增長拷貝構造操做而在該類的拷貝構造中額外增長虛表指針或者虛基類表指針的重定位操做。

換句話說:bitwise copy semantics上只能有 POD數據類型C++ POD(Plain Old Data)類型
某些狀況下對象不能有bitwise copy semantics,不然拷貝構造會出現問題。即上述的幾個不能。

如下四種狀況不該該表現出bitwise copy semantics,默認構造必須是notrivial的

  • 含有拷貝構造的基類子對象
  • 類類型成員變量有拷貝構造
  • 類聲明有虛函數時
  • 類派生自虛基類時

前兩種,基類子對象或者類類型成員的拷貝構造必須被當前對象的拷貝構造調用。默認拷貝構造必須是notrivial。
類存在虛函數表vtbl時或者繼承自虛基類時,須要重定虛表指針,默認拷貝構造必須是notirvial的。
也就是默認拷貝構造必需要有才行。

最終要回歸trivial和notrivial,不要陷在bitwise copy semanstics裏。

(2) 程序轉化語意學

相關文章
相關標籤/搜索