1. 程式人生 > >《深度探索C++物件模型》筆記(三)建構函式、拷貝構造和初始化列表

《深度探索C++物件模型》筆記(三)建構函式、拷貝構造和初始化列表

歡迎檢視系列部落格:

--------------------------------------------------------------------------------------------------------------

        看了這一章後發現:原來最難搞懂的是拷貝建構函式。

2.1預設建構函式.

        C++標準是這麼說的:對於class X,如果沒有任何user-decleared-constructor,那麼會有一個default constructor被暗中(implicitly)宣告出來。。。。一個被暗中宣告出來的default constructor將是一個trival(無用的) constructor。

        如果class X的成員屬性帶有建構函式,那麼無論如何class x需要合成一個default constructor了。被合成的default constructor是為了滿足編譯器的需要,而不是程式。

        下面的程式碼演示了編譯器對建構函式的擴張:

    class Dopey {public :Dopey();  }
    class Sneezy { public: Sneezy(int); Sneezy();}
    class Bashful{public: Bashful();}
    //以及一個class Snow_White
     class Snow_white {
    public:
        Dopey dopey;
        Sneezy sneezy;
        Bashful bashful;
    private:
        int mumble;
}
/*如果Snow_White有了這樣一個建構函式如下:
//程式設計師所寫的 default constructor
Snow_White::Snow_White():sneezy(1024)
{
    mumble = 2048;
}
//那麼編譯器會擴張為下面的程式碼,C++偽碼
Snow_White::Snow_White()sneezy(1024)
{
    //插入member class object 並呼叫其constructor
    dopey.Dopey::Dopey();
    sneezy.Sneezy::Sneezy(1024);
    bashful.Bashful::Bashful();
    //下面是使用者的程式碼
    mumble = 2048;
}
        總結:有四種情況會導致“沒有生命constructor的class”合成一個default constructor。其他情況實際上不會合成default constructor

    1.含有“帶有default constructor的的屬性”的類。

    2.基類帶有default constructor的類。

    3.帶有virtual function的類(因為虛擬函式表指標(vptr)要在這裡構造出來)

    4.帶有一個“virtual base class” 的類

C++新手一般有兩個誤解:1)任何class如果沒有定義default constructor,就會被合成出來一個。2)編譯器合成出來的default constructor會明確設定class內每一個data member的預設值。

2.2 copy constructor的建構操作

        如果類沒有明確宣告 copy constructor 那麼類將使用default memberwise initialization來完成。如果類中的屬性也是物件,那麼將遞迴方式實施memberwise initialization。

        和default constructor 一樣copy constructor也是在必要的時候才由編譯器合成出來。如果類class x中有一個屬性 class object a,其具有explicit copy constructor那麼,class X必須合成一個copy constructor,然後呼叫a的copy constructor。

        不要bitwise copy semantics(位逐次拷貝),跟default constructor的四種情況完全一樣,那麼copy constructor也有四種情況是需要合成的。

    1.含有“帶有copy constructor的的屬性”的類。

    2.基類帶有default constructor的類。

    3.帶有virtual function的類(需要調整vptr的指標)

    4.帶有一個“virtual base class” 的類

    看下面一個繼承關係的兩個類

    class ZooAnimal{                                class Bear:public ZooAnimal{
    public:                                         public:
        ZooAnimal();                                    Bear();
        virutal ~ZooAnimal();                           void animate();
        virtual void animate();                         void draw()
        virtual void draw();                            virtual void dance();
    }                                               }
那麼下面的程式碼第二行是安全的,因為他們的vptr本來就指向同一個虛擬函式表。如下圖 

Bear yogi;

Bear winnie = yogi;

ZooAnimal franny = yogi; //這是不安全的,發生了切割(sliced)行為。如下圖所示:編譯器需要將vptr做調整:


如果有虛基類,或者多重繼承的話,就更麻煩了。如果程式設計師沒有定義明確的copy constructor那麼,編譯器會很好的工作。下面討論編譯器如何工作的:

2.3程式化語意學

    NRV(named return value)原理如下:

X bar()
{
    X xx;
    //處理x
    return x;
}

經過NRV優化後如下:

void bar(X &__result)
{
    //
    __result..X::X();
    //直接處理__result,代替x
    return ;
}
注意:啟動NRV優化的條件是:需要手動寫一個copy constructor

    memcpymemset都只有在“class 不含任何由編譯器產生的內部members(主要值得vptr)”時才能有效執行。如果class中宣告有virtual functions或者含有一個virtual base class,那麼使用上述函式將會導致那些“被編譯器產生的內部members”的初值被改變。

class Shape{
    public:
        Shape() { memset ( this , 0 , sizeof ( Shape ) ) ; }
        virtual ~Shape();
}

編譯器將為這個constructor擴張為下面

Shape::Shape()//擴張後的constructor C++虛擬碼
{
    __vptr__Shape = __vptl__Shape;
    //<strong><span style="color:#ff0000;">出問題了</span></strong>:即便是編譯器剛剛設定了__vptr__Shape的值,但是經過memset之後卻為0了。
    memset ( this,0,sizeof(Shape));
};

    copy constructor要,還是不要?

    copy constructor的應用,迫使編譯器多少對你的程式做出部分轉化。尤其是當一個函式以值傳遞的方式傳會一個class object,而該class有一個copy constructor(不管是你寫的,還是編譯器預設建立的),這將導致深奧的程式轉換。

2.4成員們的初始化隊伍

下列情況,為了讓你的程式能夠正常編譯,你必須使用member initialization list:

1.當初始化一個reference member時;

2.當初始化一個const member時;

3.當呼叫一個base class的constructor,而它擁有一組引數時。

4.當呼叫一個member class的constructor,它擁有一組引數時。

需要注意以下幾點:

1:初始化列表的真正順序是標頭檔案中的宣告的定義(順便說一下,記憶體中的順序未必是真的是宣告的順序)。不然可能出現一些初始化順序相關的問題。

2:只有當class中有的class 物件時,才起到作用,例如: 

class Word{
    String _name;
    int _cnt;
public:
    Word(){
        _name = 0;
        _cnt = 0;
        }
}

那麼c++對constructor的擴張如下:

//C++虛擬碼

Word::Word(){
    //呼叫default constructor
    _name.String::String();
    //產生臨時物件temp
    String* temp = String(0);
    //memberwise 地拷貝_name;
    _name.String::operator = ( temp );
    //銷燬臨時物件temp
    temp.String::~String();
    _cnt = 0;
}
那麼,使用初始化列表的結果是什麼樣的呢?C++虛擬碼:
Word::Word:_name(0)
{
    _cnt = 0;
}
Word::Word(/*this pointer goes here*/)//C++虛擬碼
{
    //呼叫String(int) constructor
    _name.String::String(0);
    _cnt = 0;
}

        所以,如果是基礎資料型別的話,就不必初始化列表了。這問題導致一些積極程式設計師把所有members都放到初始化列表中實現。這樣反而迷惑C++初學者,讓他們搞不清楚初始化列表的真正意思。

        上面的情況如果出現在template中,不一定保證好使!

3.最好不要在初始化列表中使用函式返返回值。

請大家詳細看書中的介紹,

哎!建構函式、copy constructor和解構函式真是C++的敗筆,Objective-C不存在棧上邊的物件,不存在臨時物件,所以不存在編譯器上面的負擔。