釋放對象數組:delete與delete[]

<<c++ primer>>練習   14.11   中提到:
Account   *parray=new   Account[100];
delete   parray;
delete   []   parray;
方括號的存在會使編譯器獲取數組大小(size)然後析構函數再被依次應用在每個元素上,一共size次。否則,只有一個元素被析構。
無論哪種情況,分配的全部空間被返還給自由存儲區。

我的問題是:爲什麼無論哪種情況,分配的全部空間被返還給自由存儲區?
對於delete parray,爲什麼不是刪除單個Account元素,而是刪除了100個.
編譯器怎麼知道parray這個指針實際指向的是數組還是單個元素,即便知道指向的是自由存儲區的數組,這個數組的大小又怎麼知道。
難道是編譯器輔助行爲?

總結:空間釋放(肯定有個log記錄分配的大小)和調用析構函數(類型識別,不同的編譯器實現不同)採用不同的機制.

(1)一般在分配時分配器會自動寫一個日誌(一般在分配使用得內存之前又一個結構),用於記錄分配的大小,分配內容的sizeof等等。
直觀得想想,delete和delete[]都是傳入一個void*如果不保存日誌就無法知道分配時到底是分配了一個還是多個單元.
所以雖然delete和delete[]不同但是分配器在執行釋放過程中都會讀取這個日誌,從而瞭解到底應該釋放多少內存,但是從程序員的角度來說,既然分配了數組,就應該用delete[]

(2)在VC下用匯編跟過delete[]的執行情況,發現這個 "日誌 "就是一個4字節長的整數記錄數組元素個數,緊挨在數組第一個元素之前.  
但是有個前提:對象類型(或其基類)有顯式析構函數.換句話說,析構函數是非trivial的.  
否則的話,數組前面是沒有這個日誌的.其實對於析構函數是trivial的情況,delete[]時無需調用其析構函數,因此此時VC把delete[]當做delete同樣處理.

(3)因爲釋放數組空間和爲數組調用析構函數是兩個獨立的部分,可以使用不同的機制來實現。
釋放空間的機制是需要絕對保證的。因此,即使你不寫delete[],它也會將所有空間釋放,其機制可以是前置的長度信息,也可以不是(如後置的特徵分割符等等)。
而調用析構函數可以一般採用前置長度信息的方式(當然也可以有其他方式)。在沒有[]提示時,編譯器在調用析構就將它當一個元素,而不會使用數組方式來調用每一個析構函數了。
LS:“但是有個前提:對象類型(或其基類)有顯式析構函數.換句話說,析構函數是非trivial的. 否則的話,數組前面是沒有這個日誌的.”
——這說明,LS使用的編譯器在釋放數組空間時,並沒有用前置的長度信息的方式。由此可見,釋放數組空間和爲數組調用析構函數確實可以使用不同的機制

(4)delete parray,編譯器得到類型信息是Account單個的指針,那麼釋放時,只調用一次析構函數。
delete[] parray,編譯器得到的類型信息是Account[]類型,則按照Account數組來處理,依次調用每個元素的析構函數。

注意,以上是在編譯期間就確定下來的,編譯器識別到類型信息的不同會決定調用析構函數的情況有不同。

但是對於內存釋放,delete操作則不是通過類型信息來確定分配的內存大小,那麼內存大小的信息從什麼地方得到呢?
當我們使用operator new爲一個自定義類型對象分配內存時,實際上我們得到的內存要比實際對象的內存大一些,這些內存除了要存儲對象數據外,還需要記錄這片內存的大小,此方 法稱爲   cookie。這一點上的實現依據不同的編譯器不同。(例如   MFC   選擇在所分配內存的頭部存儲對象實際數據,而後面的部分存儲邊界標誌和內存大小信息。g++   則採用在所分配內存的頭4個自己存儲相關信息,而後面的內存存儲對象實際數據。)當我們使用   delete   operator   進行內存釋放操作時,delete   operator   就可以根據這些信息正確的釋放指針所指向的內存塊。
對於parray指針,可以根據這樣的cookie信息來得到指向內存空間的大小,delete parray和delete[] parray都是一樣的,同樣一個指針,cookie信息是相同的,所以對應的內存都會被釋放掉。但是由於編譯器理解兩種情況下的類型是不同的,所以調用 析構函數會有不同。

(5)難道是編譯器輔助行爲?
沒錯,就是。不同的編譯器可能採用的具體方法有可能不一樣,但不管採用什麼方法,編譯器必須記住那塊大小。

(6)轉自<<effective c++>>
條款5:對應的new和delete要採用相同的形式

下面的語句有什麼錯?
string *stringarray = new string[100];
delete stringarray;
一切好象都井然有序——一個new對應着一個delete——然而卻隱藏着很大的錯誤:程序的運行情況將是不可預測的。至少,stringarray指向的100個string對象中的99個不會被正確地摧毀,因爲他們的析構函數永遠不會被調用。
用new的時候會發生兩件事。首先,內存被分配(通過operator new 函數,詳見條款7-10和條款m8),然後,爲被分配的內存調用一個或多個構造函數。用delete的時候,也有兩件事發生:首先,爲將被釋放的內存調用 一個或多個析構函數,然後,釋放內存(通過operator delete 函數,詳見條款8和m8)。對於 delete來說會有這樣一個重要的問題:內存中有多少個對象要被刪除?答案決定了將有多少個析構函數會被調用。
這個問題簡單來說就是:要被刪除的指針指向的是單個對象呢,還是對象數組?這隻有你來告訴delete。如果你在用delete時沒用括號,delete就會認爲指向的是單個對象,否則,它就會認爲指向的是一個數組:
string *stringptr1 = new string;
string *stringptr2 = new string[100];
...
delete stringptr1;// 刪除一個對象
delete [] stringptr2;// 刪除對象數組
如果你在stringptr1前加了"[]"會怎樣呢?答案是:那將是不可預測的;
如果你沒在stringptr2前沒加上"[]"又會怎樣呢?答案也是:不可預測。
int這樣的固定類型來說,結果也是不可預測的,即使這樣的類型沒有析構函數。所以,解決這類問題的規則很簡單:如果你調用new時用了[],調用delete時也要用[]。如果調用new時沒有用[],那調用delete時也不要用[]。

Cpp代碼
  1. #include <iostream>  
  2.   
  3. using namespace std;  
  4.   
  5. struct foo  
  6. {  
  7.     ~foo(){}; //去掉後,就不會記錄個數了.  
  8. };  
  9.   
  10.   
  11. int main(int argc,char *argv[])  
  12. {  
  13.   
  14.      foo* f = new foo;  
  15.      delete f;  
  16.      f=0;  
  17.      delete f;  
  18.   
  19.      foo* fa = new foo[8];  
  20.      printf("%u\n", *((char *)fa - 4)); //輸出8:輔助析構函數.  
  21.      /* 
  22.      對於有顯式析構函數的對象的數組, 
  23.      編譯器會在數組前分配4個字節儲存數組元素的個數 
  24.      (也就是需要調用析構函數的次數), 
  25.      因爲必須知道數組實際的元素合個數, 
  26.      delete[]才能知道需要調用幾次析構函數。 
  27.      */  
  28.      delete fa; //有的編譯器這裏會有異常.  
  29.      //用delete而不是delete[]釋放用new[]分配的空間這種行爲是undefined的,  
  30.      //也就是由編譯器實現所決定的  
  31.      fa = 0;  
  32.      delete fa;  
  33.   
  34.      return 0;  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章