簡體   English   中英

我如何理解這些析構函數?

[英]How can I understand these destructors?

我對以下C ++代碼感到困惑(可在http://cpp.sh/8bmp在線運行)。 它結合了我在課程中正在學習的幾個概念。

#include <iostream>
using namespace std;

class A {
    public:
        A() {cout << "A ctor" << endl;}
        virtual ~A() {cout << "A dtor" << endl;}
};

class B: public A {
    public:
        B() {cout << "B ctor" << endl;}
        ~B() {cout << "B dtor" << endl;}
        void foo(){cout << "foo" << endl;}
};

int main(){
    B *b = new B[1];
    b->~B();
    b->foo();
    delete b;
    return 0;
}

輸出:

A ctor
B ctor
B dtor
A dtor
foo
A dtor

這是我不明白的:

  1. 為什么在調用析構函數之后可以調用foo
  2. 為什么在調用析構函數后可以調用delete
  3. 如果我注釋掉, delete b; 此代碼會泄漏內存嗎?
  4. A的析構函數是虛擬的。 我認為不會調用子類中重載的虛函數。 為什么〜A ~A()被調用?
  5. 如果我注釋掉b->~B(); 然后在foo之后打印B dtor行。 為什么?
  6. 如果我重復行b->~B(); 兩次,則輸出為: B dtor\\nA dtor\\nA dtor ??
  7. 如果切換delete B;我將得到相同的輸出delete B; delete[] b; 我認為第二個是正確的,因為b是用new[]創建的,但這沒關系,因為我只是將B一個實例推入堆中。 那是對的嗎?

很抱歉問了這么多問題,但這令我感到困惑。 如果我的個人問題被誤導了,請告訴我了解每個析構函數何時運行所需的知識。

“未定義的行為”(簡稱UB)是允許編譯器執行任何操作的位置-這通常意味着介於“崩潰”,“給出錯誤的結果”和“仍然執行您期望的操作”之間。 您的b->foo()絕對是未定義的,因為它發生在您的b->~B()調用之后,

由於您的foo函數實際上並沒有使用析構函數銷毀的任何東西,因此對foo的調用“有效”,因為沒有使用任何已銷毀的東西。 [這是絕對不能保證的-它只是工作而已,有點像有時候跨過馬路不看就好,有時則不然。 視其路況而定,這可能不是一個好主意,或者在大多數情況下可能會奏效-但是有一個原因,人們說“向左看,向右看,向左看,然后在安全的情況下越過”(或類似的意思)那)]

在已被破壞的對象上調用delete也是UB,所以再次幸運的是,它“有效”(就“不會導致程序崩潰”的意義而言)。

同樣,將deletenew []混合使用,反之亦然是UB-再次,編譯器[及其相關的運行時]可能根據情況和條件做對還是錯事。

不要依賴程序中未定義的行為[1]。 它一定會回來咬你。 C和C ++有很多UB案例,因此最好至少了解一些最常見的案例,例如“銷毀后使用”,“銷毀后使用”等,並留意此類情況-並避免不惜一切代價!

  1. 為什么在調用析構函數之后可以調用foo

C ++並不會阻止您腳下射擊。 僅僅因為您可以做到(並且代碼不會立即崩潰),並不意味着它是合法的或定義明確的。

  1. 為什么在調用析構函數后可以調用delete

與答案#1相同。

  1. 如果我注釋掉, delete b; 此代碼會泄漏內存嗎?

是。 必須 delete您的new (並delete[]您的new[] )。

  1. A的析構函數是虛擬的。 我認為不會調用子類中重載的虛函數。 為什么〜A()被調用?

我認為您想要的詞是優先 ,而不是過載 無論如何,您不會覆蓋〜A ~A() 注意~B()~A()具有不同的名稱。

析構函數有點特殊。 當派生類的析構函數完成運行時,它將隱式調用基類的析構函數。 為什么? 因為C ++標准說的就是那樣。

虛擬析構函數是特殊的析構函數。 我讓您多態刪除一個對象。 這意味着您可以執行以下代碼:

B *b = new B;
A *a = b;
delete a; // Legal with virtual destructors, illegal without virtual.

如果A在上面的代碼中沒有虛擬析構函數,則不會調用~B() ,這將是未定義的行為。 使用虛擬析構函數,編譯器將在delete a;時正確調用~B() delete a; 運行,即使aA*而不是B*

  1. 如果我注釋掉b->~B(); 然后在foo之后打印B dtor行。 為什么?

因為它在foo()之后運行。 delete b; foo()已經運行之后,隱式調用b的析構函數。

  1. 如果我重復行b->~B(); 兩次,則輸出為: B dtor\\nA dtor\\nA dtor ??

這是未定義的行為。 所以任何事情都可能發生。 是的,那是奇怪的輸出。 未定義的行為很奇怪。

  1. 如果切換delete B;我將得到相同的輸出delete B; delete[] b; 我認為第二個是正確的,因為b是用new[]創建的,但這沒關系,因為我只是將B一個實例推入堆中。 那是對的嗎?

您所謂的事情很重要。 deletedelete[]是不同的東西。 您不能用一個代替另一個。 必須僅在分配有new內存上調用delete ,並在分配有new delete[]的內存上調用delete new[] 不能隨意混合和匹配。 這樣做是未定義的行為。

您應該在此代碼中使用delete[] ,因為您使用了new[]

Q1:在被銷毀的對象上調用方法是“未定義的行為”。 這意味着該標准未指定應發生的情況。 UB背后的想法是,它們應該是應用程序邏輯中的錯誤,但是出於性能原因,我們不強迫編譯器對此做一些特殊的事情,因為如果正確執行操作,這會降低性能。

在這種情況下,由於foo()方法不依賴於b指向的內存中的任何內容,因此它將按預期工作。 只是因為編譯器沒有對此進行任何測試。

Q2:這也是“未定義的行為”。 您已經看到一些奇怪的事情正在發生。 首先,不調用B析構函數,僅調用A析構函數。 發生的是,當您先前調用b->~B() ,調用了B析構函數,然后將對象的vtable更改為A vtable(這意味着對象運行時類型變為A),然后將A析構函數更改為叫。 當您調用delete b ,運行時將調用對象的虛擬析構函數A。如前所述,這是“未定義的行為”。 編譯器選擇生成在調用delete b時以這種方式工作的代碼,但是它可能生成了不同的代碼,但仍然正確。

如果確實如此,則由於代碼中的另一個錯誤,在調用A析構函數后可能已經做得更糟:使用delete而不是delete[] C ++規則規定必須使用operator delete[]釋放使用operator delete[] operator new[]分配的數組,而使用operator delete則是“未定義的行為”。 實際上,我知道在大多數實現中,這樣做都是第一次,但是很有可能破壞內存管理數據,因此將來對newdelete甚至有效的調用都可能崩潰或導致內存泄漏。

Q3:如果您刪除該呼叫,將發生內存泄漏。 如果您繼續通話,則可能會導致內存損壞。 如果使用delete[] b可以避免內存泄漏。 仍然存在未定義的行為,因為將對已銷毀的對象調用析構函數,但是由於這些析構函數不執行任何操作,因此它們可能不會對您的程序造成更多損害

問題4:這是所有析構函數的規則,而不僅僅是虛擬析構函數:它們將破壞成員對象,然后破壞代碼末尾的基礎對象。

Q5:因此,當編譯器為B析構函數生成代碼時,它將在最后添加對A析構函數的調用。 但是有一條規則:目前, this不再是B對象,並且在對A析構函數中的任何虛擬方法的調用期間,即使是虛擬的,也必須調用A方法,而不是B方法。 因此,在調用A析構函數之前,B析構函數會將對象的動態類型從B“降級”到A(實際上,這意味着將對象的vtable設置為A vtable)。 由於編譯器的目標是生成有效的代碼,因此不必在A析構函數的末尾更改vtable。 請記住:在調用析構函數之后,對象上的任何方法調用都是“未定義的行為”。 優先是性能,而不是錯誤檢測。

問題6:與問題5的答案相同,我也談到了問題2

問題7:重要。 很多。 delete []需要知道創建的對象數量,以便可以為數組中的所有對象調用銷毀器。 通常,new []的實現實際上會分配一個size_t元素,以將數組中對象的數量存儲在數組元素之前。 因此,返回的指針不是分配的塊的開始,而是大小之后的位置(在32位系統上為4個字節,在64位系統上為8個字節)。 因此,第一個new B[1]將比new B多分配4或8個字節,而第二個delete []將需要在分配它之前將指針遞減4或8個字節。 因此, delete bdelete[] b非常不同。

注意:編譯器沒有強制以這種方式實現new[]delete[] 並且某些實現提供了運行時庫的版本,該庫可以執行更多檢查,以便更容易檢測到錯誤。 但是,為了獲得最佳性能,如果使用new[]則必須調用delete[] 而且,如果您在大多數情況下在對new[]分配的指針上調用delete的操作出錯,則第一次執行該操作不會崩潰或失敗。 稍后可能會在完全合法的newnew[]deletedelete[]操作中崩潰或失敗。 這通常意味着很多人撓頭想知道為什么完全正確的操作會失敗。 這是“未定義行為”的真實含義

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM