構造函數語意學(一):默認構造函數

“C++類中,如果類設計人員沒有爲類聲明構造函數,那麼編譯器就會合成一個默認構造函數來初始化類的數據成員” 那麼上述這句話對麼?答案是:否。

這是C++程序人員常犯的錯誤(當然是針對我這種新手而言,呵呵),那麼原因是什麼呢?讓我們今天來探討探討。

默認構造函數在需要的時候被編譯器產生出來,關鍵字眼在於:在需要的時候,被誰需要?做什麼事情?請看下列的程序代碼:

class Test

{

public:

int m_iVal;

Test* m_pNext;

}

void Test_bar()

{

 Test  test;//聲明瞭一個Test對象,從這個程序功能來看,似乎是需要test的所有成員都被清零或者初始化

 if(test.m_iVal || test.m_pNext)

// .... do something

}

其實,本段代碼類Test的兩個成員並不會被初始化,畢竟巧婦難爲無米之炊,包含的類對象自己自己都 沒得默認構造函數,那編譯器也沒辦法了噻。噢,我聽到你在尖叫,不是在需要的時候會合成默認構造函數麼?現在怎麼不行了?其實問題的本質在於,這個需要是誰需要,在C++中,一種是編譯器需要,一個是程序需要。上面的測試函數,初始化是程序需要而已,所以承擔責任的應該是設計class Test 的人,因此上述程序並不會合成出一個默認的構造函數。

那麼,問題來了,什麼情況下會合成默認構造函數呢??答案是當編譯器需要的時候纔會合成,存在以下四種情況,讓我們一個一個的來探討吧。

情況1:如果一個class沒有任何構造函數,但是在它內部有一個成員,而這個成員有默認構造函數,那麼編譯器這個時候就會爲此class合成一個默認構造函數;

例如:

class Foo {

public:

          Foo();

  Foo(int);

}

class Bar{

public:

  Foo    foo;

  char*  str;

}

/////////////////////////////////////////////////////

void foo_bar()

{

Bar bar;//Bar::foo會在此初始化,Bar::foo是類Bar的一個成員對象,而類Foo有默認構造函數

if(str ){......   }

}

注:合成的默認構造函數只會初始化bar對象,而它不會初始化str。因爲現在初始化Bar::foo是編譯器的責任,而初始化str是程序的責任

當然,這個時候你想要初始化str,那你得寫一個構造函數來初始化,於是上面的類變成了這個樣子:

class Bar{

public:

         Bar(){ str = 0;}

public:

  Foo    foo;

  char*  str;

}

請注意,一旦設計人員顯示的寫了構造函數,編譯器就沒法合成第二個了,直觀上來說,Bar::foo對象並沒有被初始化,但是編譯器還有一個神奇的功能,雖然它不會合成第二個,但是它會擴展現有的構造函數,來初始化包含在該類的數據對象。因此上述的構造函數Bar()擴展後可能是這個樣子

Bar::Bar()

{

  foo.Foo::Foo();//調用Foo的構造函數來初始化foo對象

str  = 0;//現有構造函數的語句

}

現在我們擴展一下,考慮下述情況:一個類中包含了多個其它類的對象,那這個時候編譯器擴展或者合成的構造函數的初始化操作是怎樣進行的。

答案是:這個時候C++編譯器會按照這些類成員對象在該類的聲明次序來進行構造函數的擴展。請看下例:

class Test1 {public: Test1();....}

class Test2{public: Test2();.....}

class Test3{public: Test3();....}

class Test{

    public:

Test():test2(1024) {  number = 2048; }

     public:

Test1 test1;

  Test2 test2;

Test3 test3;

private:

int  number;

}

則這個構造函數Test會被擴張爲:

Test::Test():test2(1024)

{

test1.Test1::Test1();

test2.Test2::Test2(1024);

test3.Test3::Test3();

number = 2048;

}

情況2:如果一個類沒有任何的構造函數,但是其基類有默認構造函數,那麼這個類就會由編譯器合成一個默認構造函數。這個情況比較簡單,很好理解。

情況3:帶有虛函數的類,如果沒有任何構造函數,那麼編譯器會合成默認構造函數;

              (1).class 聲明或繼承一個virtual函數;

      (2).class 派生於一個繼承鏈,其中一個或者多個virtual基類;

請看代碼:

//抽象基類

class Widget{

public:

virtual void flip() = 0; 

}

void flip(const Widget& widget){ widget.flip(); }

              class Bell:public Widget{  };

      class Whistle:public Widget{  };

//存在如下函數

    void foo()

{

  Bell b;

Whistle w;

 flip(b);

flip(w)

}

編譯期間會進行擴張:一個virtual function table 被產生出來;在每個類對象中,還會產生一個額外的pointer member,也就是指向虛函數表地址的指針。

情況4:帶有一個虛基類的類,這個我自己也不是很明白,大家可以查閱相關資料進行分析。

還有幾點需要注意的是:(1).一個類聲明瞭構造函數,編譯器不會合成第二個,而是在此構造函數上進行擴展;

                                            (2).一個類中包含多個帶有默認構造函數的類,那麼編譯器合成的默認構造函數,拷貝構造函數,析構函數等都會以inline的方式完成,因爲inline函數 是靜態鏈接,不會被檔案以外的人看到

好了,現在來總結下:

以上四種情況能合成默認構造函數,另外,在合成的默認構造函數中,只有基類對象,和內含於類的其它類成員會被初始化,至於本類的其它非靜態的數據對象不會被初始化,這些是程序需要,所以和編譯器關係不大,需要設計人員顯示的進行初始化。











發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章