2011-08-27 86 views
1

這裏是一個混淆了我的代碼:在類的初始化過程中會發生什麼?

#include <iostream> 
using namespace std; 

class B { 
public: 
    B() { 
     cout << "constructor\n"; 
    } 
    B(const B& rhs) { 
     cout << "copy ctor\n"; 
    } 
    B & operator=(const B & rhs) { 
     cout << "assignment\n"; 
    } 
    ~B() { 
     cout << "destructed\n"; 
    } 
    B(int i) : data(i) { 
     cout << "constructed by parameter " << data << endl; 
    } 

private: 
    int data; 
}; 

B play(B b) 
{ 
    return b; 
} 

int main(int argc, char *argv[]) 
{ 
#if 1 
    B t1; 
    t1 = play(5); 
#endif 

#if 0 
    B t1 = play(5); 
#endif 

    return 0; 
} 

環境爲g ++ 4.6.0在Fedora 15. 第一個代碼片段輸出如下:

constructor 
constructed by parameter 5 
copy ctor 
assignment 
destructed 
destructed 
destructed 

和第2片段代碼輸出是:

constructed by parameter 5 
copy ctor 
destructed 
destructed 

爲什麼有三種析構函數被調用的第一個例子,而在第二個它是隻有兩個?

+0

您期望看到多少構造函數?如果沒有足夠的嘗試使用'-fno-elide-constructors'進行編譯,並且gcc不會消除默認情況下執行的一些構造函數調用。 –

回答

2

第一種情況:

B t1; 
t1 = play(5); 
  1. 創建通過調用B默認構造對象t1
  2. 爲了調用play(),使用B(int i)創建了B的臨時對象。 5作爲和B的對象被創建,play()被調用。
  3. return b;內部play()導致調用copy constructor返回對象的副本。
  4. t1 =調用Assignemnt運算符將返回的對象副本分配給t1
  5. 第一個析構函數,破壞在#3中創建的臨時對象。
  6. 第二個析構函數破壞#2中返回的臨時對象。
  7. 第三個析構函數破壞對象t1

第二種情況:

B t1 = play(5); 
  1. B類的臨時對象通過調用的B參數的構造這需要int作爲paraemter創建。
  2. 此臨時對象用於調用B類的複製構造函數
  3. 第一個析構函數破壞#1中創建的臨時文件。
  4. 第二個析構函數破壞對象t1

一個析構函數調用是在第二種情況較少,因爲在第二種情況下,編譯器使用Return value Optimization和elides通話,而從play()回國創建一個額外的臨時對象。而是在臨時分配的位置創建Base對象。

+0

#5和#6是錯誤的方式。 –

+0

@Charles Bailey:grr ..混亂的困惑,我希望我的編輯能夠做到。 –

+0

我不同意你最後一段的推理。我沒有看到任何理由爲什麼函數調用在兩種情況下都會得到不同的優化。從副本的數量來看,這兩種情況下都使用NRVO。 –

0

第一個片段構造三個對象:

  • 乙T1
  • B(5)< - 從(int)構造;這是播放功能的臨時對象
  • return b;或B(b)< - 複製ctor

這是我的猜測,雖然它看起來效率低下。

+0

但是第二個解釋是什麼? – zhanglistar

0

請參閱Als發佈的關於第一個場景的詳情。我認爲(編輯:錯誤;見下文)與第二種情況的區別在於,編譯器足夠聰明,可以使用NRVO(命名爲返回值優化)並取消中間副本:而不是創建臨時副本返回(來自播放),編譯器使用播放函數內的實際「b」作爲t1的拷貝構造函數的右值。

戴夫亞伯拉罕有一個article複製elision,這裏的維基百科return value optimization

編輯:其實,ALS增加了播放的播放第二個方案中,太多。 :)

進一步編輯:其實,我是不正確的上面。 NRVO在這兩種情況下都沒有被使用,因爲標準根據接受的答案for this question直接從函數參數(b在播放)到函數的返回值位置(至少沒有內聯)中直接刪除副本。

即使允許NRVO,我們也可以知道它至少在第一種情況下沒有被使用:如果是,則第一種情況不會涉及任何拷貝構造函數。第一種情況下的複製構造函數來自隱藏的副本,從命名值b(在播放函數中)到用於播放的隱藏返回值位置。第一種情況不涉及明確的複製結構,所以這是唯一可以出現的地方。

實際發生的情況是:無論是哪種情況都沒有發生NRVO,並且在返回時創建了一個隱藏副本......但在第二種情況下,編譯器能夠直接在t1處構造隱藏返回副本位置。所以,從b到返回值的拷貝沒有被消除,但是從t1返回值的副本是。但是,編譯器在t1已經構建的第一種情況下做了這種優化的時間更加困難(閱讀:它沒有這樣做))。如果t1已經在與返回值的位置不兼容的地址構造,那麼編譯器不能直接將t1的地址用於隱藏的返回值副本。

1

首先,檢查子表達式play(5)。這兩種情況下的表達方式都是相同的。

在一個函數調用表達式中的每個參數是從它的參數(ISO/IEC 14882:2003 5.2.2/4)複製初始化。在這種情況下,這涉及將5轉換爲B,方法是使用非顯式構造函數,使用int創建臨時B,然後使用複製構造函數初始化參數b。但是,通過使用12.8中規定的規則下的轉換構造函數int直接初始化b,可以實現刪除臨時實現。

類型的play(5)B以及 - 作爲函數返回一個非參考 - 它是一個右值

return語句隱式地將返回表達式轉換爲返回值類型(6.6.3),然後使用轉換後的表達式對返回對象進行復制初始化(8.5/12)。

在這種情況下,返回表達式已經是正確的類型,所以不需要轉換,但仍需要複製初始化。


除了在返回值優化

命名返回值優化(NRVO)是指情況return語句是,如果形式return x;其中x是一個自動的對象本地的功能。發生時,允許實現在返回值的位置構造x,並消除return點處的複製初始化。

儘管標準中沒有這樣命名,但NRVO通常是指12.8/15中描述的第一種情況。

play這個特殊的優化是不可能的,因爲b不是函數體的本地對象,它是在函數輸入時已經構造的參數的名稱。

(未命名的)返回值優化(RVO)在引用的內容上甚至不太一致,但通常用於引用返回表達式不是命名對象而是轉換爲返回值的表達式的情況可以合併返回對象的複製初始化,以便從轉換結果中直接初始化返回對象,從而消除一個臨時對象。

的RVO不play適用,因爲b已經B型的,因此副本初始化相當於直接初始化並沒有臨時對象是必要的。


在這兩種情況下play(5)需要B(int)一個B使用參數和B副本初始化到返回對象的構造。它也可能在參數初始化時使用第二個副本,但即使未明確請求優化,許多編譯器也會消除此副本。這兩個(或全部)對象都是臨時對象。

在表達式語句t1 = play(5);拷貝賦值運算符將被稱爲複製的play返回值t1和兩個臨時變量的值(參數和play返回值)將被銷燬。自然t1必須在此聲明之前構建,其析構函數將在其生命週期結束時調用。

在聲明語句B t1 = play(5);,邏輯t1被初始化的發揮返回值和相同數量的臨時將被作爲表達式語句t1 = play(5);。但是,這是12.8/15中所涉及的第二種情況,允許實現消除用於返回值play的臨時值,並允許返回對象的別名爲t1play函數的運行方式完全相同,但因爲返回對象只是t1的別名,所以它的return語句有效地直接初始化t1,並且沒有單獨的臨時對象用於需要銷燬的返回值。

0

在你的第一個例子,你打電話三個構造函數:

  • 在聲明B t1;B()構造,這也是一個定義,如果B()是公開的。換句話說,編譯器會嘗試將任何已聲明的對象初始化爲一些基本的有效狀態,並將B()作爲將B大小的內存塊轉換爲所述基本有效狀態的方法,以便在t1上調用的方法不會中斷該程序。

  • B(int)構造函數,用作隱式轉換; play()需要一個B,但給了一個int,但B(int)被認爲是一種將int轉換爲B的方法。

  • B(const B& rhs)拷貝構造函數,這將複製由play()返回到臨時值,使其有足夠的範圍長的生存B的值賦值運算符使用。

當範圍退出時,上述每個構造函數都必須與析構函數匹配。

在你的第二個例子,但是,你明確初始化的t1play()結果值的,所以編譯器不需要浪費週期提供了基本的狀態t1其指定的play()的一個副本之前導致新變量。所以,你只能叫

  • B(int)得到一個有用的論據play(B)

  • B(const B& rhs),使t1將被初始化(無論你的拷貝構造決定的)的play()的結果正確的副本。

你看不到三分之一的構造在這種情況下,因爲編譯器「eliding」的play()返回值到t1;也就是說,它知道t1play()返回之前不存在有效狀態,所以它只是將返回值直接寫入爲t1預留的內存中。

相關問題