從語言的角度來看,唯一重要的是它是未定義的行爲。什麼都可以。但這並不意味着你無法解釋你所得到的行爲。
要注意的第一件事情是最關鍵的區別不在於你提供的用戶析構函數,但該析構函數(隱含定義或用戶定義的)是不平凡。基本上,如果析構函數什麼都不做,編譯器就不會認爲用戶提供的析構函數是微不足道的。在你沒有提供用戶提供的析構函數和析構函數的情況下,可能仍然是不平凡的,包括子對象(成員或基礎)具有非平凡析構函數的任何情況。
struct NonTrivialDestructor {
std::string s;
};
struct NotTrivialEither : NonTrivialDestructor {};
他這樣說
現在,在兩種情況下,您所遇到的碰撞,而不是主要的區別是,在後一種情況下,編譯器知道構函數不會做任何事情,因此它知道如果他們被叫或不會有所作爲。
這句話的重要性,在情況下析構函數做(或可以做)的東西,編譯器必須確保在需要時生成的代碼調用盡可能多的析構函數。由於由new[]
返回的指針不包含有多少對象被分配(因此需要被銷燬)的信息,所以需要在其他地方跟蹤該信息。最常見的方法是編譯器將生成分配一個附加對象的代碼,它將設置值爲對象數量並分配數組的其餘部分,構造所有對象等。內存佈局爲:
v
+------+---+---+---+--
| size | T | T | T |...
+------+---+---+---+--
^
通過new[]
返回的指針需要保持第一對象的,這意味着即使底層分配器(operator new
)返回指針v
,new[]
將上述附圖中返回^
地址。
的附加信息可以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/delete
和new[]/delete[]
。
因爲未定義的行爲。未定義的行爲是核心,未定義,可能意味着任何事情都可能發生。 –
@Joachim:雖然這是真的,但機器中發生的所有事情都有其原因,即使它超出了語言的範圍。 UB不會奇蹟般地關閉物理定律。 –
我對這個原因感興趣:兩個編譯器都這麼做。 – Liviu