2013-12-18 19 views
0

刪除指向分配爲new []的數組的指針是「未定義的行爲」。我只是好奇,爲什麼第一個delete在下面的代碼導致堆腐敗如果析構函數被定義,否則沒有任何反應(正如我虛心預計)。使用Visual Studio 8和GNU GCC版本4.7.2(http://www.compileonline.com/compile_cpp11_online.php)進行測試。在新[]後刪除:有或沒有用戶析構函數

struct A 
{ 
    //~A() {} 
}; 

int main() 
{ 
    A* a = new A[10]; 
    delete a; // heap corruption when the user destructor is defined 

    int *b = new int[100]; 
    delete b; // no heap corruption 
}; 
+6

因爲未定義的行爲。未定義的行爲是核心,未定義,可能意味着任何事情都可能發生。 –

+3

@Joachim:雖然這是真的,但機器中發生的所有事情都有其原因,即使它超出了語言的範圍。 UB不會奇蹟般地關閉物理定律。 –

+0

我對這個原因感興趣:兩個編譯器都這麼做。 – Liviu

回答

6

UB是不是你可以在語言的範圍內理順一下,至少在沒有工具鏈的詳細知識,優化級別,操作系統,體系結構和處理器。

儘管如此:沒有析構函數可以調用,編譯器根本不會生成任何代碼,使得代碼的錯誤在現實世界中可見。這樣的析構函數調用可能會觸及動態分配的內存塊的頭部,這在單個new A示例中不存在。

+0

確實,添加一個'std :: string'成員迫使編譯器生成一個析構函數並重新出現堆損壞。 – Liviu

+0

對不起,-1代表「UB不是你可以合理化的東西」。既不真實也不有助於這個問題。相反,我同意你對你所說的任何確定性機器可以合理化的問題的評論。所有的UB意味着你不能單獨使用C++標準來對其進行合理化。 –

+0

@SteveJessop:現在好點? –

5

不匹配newnew[]deletedelete[]不確定的行爲。真的,這是討論的結束。

之後會發生任何事情。進一步分析是毫無意義的。

3

原因很可能是因爲當new[]是一個不平凡的可破壞類型的數組時,實現將數組的大小存儲在從底層分配機制獲得的塊的開始位置。它需要這個,以便知道當你返回數組的第一個元素的地址時,它將與它所分配的塊的地址不同,可能會有一定數量的字節,可能會破壞多少個對象sizeof(size_t)

作爲一種節省內存的優化,對於不需要銷燬的類型,它不會這樣做。

因此,delete[]將根據類型是否具有析構函數來做不同的事情。如果確實如此,它會查看指針前面的字節,銷燬許多對象,減去偏移量並釋放塊。如果沒有,那麼它只會釋放它給出的地址。正如它發生的那樣,「只是釋放地址」也是delete在沒有析構函數的情況下所做的,所以當你不匹配它們時,你在這個實現上「擺脫」了UB。

現在,可能會有其他機制會產生您看到的相同結果,並且原則上可以檢查此特定版本的GCC的來源以確認。但我很確定我記得這是GCC如何使用它,所以它可能仍然如此。

2

從語言的角度來看,唯一重要的是它是未定義的行爲。什麼都可以。但這並不意味着你無法解釋你所得到的行爲。

要注意的第一件事情是最關鍵的區別不在於你提供的用戶析構函數,但該析構函數(隱含定義或用戶定義的)是不平凡。基本上,如果析構函數什麼都不做,編譯器就不會認爲用戶提供的析構函數是微不足道的。在你沒有提供用戶提供的析構函數和析構函數的情況下,可能仍然是不平凡的,包括子對象(成員或基礎)具有非平凡析構函數的任何情況。

struct NonTrivialDestructor { 
    std::string s; 
}; 
struct NotTrivialEither : NonTrivialDestructor {}; 
他這樣說

現在,在兩種情況下,您所遇到的碰撞,而不是主要的區別是,在後一種情況下,編譯器知道構函數不會做任何事情,因此它知道如果他們被叫或不會有所作爲。

這句話的重要性,在情況下析構函數(或可以做)的東西,編譯器必須確保在需要時生成的代碼調用盡可能多的析構函數。由於由new[]返回的指針不包含有多少對象被分配(因此需要被銷燬)的信息,所以需要在其他地方跟蹤該信息。最常見的方法是編譯器將生成分配一個附加對象的代碼,它將設置值爲對象數量並分配數組的其餘部分,構造所有對象等。內存佈局爲:

v 
+------+---+---+---+-- 
| size | T | T | T |... 
+------+---+---+---+-- 
     ^

通過new[]返回的指針需要保持第一對象的,這意味着即使底層分配器(operator new)返回指針vnew[]將上述附圖中返回^地址。

的附加信息可以delete[]知道有多少構造函數調用,基本上做這樣的事情:

pseudo-delete[](T * p) { 
    size_t *size = reinterpret_cast<size_t*>(p)-1; 
    for (size_t i = 0; i < *size; ++i) 
     (p+i)->~T(); 
    deallocate(size); 
} 

也就是說,delete[]提取從以前的存儲位置的信息,使用了多少析構函數開車運行,然後釋放從系統分配的內存(圖中的指針v)。

另一方面,delete是爲了處理單個對象的刪除,因爲沒有額外的計數(計數必須是1),所以沒有附加的信息被跟蹤。純代碼delete的僞代碼將直接調用p->~T()deallocate(p)。這是你遇到崩潰的地方。

由底層機制分配的內存地址爲v,但您試圖釋放^,導致庫失敗並崩潰。

new[]與擁有平凡的析構函數的類型時,編譯器知道調用析構函數什麼都不會做,所以它完全跳過操作,這在僞代碼上述表示的for環路走了。這又消除了對額外size_t的需要,並且通過一個調用返回到new []存儲器具有如下的佈局:

v 
+---+---+---+-- 
| T | T | T |... 
+---+---+---+-- 
^ 

在這種情況下,從存儲器分配器獲得的指針和由new[]返回的指針是在相同的位置,這意味着當delete被評估時,用於釋放的指針與用於分配的指針相同。

需要注意的是這一切的討論是關於實現細節是很重要的,這是標準的未定義行爲有的實現可能會選擇將始終跟蹤的大小,或使用不同的機制來跟蹤元素的數量和這裏討論的一切都是假的。您不能取決於此行爲,但我希望它可以幫助人們瞭解爲什麼標準需要匹配new/deletenew[]/delete[]

相關問題