C/C++內存管理詳解

轉自:http://chenqx.github.io/2014/09/25/Cpp-Memory-Management/

  內存管理是C++最令人切齒痛恨的問題,也是C++最有爭議的問題,C++高手從中獲得了更好的性能,更大的自由,C++菜鳥的收穫則是一遍一遍的檢查代碼和對C++的痛恨,但內存管理在C++中無處不在,內存泄漏幾乎在每個C++程序中都會發生,因此要想成爲C++高手,內存管理一關是必須要過的,除非放棄C++,轉到Java或者.NET,他們的內存管理基本是自動的,當然你也放棄了自由和對內存的支配權,還放棄了C++超絕的性能。
  偉大的Bill Gates 曾經失言:

 640K ought to be enough for everybody — Bill Gates 1981

  程序員們經常編寫內存管理程序,往往提心吊膽。如果不想觸雷,唯一的解決辦法就是發現所有潛伏的地雷並且排除它們,躲是躲不了的。

內存分配方式

簡介

  在C++中,內存分成5個區,他們分別是堆、棧、自由存儲區、全局/靜態存儲區和常量存儲區。
  :在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。
  :就是那些由 new分配的內存塊,他們的釋放編譯器不去管,由我們的應用程序去控制,一般一個new就要對應一個 delete。如果程序員沒有釋放掉,那麼在程序結束後,操作系統會自動回收。
  自由存儲區:就是那些由malloc等分配的內存塊,他和堆是十分相似的,不過它是用free來結束自己的生命的。
  全局/靜態存儲區:全局變量和靜態變量被分配到同一塊內存中,在以前的C語言中,全局變量又分爲初始化的和未初始化的,在C++裏面沒有這個區分了,他們共同佔用同一塊內存區。
  常量存儲區:這是一塊比較特殊的存儲區,他們裏面存放的是常量,不允許修改。

明確區分堆與棧

  堆與棧的區分問題,似乎是一個永恆的話題,由此可見,初學者對此往往是混淆不清的,所以我決定拿他第一個開刀。
  首先,我們舉一個例子:

1
void f() { int* p=new int[5]; }

  這條短短的一句話就包含了堆與棧,看到new,我們首先就應該想到,我們分配了一塊堆內存,那麼指針p呢?他分配的是一塊棧內存,所以這句話的意思就是:在棧內存中存放了一個指向一塊堆內存的指針p。在程序會先確定在堆中分配內存的大小,然後調用operator new分配內存,然後返回這塊內存的首地址,放入棧中,他在VC6下的彙編代碼如下:

00401028 push 14h
0040102A call operator new (00401060)
0040102F add esp,4
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax

  這裏,我們爲了簡單並沒有釋放內存,那麼該怎麼去釋放呢?是delete p麼?澳,錯了,應該是delete []p,這是爲了告訴編譯器:我刪除的是一個數組,編譯器就會根據相應的Cookie信息去進行釋放內存的工作。

堆和棧究竟有什麼區別

  好了,我們回到我們的主題:堆和棧究竟有什麼區別?
  主要的區別由以下幾點:
  (1). 管理方式不同
  (2). 空間大小不同
  (3). 能否產生碎片不同
  (4). 生長方向不同
  (5). 分配方式不同
  (6). 分配效率不同
  管理方式:對於棧來講,是由編譯器自動管理,無需我們手工控制;對於堆來說,釋放工作由程序員控制,容易產生memory leak
  空間大小:一般來講在32位系統下,堆內存可以達到4G的空間,從這個角度來看堆內存幾乎是沒有什麼限制的。但是對於棧來講,一般都是有一定的空間大小的,例如,在VC6下面,默認的棧空間大小是1M(好像是,記不清楚了)。當然,我們可以修改:
  打開工程,依次操作菜單如下:Project->Setting->Link,在Category 中選中Output,然後在Reserve中設定堆棧的最大值和commit
  注意:reserve最小值爲4Byte;commit是保留在虛擬內存的頁文件裏面,它設置的較大會使棧開闢較大的值,可能增加內存的開銷和啓動時間。
  碎片問題:對於堆來講,頻繁的new/delete勢必會造成內存空間的不連續,從而造成大量的碎片,使程序效率降低。對於棧來講,則不會存在這個問題,因爲棧是先進後出的隊列,他們是如此的一一對應,以至於永遠都不可能有一個內存塊從棧中間彈出,在他彈出之前,在他上面的後進的棧內容已經被彈出,詳細的可以參考數據結構,這裏我們就不再一一討論了。
  生長方向:對於堆來講,生長方向是向上的,也就是向着內存地址增加的方向;對於棧來講,它的生長方向是向下的,是向着內存地址減小的方向增長。
  分配方式:堆都是動態分配的,沒有靜態分配的堆。棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如局部變量的分配。動態分配由alloca函數進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。
  分配效率:棧是機器系統提供的數據結構,計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆則是C/C++函數庫提供的,它的機制是很複雜的,例如爲了分配一塊內存,庫函數會按照一定的算法(具體的算法可以參考數據結構/操作系統)在堆內存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於內存碎片太多),就有可能調用系統功能去增加程序數據段的內存空間,這樣就有機會分到足夠大小的內存,然後進行返回。顯然,堆的效率比棧要低得多。
  從這裏我們可以看到,堆和棧相比,由於大量new/delete的使用,容易造成大量的內存碎片;由於沒有專門的系統支持,效率很低;由於可能引發用戶態和核心態的切換,內存的申請,代價變得更加昂貴。所以棧在程序中是應用最廣泛的,就算是函數的調用也利用棧去完成,函數調用過程中的參數,返回地址,EBP和局部變量都採用棧的方式存放。所以,我們推薦大家儘量用棧,而不是用堆。
  雖然棧有如此衆多的好處,但是由於和堆相比不是那麼靈活,有時候分配大量的內存空間,還是用堆好一些。
  無論是堆還是棧,都要防止越界現象的發生(除非你是故意使其越界),因爲越界的結果要麼是程序崩潰,要麼是摧毀程序的堆、棧結構,產生以想不到的結果,就算是在你的程序運行過程中,沒有發生上面的問題,你還是要小心,說不定什麼時候就崩掉,那時候debug可是相當困難的:)

控制C++的內存分配

  在嵌入式系統中使用C++的一個常見問題是內存分配,即對new 和 delete 操作符的失控。
  具有諷刺意味的是,問題的根源卻是C++對內存的管理非常的容易而且安全。具體地說,當一個對象被消除時,它的析構函數能夠安全的釋放所分配的內存。
  這當然是個好事情,但是這種使用的簡單性使得程序員們過度使用new 和 delete,而不注意在嵌入式C++環境中的因果關係。並且,在嵌入式系統中,由於內存的限制,頻繁的動態分配不定大小的內存會引起很大的問題以及堆破碎的風險。
  作爲忠告,保守的使用內存分配是嵌入式環境中的第一原則。
  但當你必須要使用newdelete時,你不得不控制C++中的內存分配。你需要用一個全局的new 和delete來代替系統的內存分配符,並且一個類一個類的重載newdelete
  一個防止堆破碎的通用方法是從不同固定大小的內存持中分配不同類型的對象。對每個類重載new 和delete就提供了這樣的控制。

重載全局的new和delete操作符

  可以很容易地重載new 和 delete 操作符,如下所示:

1
2
3
4
5
6
7
8
void * operator new(size_t size){
void *p = malloc(size);
return (p);
}
void operator delete(void *p){
free(p);
}

  這段代碼可以代替默認的操作符來滿足內存分配的請求。出於解釋C++的目的,我們也可以直接調用malloc() 和free()
  也可以對單個類的new 和 delete操作符重載。這是你能靈活的控制對象的內存分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class TestClass {
public:
void * operator new(size_t size);
void operator delete(void *p);
// .. other members here ...
};
void *TestClass::operator new(size_t size){
void *p = malloc(size); // Replace this with alternative allocator
return (p);
}
void TestClass::operator delete(void *p){
free(p); // Replace this with alternative de-allocator
}

  所有TestClass 對象的內存分配都採用這段代碼。更進一步,任何從TestClass 繼承的類也都採用這一方式,除非它自己也重載了new和 delete 操作符。通過重載new 和 delete 操作符的方法,你可以自由地採用不同的分配策略,從不同的內存池中分配不同的類對象。

爲單個的類重載new[]和delete[]

  必須小心對象數組的分配。你可能希望調用到被你重載過的new 和 delete 操作符,但並不如此。內存的請求被定向到全局的new[]delete[] 操作符,而這些內存來自於系統堆。
  C++將對象數組的內存分配作爲一個單獨的操作,而不同於單個對象的內存分配。爲了改變這種方式,你同樣需要重載new[] 和 delete[]操作符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TestClass {
public:
void * operator new[ ](size_t size);
void operator delete[ ](void *p);
// .. other members here ..
};
void *TestClass::operator new[ ](size_t size){
void *p = malloc(size);
return (p);
}
void TestClass::operator delete[ ](void *p){
free(p);
}
int main(void){
TestClass *p = new TestClass[10];
// ... etc ...
delete[ ] p;
}

  但是注意:對於多數C++的實現,new[]操作符中的個數參數是數組的大小加上額外的存儲對象數目的一些字節。在你的內存分配機制重要考慮的這一點。你應該儘量避免分配對象數組,從而使你的內存分配策略簡單。

常見的內存錯誤及其對策

  發生內存錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程序運行時才能捕捉到。而這些錯誤大多沒有明顯的症狀,時隱時現,增加了改錯的難度。有時用戶怒氣衝衝地把你找來,程序卻沒有發生任何問題,你一走,錯誤又發作了。 常見的內存錯誤及其對策如下:

  • 內存分配未成功,卻使用了它。編程新手常犯這種錯誤,因爲他們沒有意識到內存分配會不成功。常用解決辦法是,在使用內存之前檢查指針是否爲NULL。如果指針p是函數的參數,那麼在函數的入口處用assert(p!=NULL)進行檢查。如果是用mallocnew來申請內存,應該用if(p==NULL) 或if(p!=NULL)進行防錯處理。
  • 內存分配雖然成功,但是尚未初始化就引用它。犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以爲內存的缺省初值全爲零,導致引用初值錯誤(例如數組)。內存的缺省初值究竟是什麼並沒有統一的標準,儘管有些時候爲零值,我們寧可信其無不可信其有。所以無論用何種方式創建數組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。
  • 內存分配成功並且已經初始化,但操作越過了內存的邊界。例如在使用數組時經常發生下標“多1”或者“少1”的操作。特別是在for循環語句中,循環次數很容易搞錯,導致數組操作越界。
  • 忘記了釋放內存,造成內存泄露。含有這種錯誤的函數每被調用一次就丟失一塊內存。剛開始時系統的內存充足,你看不到錯誤。終有一次程序突然死掉,系統出現提示:內存耗盡。動態內存的申請與釋放必須配對,程序中mallocfree的使用次數一定要相同,否則肯定有錯誤(new/delete同理)。
  • 釋放了內存卻繼續使用它。

  有三種情況:
  (1). 程序中的對象調用關係過於複雜,實在難以搞清楚某個對象究竟是否已經釋放了內存,此時應該重新設計數據結構,從根本上解決對象管理的混亂局面。
  (2). 函數的return語句寫錯了,注意不要返回指向“棧內存”的“指針”或者“引用”,因爲該內存在函數體結束時被自動銷燬。
  (3). 使用freedelete釋放了內存後,沒有將指針設置爲NULL導致產生“野指針”
  那麼如何避免產生野指針呢?這裏列出了5條規則,平常寫程序時多注意一下,養成良好的習慣。

規則1:用mallocnew申請內存之後,應該立即檢查指針值是否爲NULL。防止使用指針值爲NULL的內存。
規則2:不要忘記爲數組和動態內存賦初值。防止將未被初始化的內存作爲右值使用。
規則3:避免數組或指針的下標越界,特別要當心發生“多1”或者“少1”操作。
規則4:動態內存的申請與釋放必須配對,防止內存泄漏。
規則5:用freedelete釋放了內存之後,立即將指針設置爲NULL,防止產生“野指針”。

針與數組的對比

  C++/C程序中,指針和數組在不少地方可以相互替換着用,讓人產生一種錯覺,以爲兩者是等價的。
  數組要麼在靜態存儲區被創建(如全局數組),要麼在棧上被創建。數組名對應着(而不是指向)一塊內存,其地址與容量在生命期內保持不變,只有數組的內容可以改變。
  指針可以隨時指向任意類型的內存塊,它的特徵是“可變”,所以我們常用指針來操作動態內存。指針遠比數組靈活,但也更危險。
  下面以字符串爲例比較指針與數組的特性。

修改內容

  下面示例中,字符數組a的容量是6個字符,其內容爲 hello。a的內容可以改變,如a[0]= ‘X’。指針p指向常量字符串“world”(位於靜態存儲區,內容爲world),常量字符串的內容是不可以被修改的。從語法上看,編譯器並不覺得語句p[0]= ‘X’有什麼不妥,但是該語句企圖修改常量字符串的內容而導致運行錯誤。

1
2
3
4
5
6
char a[] = “hello”;
a[0] = ‘X’;
cout << a << endl;
char *p = “world”; // 注意p指向常量字符串
p[0] = ‘X’; // 編譯器不能發現該錯誤
cout << p << endl;

內容複製與比較

  不能對數組名進行直接複製與比較。若想把數組a的內容複製給數組b,不能用語句 b = a ,否則將產生編譯錯誤。應該用標準庫函數strcpy進行復制。同理,比較b和a的內容是否相同,不能用if(b==a) 來判斷,應該用標準庫函數strcmp進行比較。
  語句 p = a 並不能把a的內容複製指針p,而是把a的地址賦給了p。要想複製a的內容,可以先用庫函數malloc爲p申請一塊容量爲strlen(a)+1個字符的內存,再用strcpy進行字符串複製。同理,語句if(p==a) 比較的不是內容而是地址,應該用庫函數strcmp來比較。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 數組…
char a[] = "hello";
char b[10];
strcpy(b, a); // 不能用 b = a;
if(strcmp(b, a) == 0) // 不能用 if (b == a)
// 指針…
int len = strlen(a);
char *p = (char *)malloc(sizeof(char)*(len+1));
strcpy(p,a); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)

計算內存容量

  用運算符sizeof可以計算出數組的容量(字節數)。如下示例中,sizeof(a)的值是12(注意別忘了’’)。指針p指向a,但是sizeof(p)的值卻是4。這是因爲sizeof(p)得到的是一個指針變量的字節數,相當於sizeof(char*),而不是p所指的內存容量。C++/C語言沒有辦法知道指針所指的內存容量,除非在申請內存時記住它。

1
2
3
4
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12字節
cout<< sizeof(p) << endl; // 4字節

  注意當數組作爲函數的參數進行傳遞時,該數組自動退化爲同類型的指針。如下示例中,不論數組a的容量是多少,sizeof(a)始終等於sizeof(char *)

1
2
3
void Func(char a[100]){
cout<< sizeof(a) << endl; // 4字節而不是100字節
}

指針參數是如何傳遞內存的

  如果函數的參數是一個指針,不要指望用該指針去申請動態內存。如下示例中,Test函數的語句GetMemory(str, 200)並沒有使str獲得期望的內存,str依舊是NULL,爲什麼?

1
2
3
4
5
6
7
8
9
10
void GetMemory(char *p, int num){
p = (char *)malloc(sizeof(char) * num);
}
void Test(void){
char *str = NULL;
GetMemory(str, 100); // str 仍然爲 NULL
strcpy(str, "hello"); // 運行錯誤
}

  毛病出在函數GetMemory中。編譯器總是要爲函數的每個參數製作臨時副本,指針參數p的副本是 _p,編譯器使 _p=p。如果函數體內的程序修改了_p的內容,就導致參數p的內容作相應的修改。這就是指針可以用作輸出參數的原因。在本例中,_p申請了新的內存,只是把 _p所指的內存地址改變了,但是p絲毫未變。所以函數GetMemory並不能輸出任何東西。事實上,每執行一次GetMemory就會泄露一塊內存,因爲沒有用free釋放內存。
  如果非得要用指針參數去申請內存,那麼應該改用“指向指針的指針”,見示例:

1
2
3
4
5
6
7
8
9
10
11
12
void GetMemory2(char **p, int num){
*p = (char *)malloc(sizeof(char) * num);
}
void Test2(void){
char *str = NULL;
GetMemory2(&str, 100); // 注意參數是 &str,而不是str
strcpy(str, "hello");
cout<< str << endl;
free(str);
}

  由於“指向指針的指針”這個概念不容易理解,我們可以用函數返回值來傳遞動態內存。這種方法更加簡單,見示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
char *GetMemory3(int num){
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
void Test3(void){
char *str = NULL;
str = GetMemory3(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
}

  用函數返回值來傳遞動態內存這種方法雖然好用,但是常常有人把return語句用錯了。這裏強調不要用return語句返回指向“棧內存”的指針,因爲該內存在函數結束時自動消亡,見示例:

1
2
3
4
5
6
7
8
9
10
char *GetString(void){
char p[] = "hello world";
return p; // 編譯器將提出警告
}
void Test4(void){
char *str = NULL;
str = GetString(); // str 的內容是垃圾
cout<< str << endl;
}

  用調試器逐步跟蹤Test4,發現執行str = GetString語句後str不再是NULL指針,但是str的內容不是“hello world”而是垃圾。
  如果把上述示例改寫成如下示例,會怎麼樣?

1
2
3
4
5
6
7
8
9
10
char *GetString2(void){
char *p = "hello world";
return p;
}
void Test5(void){
char *str = NULL;
str = GetString2();
cout<< str << endl;
}

  函數Test5運行雖然不會出錯,但是函數GetString2的設計概念卻是錯誤的。因爲GetString2內的“hello world”是常量字符串,位於靜態存儲區,它在程序生命期內恆定不變。無論什麼時候調用GetString2,它返回的始終是同一個“只讀”的內存塊。

杜絕“野指針”

  “野指針”不是NULL指針,是指向“垃圾”內存的指針。人們一般不會錯用NULL指針,因爲用if語句很容易判斷。但是“野指針”是很危險的,if語句對它不起作用。 “野指針”的成因主要有三種:

  (1). 指針變量沒有被初始化。任何指針變量剛被創建時不會自動成爲NULL指針,它的缺省值是隨機的,它會亂指一氣。所以,指針變量在創建的同時應當被初始化,要麼將指針設置爲NULL,要麼讓它指向合法的內存。例如:

1
2
char *p = NULL;
char *str = (char *) malloc(100);

  (2). 指針p被free或者delete之後,沒有置爲NULL,讓人誤以爲p是個合法的指針。

  (3). 指針操作超越了變量的作用域範圍。這種情況讓人防不勝防,示例程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A{
public:
void Func(void){ cout << “Func of class A” << endl; }
};
void Test(void){
A *p;
{
A a;
p = &a; // 注意 a 的生命期
}
p->Func(); // p是“野指針”
}

  函數Test在執行語句p->Func()時,對象a已經消失,而p是指向a的,所以p就成了“野指針”。但奇怪的是我運行這個程序時居然沒有出錯,這可能與編譯器有關。

有了malloc/free爲什麼還要new/delete

  mallocfree是C++/C語言的標準庫函數,new/delete是C++的運算符。它們都可用於申請動態內存和釋放內存。
  對於非內部數據類型的對象而言,光用maloc/free無法滿足動態對象的要求。對象在創建的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。由於malloc/free是庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加於malloc/free
  因此C++語言需要一個能完成動態內存分配和初始化工作的運算符new,以及一個能完成清理與釋放內存工作的運算符delete。注意new/delete不是庫函數。我們先看一看malloc/freenew/delete如何實現對象的動態內存管理,見示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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不能執行構造函數與析構函數,必須調用成員函數InitializeDestroy來完成初始化與清除工作。函數UseNewDelete則簡單得多。
  所以我們不要企圖用malloc/free來完成動態對象的內存管理,應該用new/delete。由於內部數據類型的“對象”沒有構造與析構的過程,對它們而言malloc/freenew/delete是等價的。
  既然new/delete的功能完全覆蓋了malloc/free,爲什麼C++不把malloc/free淘汰出局呢?這是因爲C++程序經常要調用C函數,而C程序只能用malloc/free管理動態內存。
  如果用free釋放“new創建的動態對象”,那麼該對象因無法執行析構函數而可能導致程序出錯。如果用delete釋放“malloc申請的動態內存”,結果也會導致程序出錯,但是該程序的可讀性很差。所以new/delete必須配對使用,malloc/free也一樣。

內存耗盡怎麼辦

  如果在申請動態內存時找不到足夠大的內存塊,mallocnew將返回NULL指針,宣告內存申請失敗。通常有三種方式處理“內存耗盡”問題。
  (1). 判斷指針是否爲NULL,如果是則馬上用return語句終止本函數。例如:

1
2
3
4
5
6
void Func(void){
A *a = new A;
if(a == NULL)
return;
}

  (2). 判斷指針是否爲NULL,如果是則馬上用exit(1)終止整個程序的運行。例如:

1
2
3
4
5
6
7
8
9
void Func(void){
A *a = new A;
if(a == NULL){
cout << “Memory Exhausted” << endl;
exit(1);
}
}

  (3). 爲newmalloc設置異常處理函數。例如Visual C++可以用_set_new_hander函數爲new設置用戶自己定義的異常處理函數,也可以讓malloc享用與new相同的異常處理函數。詳細內容請參考C++使用手冊。
  上述 (1)、(2) 方式使用最普遍。如果一個函數內有多處需要申請動態內存,那麼方式 (1) 就顯得力不從心(釋放內存很麻煩),應該用方式 (2) 來處理。
  很多人不忍心用exit(1),問:“不編寫出錯處理程序,讓操作系統自己解決行不行?”
  不行。如果發生“內存耗盡”這樣的事情,一般說來應用程序已經無藥可救。如果不用exit(1) 把壞程序殺死,它可能會害死操作系統。道理如同:如果不把歹徒擊斃,歹徒在老死之前會犯下更多的罪。
  有一個很重要的現象要告訴大家。對於32位以上的應用程序而言,無論怎樣使用malloc與new,幾乎不可能導致“內存耗盡”。對於32位以上的應用程序,“內存耗盡”錯誤處理程序毫無用處。這下可把Unix和Windows程序員們樂壞了:反正錯誤處理程序不起作用,我就不寫了,省了很多麻煩。
  必須強調:不加錯誤處理將導致程序的質量很差,千萬不可因小失大。

1
2
3
4
5
6
7
8
9
void main(void){
float *p = NULL;
while(TRUE){
p = new float[1000000];
cout << “eat memory” << endl;
if(p==NULL)
exit(1);
}
}

malloc/free的使用要點

  函數malloc的原型如下:

1
void * malloc(size_t size);

  用malloc申請一塊長度爲length的整數類型的內存,程序如下:

1
int *p = (int *) malloc(sizeof(int) * length);

  我們應當把注意力集中在兩個要素上:“類型轉換”和“sizeof”。
  * malloc返回值的類型是void*,所以在調用malloc時要顯式地進行類型轉換,將void *轉換成所需要的指針類型。
  * malloc函數本身並不識別要申請的內存是什麼類型,它只關心內存的總字節數。我們通常記不住intfloat等數據類型的變量的確切字節數。例如int變量在16位系統下是2個字節,在32位下是4個字節;而float變量在16位系統下是4個字節,在32位下也是4個字節。最好用以下程序作一次測試:

1
2
3
4
5
6
7
8
cout << sizeof(char) << endl;
cout << sizeof(int) << endl;
cout << sizeof(unsigned int) << endl;
cout << sizeof(long) << endl;
cout << sizeof(unsigned long) << endl;
cout << sizeof(float) << endl;
cout << sizeof(double) << endl;
cout << sizeof(void *) << endl;

  在malloc的“()”中使用sizeof運算符是良好的風格,但要當心有時我們會昏了頭,寫出 p = malloc(sizeof(p))這樣的程序來。
  函數free的原型如下:

1
void free( void * memblock );

  爲什麼free函數不象malloc函數那樣複雜呢?這是因爲指針p的類型以及它所指的內存的容量事先都是知道的,語句free(p)能正確地釋放內存。如果pNULL指針,那麼freep無論操作多少次都不會出問題。如果p不是NULL指針,那麼freep連續操作兩次就會導致程序運行錯誤。

new/delete的使用要點

  運算符new使用起來要比函數malloc簡單得多,例如:

1
2
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];

  這是因爲new內置了sizeof、類型轉換和類型安全檢查功能。對於非內部數據類型的對象而言,new在創建動態對象的同時完成了初始化工作。如果對象有多個構造函數,那麼new的語句也可以有多種形式。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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創建對象數組,那麼只能使用對象的無參數構造函數。例如:

1
Obj *objects = new Obj[100]; // 創建100個動態對象

  不能寫成:

1
Obj *objects = new Obj[100](1);// 創建100個動態對象的同時賦初值1

  在用delete釋放對象數組時,留意不要丟了符號‘[]’。例如:

1
2
delete []objects; // 正確的用法
delete objects; // 錯誤的用法

  後者有可能引起程序崩潰和內存泄漏。



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