2016-08-15 79 views
1

我有一個簡單的程序是這樣的:爲什麼輸出與我所期望的不同?

#include "stdafx.h" 
#include <iostream> 

using namespace std; 

int main() 
{ 
    class B { 
    protected: 
     int  data = 0; 
    public: 
     B() { cout << "B() ctor\n";} 
     virtual ~B() { cout << "~B()\n"; } 
     virtual void method() { cout << "data in B: " << data << "\n"; } 
    }; 

    class A : public B 
    { 
     int dataA = 2; 
    public: 
     A() { cout << "A() ctor\n"; } 
     ~A() { cout << "~A()\n"; } 
     void method() { cout << "data in A: " << dataA << "\n"; } 
    }; 

    { 
     B* fptrList[]{ &B{}, &A{}}; 
     for (auto& itr : fptrList) 
      itr->method(); 
    } 

    cin.get(); 
    return 0; 
} 

這裏是一個結果,我想到:

B() ctor 
B() ctor 
A() ctor 
data in B: 0 
data in A: 2 
~A() 
~B() 
~B() 

下面是實際的結果,當我跑這個程序:

B() ctor 
~B() 
B() ctor 
A() ctor 
~A() 
~B() 
data in B: 0 
data in B: 0 

我問題是:

  • 爲什麼輸出與我所期望的不同?
  • 在調用〜A()和〜B()後,如何調用method()方法?
  • 爲什麼類B的method()被調用兩次?
+1

這只是UB,試圖解釋行爲是沒有意義的。 – songyuanyao

+0

我不這麼認爲。您看到對method()的調用獲得成功並打印出正確的數據值。如果這些對象實際上被銷燬,那麼調用訪問「data」成員的method()應​​該會引發一個運行時錯誤。 –

+1

這是UB,沒有保證。它可能會引起運行時錯誤,可能會運行良好(不幸的是)。 – songyuanyao

回答

5

該程序無法解釋,因爲它展示未定義的行爲。

翻譯:它的越野車。這是臨時對象的地址,然後在臨時對象被破壞後嘗試對它們進行解引用。

一個很好的C++編譯器,甚至會告訴你該程序被打破,將拒絕參加這場災難:

t.C: In function ‘int main()’: 
t.C:26:27: error: taking address of temporary [-fpermissive] 
     B* fptrList[]{ &B{}, &A{}}; 
         ^
t.C:26:33: error: taking address of temporary [-fpermissive] 
     B* fptrList[]{ &B{}, &A{}}; 
           ^

該程序的任何輸出是毫無意義的垃圾。

5

這是怎麼回事:

  • 初始化fptrList臨時變量AB
  • 臨時變量被摧毀的將它們的地址之後的地址,讓你的代碼是未定義行爲。
  • 正確的做法是嘗試使用運算符new和智能指針,或者在初始化程序之外創建實例。

這裏是一個可能的解決辦法:

{ 
    B b; 
    A a; 
    B* fptrList[]{ &b, &a }; 
    for (auto& itr : fptrList) 
     itr->method(); 
} 
+0

快速提問:爲什麼在他們的地址被採取後,而不是在聲明結束後,臨時銷燬? – Rakete1111

+0

@ Rakete1111由於在獲取地址後不需要臨時對象,因此編譯器可以自由決定何時調用析構函數。看起來你的編譯器決定馬上銷燬它們。 – dasblinkenlight

+0

我知道你的「修復」是正確的方法。我只是試了一下,看到它有多奇怪。 –

0

好吧,這是不確定的行爲,但問題仍然是有趣的,爲什麼它是這個不確定的行爲。

  • 爲什麼按此順序調用構造函數/析構函數?如已經建立,您創建臨時對象,它們是相繼創建/銷燬的。

  • 爲什麼我可以調用已經不存在的對象的方法?您的臨時對象位於堆棧上,因此內存將僅在main函數結束時釋放,因此您仍然可以訪問此內存,並且不會因調用其他函數而遭到破壞(例如,打印到終端)。如果您要創建new的對象,那麼要刪除它並嘗試使用它 - 機會會更高,系統已經回收了此內存,並且會出現分段錯誤。

  • 爲什麼我看到2次B的方法叫?這個很有趣。爲了調用一個對象的虛函數,編譯器委託決定哪個方法應該被調用到一個虛擬表(它的地址佔用了這個對象的前8個字節(至少對於我的編譯器和64位))。關於虛擬方法的衆所周知的細節是,在析構函數調用期間,所有虛擬方法都被調用,就好像它們不是虛擬的。但是你的代碼有什麼用處呢?您會看到它的副作用:在析構函數中通過用當前類的虛擬表覆蓋當前對象的虛擬表來確保非虛擬行爲。因此,在調用B的析構函數後,內存中將包含類B的虛擬表,您可以看到它,因爲B::method被調用了兩次。

讓我們跟蹤虛表它的價值在你的程序:

通話 A{}
  1. :起初,超B -constructor被稱爲 - 的(尚未完全完成)對象有類B的虛擬表(該地址被移入對象所佔用的前8個字節),而不是A -constructor被調用 - 現在對象具有類A的虛擬表。
  2. 調用~A():執行後,A的析構函數自動調用B的析構函數。 B的析構函數所做的第一件事是用類B的虛擬表覆蓋對象的虛擬表。
  3. 因此,在銷燬之後,內存仍然存在並且被解釋爲對象將具有虛擬表類B
  4. itr->method();找到類B的虛擬表itr指向並呼叫B::method()
相關問題