記憶體分配
1.1 記憶體分配
1.1.1記憶體分配方式
記憶體分配方式有三種:
(1)從靜態儲存區域分配。記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在。例如全域性變數,變數等。
(2)在棧上建立。在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內建於處理器的指令集中,效率很高,但是分配的記憶體容量有限。
(3)從堆上分配,亦稱動態記憶體分配。程式在執行的時候用malloc或new申請任意多少所需要的記憶體,程式設計師自己負責在何時用free或delete釋放記憶體。動態記憶體的生存期由我們決定,使用非常靈活,但問題也最多。
全域性變數和static變數是程式是整個程式都需要用到的,單獨分出一塊儲存區儲存,該儲存區儲存的資料不清空,在程式的整個執行期間;區域性變數是函式退出時自動清空的,放在棧(stack)裡做臨時儲存。用指令new和malloc分配的記憶體需要自己手動申請在堆(heap)中申請並手動用free和delete指令釋放。
關於英文單詞stack和heap的解釋。從英漢詞典裡看這兩個單詞意思都有堆的意思,stack還可查到棧的意思,不好區分,讓人迷惑。然而在英英詞典裡這兩個單詞的意思差異明顯。Stack: a neat pile of things,而Heap: a large untidy pile of things。一個是整潔、整齊的一堆,而另一個是不整齊、凌亂的一堆。
1.1.2常見的記憶體錯誤及其對策
發生記憶體錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程式執行時才能捕捉到。而這些錯誤大多沒有明顯的症狀,時隱時現,增加了改錯的難度。有時使用者怒氣衝衝地把你找來,程式卻沒有發生任何問題,你一走,錯誤又發作了。 常見的記憶體錯誤及其對策如下:
² 記憶體分配未成功,卻使用了它
程式設計新手常犯這種錯誤,因為他們沒有意識到記憶體分配會不成功。常用解決辦法是,在使用記憶體之前檢查指標是否為NULL。如果指標p是函式的引數,那麼在函式的入口處用assert(p!=NULL)進行檢查。如果是用malloc或new來申請記憶體,應該用if(p==NULL) 或if(p!=NULL)進行防錯處理。
² 記憶體分配雖然成功,但是尚未初始化就引用它
犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為記憶體的預設初值全為零,導致引用初值錯誤(例如陣列)。記憶體的預設初值究竟是什麼並沒有統一的標準,儘管有些時候為零值,我們寧可信其無不可信其有。所以無論用何種方式建立陣列,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。
² 記憶體分配成功並且已經初始化,但操作越過了記憶體的邊界
例如在使用陣列時經常發生下標“多1”或者“少1”的操作。特別是在for迴圈語句中,迴圈次數很容易搞錯,導致陣列操作越界。
² 忘記了釋放記憶體,造成記憶體洩露
含有這種錯誤的函式每被呼叫一次就丟失一塊記憶體。剛開始時系統的記憶體充足,你看不到錯誤。終有一次程式突然死掉,系統出現提示:記憶體耗盡。
動態記憶體的申請與釋放必須配對,程式中malloc與free的使用次數一定要相同,否則肯定有錯誤(new/delete同理)。
² 釋放了記憶體卻繼續使用它
有三種情況:
(1)程式中的物件呼叫關係過於複雜,實在難以搞清楚某個物件究竟是否已經釋放了記憶體,此時應該重新設計資料結構,從根本上解決物件管理的混亂局面。
(2)函式的return語句寫錯了,注意不要返回指向“棧記憶體”的“指標”或者“引用”,因為該記憶體在函式體結束時被自動銷燬。
(3)使用free或delete釋放了記憶體後,沒有將指標設定為NULL。導致產生“野指標”。
1.1.3 記憶體使用一般規則
【規則1】用malloc或new申請記憶體之後,應該立即檢查指標值是否為NULL。防止使用指標值為NULL的記憶體;
【規則2】不要忘記為陣列和動態記憶體賦初值。防止將未被初始化的記憶體作為右值使用;
【規則3】避免陣列或指標的下標越界,特別要當心發生“多1”或者“少1”操作;
【規則4】動態記憶體的申請與釋放必須配對,防止記憶體洩漏;
【規則5】用free或delete釋放了記憶體之後,立即將指標設定為NULL,防止產生“野指標”。
1.1.4 指標與陣列的對比
C++/C程式中,指標和陣列在不少地方可以相互替換著用,讓人產生一種錯覺,以為兩者是等價的。
陣列要麼在靜態儲存區被建立(如全域性陣列),要麼在棧上被建立。陣列名對應著(而不是指向)一塊記憶體,其地址與容量在生命期內保持不變,只有陣列的內容可以改變。
指標可以隨時指向任意型別的記憶體塊,它的特徵是“可變”,所以我們常用指標來操作動態記憶體。指標遠比陣列靈活,但也更危險。
下面以字串為例比較指標與陣列的特性。
(1)修改內容
下例中,字元陣列a的容量是6個字元,其內容為hello。a的內容可以改變,如a[0]= 'x'。指標p指向常量字串"world"(位於靜態儲存區,內容為world),常量字串的內容是不可以被修改的。從語法上看,編譯器並不覺得語句 p[0]= 'x'有什麼不妥,但是該語句企圖修改常量字串的內容而導致執行錯誤。
#include
void main()
{
char a[] = "hello";
a[0] = 'x';
cout << a << endl;
char *p = "world"; // 注意p指向常量字串
p[0] = 'x'; // 編譯器不能發現該錯誤
cout << p << endl;
}
(2)內容複製與比較
不能對陣列名進行直接複製與比較。下例中,若想把陣列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來比較。
// 陣列…
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)
…
(3)計算記憶體容量
用運算子sizeof可以計算出陣列的容量(位元組數)。下例(a)中,sizeof(a)的值是12(注意別忘了' ')。指標p指向a,但是 sizeof(p)的值卻是4。這是因為sizeof(p)得到的是一個指標變數的位元組數,相當於sizeof(char*),而不是p所指的記憶體容量。 C++/C語言沒有辦法知道指標所指的記憶體容量,除非在申請記憶體時記住它。注意當陣列作為函式的引數進行傳遞時,該陣列自動退化為同型別的指標。下例(b)中,不論陣列a的容量是多少,sizeof(a)始終等於sizeof(char *)。
//示例(a)
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12位元組
cout<< sizeof(p) << endl; // 4位元組
//示例(b)
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4位元組而不是100位元組
}