程序員們經常編寫內存管理程序,往往提心吊膽。如果不想觸雷,唯一的解決辦法就是發現所有潛伏的地雷並且排除它們,躲是躲不了的。本節的內容比一般教科書的要深入得多,讀者需細心閱讀,做到真正地通曉內存管理。
一、內存分配方式
內存分配方式有三種:
(1) 從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量,static 變量。
(2) 在棧上創建。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。
(3) 從堆上分配,亦稱動態內存分配。程序在運行的時候用 malloc 或 new 申請任意多少的內存,程序員自己負責在何時用 free 或 delete 釋放內存。動態內存的生存期由我們決定,使用非常靈活,但問題也最多。
二、常見的內存錯誤及其對策
發生內存錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程序運行時才能捕捉到。而這些錯誤大多沒有明顯的症狀,時隱時現,增加了改錯的難度。有時用戶怒氣沖沖地把你找來,程序卻沒有發生任何問題,你一走,錯誤又發作了。
常見的內存錯誤及其對策如下:
(1)內存分配未成功,卻使用了它。
編程新手常犯這種錯誤,因為他們沒有意識到內存分配會不成功。常用解決辦法是,在使用內存之前檢查指針是否為 NULL。如果指針 p 是函數的參數,那么在函數的入口處用 assert(p!=NULL)
進行檢查。如果是用 malloc 或 new 來申請內存,應該用 if(p==NULL)
或 if(p!=NULL)
進行防錯處理。
(2)內存分配雖然成功,但是尚未初始化就引用它。
犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為內存的缺省初值全為零,導致引用初值錯誤(例如數組)。
內存的缺省初值究竟是什么並沒有統一的標准,盡管有些時候為零值,我們寧可信其無不可信其有。所以無論用何種方式創建數組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。
(3)內存分配成功並且已經初始化,但操作越過了內存的邊界。
例如在使用數組時經常發生下標 “多 1” 或者 “少 1” 的操作。特別是在 for 循環語句中,循環次數很容易搞錯,導致數組操作越界。所以要避免數組或指針的下標越界,特別要當心發生 “多 1” 或者 “少 1” 操作。
(4)忘記了釋放內存,造成內存泄露。
含有這種錯誤的函數每被調用一次就丟失一塊內存。剛開始時系統的內存充足,你看不到錯誤。終有一次程序突然死掉,系統出現提示:內存耗盡。
動態內存的申請與釋放必須配對,程序中 malloc 與 free 的使用次數一定要相同,否則肯定有錯誤(new/delete 同理)。
(5)釋放了內存卻繼續使用它。
有三種情況:
(1)函數的 return 語句寫錯了,注意不要返回指向 “棧內存” 的 “指針” 或者 “引用”,因為該內存在函數體結束時被自動銷毀。
(2)使用 free 或 delete 釋放了內存后,沒有將指針設置為 NULL。導致產生 “野指針”。
(3)程序中的對象調用關系過於復雜,實在難以搞清楚某個對象究竟是否已經釋放了內存,此時應該重新設計數據結構,從根本上解決對象管理的混亂局面。
三、free 和 delete 把指針怎么啦?
別看 free 和 delete 的名字惡狠狠的(尤其是 delete),它們只是把指針所指的內存給釋放掉,但並沒有把指針本身干掉。
用調試器跟蹤下例,發現指針 p 被 free 以后其地址仍然不變(非 NULL),只是該地址對應的內存是垃圾,p 成了“野指針”。如果此時不把 p 設置為 NULL,會讓人誤以為 p 是個合法的指針。
如果程序比較長,我們有時記不住 p 所指的內存是否已經被釋放,在繼續使用 p 之前,通常會用語句 if (p != NULL)進行防錯處理。很遺憾,此時 if 語句起不到防錯作用,因為即便 p 不是 NULL 指針,它也不指向合法的內存塊。
char *p = (char *) malloc(100);
strcpy(p, “hello”);
free(p); // p 所指的內存被釋放,但是 p 所指的地址仍然不變
…
if(p != NULL) // 沒有起到防錯作用
{
strcpy(p, “world”); // 出錯,p 成為野指針
}
四、動態內存會被自動釋放嗎?
函數體內的局部變量在函數結束時自動消亡。很多人誤以為下例是正確的。理由是 p 是局部的指針變量,它消亡的時候會讓它所指的動態內存一起完蛋。這是錯覺!
void Func(void)
{
char *p = (char *) malloc(100); // 動態內存會自動釋放嗎?
}
我們發現指針有一些“似是而非”的特征:
(1)指針消亡了,並不表示它所指的內存會被自動釋放。
(2)內存被釋放了,並不表示指針會消亡或者成了 NULL 指針。
這表明釋放內存並不是一件可以草率對待的事。也許有人不服氣,一定要找出可以草率行事的理由:
如果程序終止了運行,一切指針都會消亡,動態內存會被操作系統回收。既然如此,在程序臨終前,就可以不必釋放內存、不必將指針設置為 NULL 了。終於可以偷懶而不會發生錯誤了吧?
想得美。如果別人把那段程序取出來用到其它地方怎么辦?
五、有了 malloc/free 為什么還要 new/delete ?
malloc 與 free 是 C++/C 語言的標准庫函數,new/delete 是 C++ 的運算符。它們都可用於申請動態內存和釋放內存。
對於非內部數據類型的對象而言,光用 maloc/free 無法滿足動態對象的要求。對象在創建的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。由於 malloc/free 是庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加於 malloc/free。
因此 C++語言需要一個能完成動態內存分配和初始化工作的運算符 new,以及一個能完成清理與釋放內存工作的運算符 delete。注意 new/delete不是庫函數。
我們先看一看 malloc/free 和 new/delete 如何實現對象的動態內存管理。示例如下:
class Obj
{
public :
Obj(void){ cout << “initialization” << endl; }
~Obj(void){ cout << “destroy” << endl; }
void initialize(void){ cout << “initialization” << endl; }
void destroy(void){ cout << “destroy” << endl; }
};
void useMallocFree(void)
{
Obj *a = (obj *)malloc(sizeof(obj)); // 申請動態內存
a->initialize(); // 初始化
//…
a->destroy(); // 清除工作
free(a); // 釋放內存
}
void useNewDelete(void)
{
Obj *a = new Obj; // 申請動態內存並且初始化
//…
delete a; // 清除並且釋放內存
}
Obj 類的 initialize 函數模擬了構造函數的功能,destroy 函數模擬了析構函數的功能。useMallocFree 函數中,由於 malloc/free 不能執行構造函數與析構函數,必須調用 initialize 和 destroy 成員函數來完成初始化與清除工作。
useNewDelete 函數則簡單得多。所以我們不要企圖用 malloc/free 來完成動態對象的內存管理,應該用 new/delete。由於內部數據類型的 “對象” 沒有構造與析構的過程,對它們而言 malloc/free 和 new/delete 是等價的。
既然 new/delete 的功能完全覆蓋了 malloc/free,為什么 C++不把 malloc/free 淘汰出局呢?
這是因為 C++程序經常要調用 C 函數,而 C 程序只能用 malloc/free 管理動態內存。如果用 free 釋放 “new 創建的動態對象”,那么該對象因無法執行析構函數而可能導致程序出錯。如果用 delete 釋放 “malloc 申請的動態內存”,理論上講程序不會出錯,但是該程序的可讀性很差。所以 new/delete 必須配對使用,malloc/free 也一樣。
六、內存耗盡怎么辦?
如果在申請動態內存時找不到足夠大的內存塊,malloc 和 new 將返回 NULL 指針,宣告內存申請失敗。通常有三種方式處理“內存耗盡”問題。
(1)判斷指針是否為 NULL,如果是則馬上用 return 語句終止本函數。例如:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
return;
}
…
}
(2)判斷指針是否為 NULL,如果是則馬上用 exit(1)終止整個程序的運行。 例如:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
cout << “Memory Exhausted” << endl;
exit(1);
}
…
}
(3) 為 new 和 malloc 設置異常處理函數。例如 Visual C++可以用 _set_new_hander
函數為 new 設置用戶自己定義的異常處理函數,也可以讓 malloc 享用與 new 相同的異常處理函數。詳細內容請參考 C++使用手冊。
上述(1)(2)方式使用最普遍。如果一個函數內有多處需要申請動態內存,那么方式(1)就顯得力不從心(釋放內存很麻煩),應該用方式(2)來處理。
有一個很重要的現象要告訴大家。對於 32 位以上的應用程序而言,無論怎樣使用 malloc 與 new,幾乎不可能導致“內存耗盡”。因為 32 和 64 位操作系統支持“虛存”,內存用完了,會自動用硬盤空間頂替。
可以得出這么一個結論:對於 32 位以上的應用程序,“內存耗盡”錯誤處理程序毫無用處。這下可把 Unix 和 Windows 程序員們樂壞了:反正錯誤處理程序不起作用,我就不寫了,省了很多麻煩。但是必須強調:不加錯誤處理將導致程序的質量很差,千萬不可因小失大。
試圖耗盡操作系統內存的示例程序如下:
void main(void)
{
float *p = NULL;
while(TRUE)
{
p = new float[1000000];
cout << “eat memory” << endl;
if(p==NULL)
exit(1);
}
}
七、malloc/free 的使用要點
函數 malloc 的原型如下:
void *malloc(size_t size);
用 malloc 申請一塊長度為 length 的整數類型的內存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);
我們應當把注意力集中在兩個要素上:“類型轉換” 和 “sizeof”。
- malloc 返回值的類型是 void *,所以在調用 malloc 時要顯式地進行類型轉換,將 void * 轉換成所需要的指針類型。
- malloc 函數本身並不識別要申請的內存是什么類型,它只關心內存的總字節數。所以在 malloc 的 “()” 中使用 sizeof 運算符是良好的風格,但要當心有時我們會昏了頭,寫出 p = malloc(sizeof(p))這樣的程序來。
函數 free 的原型如下:
void free(void *memblock);
為什么 free 函數不象 malloc 函數那樣復雜呢?這是因為指針 p 的類型以及它所指的內存的容量事先都是知道的,語句 free(p)能正確地釋放內存。
如果 p 是 NULL 指針,那么 free 對 p 無論操作多少次都不會出問題。如果 p 不是 NULL 指針,那么 free 對 p 連續操作兩次就會導致程序運行錯誤。
八、new/delete 的使用要點
運算符 new 使用起來要比函數 malloc 簡單得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
這是因為 new 內置了 sizeof、類型轉換和類型安全檢查功能。對於非內部數據類型的對象而言,new 在創建動態對象的同時完成了初始化工作。如果對象有多個構造函數,那么 new 的語句也可以有多種形式。例如:
class Obj
{
public :
Obj(void); // 無參數的構造函數
Obj(int x); // 帶一個參數的構造函數
…
}
void Test(void)
{
Obj *a = new Obj;
Obj *b = new Obj(1); // 初值為 1
… delete a;
delete b;
}
如果用 new 創建對象數組,那么只能使用對象的無參數構造函數。例如:
Obj *objects = new Obj[100]; // 創建 100 個動態對象
不能寫成:
Obj *objects = new Obj[100](1);// 創建 100 個動態對象的同時賦初值 1
在用 delete 釋放對象數組時,留意不要丟了符號‘[]’。例如:
delete []objects; // 正確的用法
delete objects; // 錯誤的用法
后者相當於 delete objects[0]
,漏掉了另外 99 個對象。
參考:
《高質量C++C 編程指南 林銳》的第7章 內存管理