C++的構造函數總結


 構造函數是C++的一個很基礎的知識點,在平時編程的時候,相信大家都很熟悉,雖然比較基礎,但是細究下來,還是有不少細節需要注意。這篇文章主要總結C++構造函數需要注意一些細節,一方面,可以幫助下大家鞏固下這方面知識。同時,也是有助於自己更好得整理以前的知識。
  
讓我們由一個對象的創建開始。當一個對象創建的時候,編譯器就會調用這個對象的構造函數,在這個時候,或許大家就會有疑問了:我並沒有為對象指定構造函數,那么編譯器調用的構造函數由哪里來呢?

有這點疑惑就是一個好的開始,那么當我們沒有指定構造函數時,編譯器調用的構造函數由哪里來呢?,答案是編譯器會自己為對象產生所需的構造函數。

那么現在又有了兩個問題:
>1.編譯器在什么條件下會為我們自動生成默認構造函數?
>
>2.自動生成的構造函數主要做了什么?

我們先來回答問題1,答案是:
>在我們沒有對象指定構造函數的時候,編譯器會為我們生成默認構造函數,拷貝構造函數,默認析構函數。

這樣的話,無論我們使用通過new,拷貝來構造一個對象就都可以完成了。在此需要提及一下拷貝構造函數和賦值構造函數的區別,請看下例:

class Obj{}; // 聲明一個對象Obj
Obj a; //調用默認構造函數來構造對象 
Obj b(a);//調用默認拷貝構造函數來構造對象
Obj c = b;//調用的也是拷貝構造函數,最好將其寫做 Obj c(b)。

 

所以,當我們不需要編譯器生成的構造函數時,就應該明確說出來,即如果們聲明自己的構造函數,拷貝構造函數的話,編譯器就不會為我們生成這些函數了。
通過利用這點我們就可以限制對象的產生,例如,我們將默認構造函數,拷貝構造函數聲明為私有,就可以防止外界來產生這個對象,這點主要是在單例模式中使用。

在上面我們了解了編譯器會自動為對象生成函數的條件。下面來看第二個問題。

在這個問題中,包含了兩個構造函數:默認構造函數和拷貝構造函數。下面我們將分別回答這個問題。

1.編譯器生成的默認構造函數主要做了什么?

實際這個問題中的表述是不准確的,因為按照標准,編譯器只在需要的時候產生才產生一個符合編譯器要求的默認構造函數,其他情況下是不會產生一個默認構造函數,因為是不需要的。那么什么是需要的時候呢?答案如下:

>1.內部的成員變量擁有默認構造函數,如果有多個成員變量,那么會按照成員變量聲明的順序來調用成員變量的默認構造函數。

>2.基類擁有默認構造函數。在子類構造的時候,需要先構造父類。

>3.類中聲明有虛函數,因為編譯器需要為類中的虛函數表指針指定正確的地址。

>4.帶有虛基類(virtual base class)。因為編譯器需要確定下來虛基類在對象中的偏移,以方便調用虛基類中數據。

上面四種情況下,編譯器會為對象合成默認構造函數,而通過上面的情況,也可以知道編譯器合成的默認構造函數做了什么(此知識點在《深度探索C++對象模型中》詳細描述)。同時也要注意一點:編譯器合成的默認構造函數並沒有初始化成員變量,如果要為成員變量在構造是指定特定的值,需要在自定義的構造函數中來指定。

第一個問題可以告一段落,下面我們來看默認拷貝構造函數做了什么。

回答這個問題前,我們再來回顧下拷貝構造函數的調用時機,在以下三種情況下會調用對象的拷貝構造函數

>1.以一個對象的值作為另一個對象的初值。例如:

class Obj{};
Obj a;
Obj b = a; 
Obj c(a);

 >2.當作為函數的參數時。例如:

class Obj{};
void Foo(Obj obj);

>3.當作為函數的返回值時。例如:

class Obj{};
Obj foo()
{
    Obj obj;
    return obj;//調用拷貝構造函數。
}

由上面第二種情況可知,**在函數中使用引用傳參可以減少對象的構造**。

了解完拷貝構造函數的調用時機,我們再來看看編譯器生成的默認拷貝構造函數都做了什么。默認拷貝構造函數主要作用是按位拷貝,在必要的時候,除了按位拷貝,還插入一些其他行為,具體內容請看先前的文章----[C++拷貝構造函數總結](http://www.cnblogs.com/yetuweiba/p/3390853.html)。在實現拷貝構造函數中我們需要注意一點深拷貝和淺拷貝,防止出現的拷貝不完全的錯誤。

以上就是構造函數的語法基礎知識點,根據上面的原理,可以總結出來一些容易出錯的地方,具體請看下面。

>1.如果對象擁有成員變量,需要在自定義構造函數中設定初值,盡可能地使用成員初始化列表對成員變量進行初始化。同時,成員變量是按照聲明的順序來構造的,所以,要注意依賴。

在構造函數中對成員變量進行初始化的話,在編譯器中實際上是先調用此成員變量的構造函數,再調用它的賦值函數,所以,使用成員初始化列表會節省效率。

成員變量的構造是按照聲明的順序進行的,所以,不要讓聲明早的成員變量依賴聲明晚的成員變量。示例如下

class Obj
{
private:
    int a;
    int b;
// 下面的構造函數是錯誤的
Obj(const int value)
: a(b + 1)    // error,此時b還未構造好,a的值會不可預料。
, b(value) // 正確的做法是調換a和b的聲明順序,在成員初始話列表中也調換a和b的順序
{}
};

 

>2.謹慎在構造函數中調用虛函數(最好不要在構造函數中調用虛函數)。

我們先來舉一個例子:

class Base
{
    Base()
    {
        fun();//error,此時調用的是Base的fun,會發生調用錯誤。
    }
    virtual void fun() = 0;
};
class Derivate : public Base
{
    Derivate(){};
    virtual void fun()
    {
        std::cout << "I am Derivate" << std::endl;
    }
};

Derivate d;

上面代碼中,構造d會發生錯誤,因為基類早於派生類進行構造,在基類構造的時候,是不會下降到派生類中的,也就是此時只會調用基類的函數,而Base中的fun是存虛函數,此時就會發生錯誤。雖然我們可以在Derivate的構造函數中調用fun,但這樣做,就意味着接口的設計出現了問題。所以,最好不要在構造函數中調用虛函數。

>3.在並發環境下,注意構造函數的安全性。

當在並發的環境下,原本簡單的事情就會變的復雜。我們都知道,在並發環境下,線程的執行是亂序的,我們拿到一個指向對象的指針,這個指針指向的可能是構造函數執行一般的對象,例如:

class Foo : public Observer
{
    // error
    Foo(Observable * s)
    {
        s->register(this)
    }
}

上面例子的做法是不安全的,因為在構造函數中將自身注冊出去,此時對象可能仍未完全構造完。如果,Foo是積累的話,那么注冊的時候派生類還沒有構造完成,此時也會引發錯誤。所以,由上例可知:在對象構造期間不要對外暴露this指針(陳碩)。解決上面錯誤的一個方法就是“二段式構造”,即對象構造完成后,再對外暴露this指針。


以上就是關於C++中構造函數的一些總結,如果有錯誤的地方,請大家多多指教,謝謝。另外,不支持Markdown,有些不方便。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM