1. 程式人生 > >記憶體管理(一)

記憶體管理(一)

記憶體管理  記憶體分配的方式
     記憶體分配方式有5種:

       1)從靜態儲存區域分配。例如程式中定義的全域性變數和static變數就是這種方式分配記憶體的。記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在。
       2)在棧上建立。這是出現最多的情況,我們程式中的int var就是這種情況的記憶體分配方式【函式引數及函式區域性變數】。在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限
       3)從堆上分配,亦稱動態記憶體分配

。程式在執行的時候用malloc 或new 申請任意多少的記憶體,程式設計師自己負責在何時用free 或delete 釋放記憶體。動態記憶體的生存期由我們決定,使用非常靈活,但問題也最多。
      4)、文字常量區:常量字串就是放在這裡的。程式結束後由系統釋放。
      5)、程式程式碼區:既可執行程式碼。
    

常見的記憶體錯誤及其相應建議
       發生記憶體錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程式執行時才能捕捉到。而這些錯誤大多沒有明顯的症狀,時隱時現,增加了改錯的難度。有時使用者怒氣衝衝地把你找來,程式卻沒有發生任何問題,你一走,錯誤又發作了。
       常見的記憶體錯誤和處理方法如下:
       1)記憶體分配未成功,卻使用了它


       程式設計新手常犯這種錯誤,因為他們沒有意識到記憶體分配會不成功。常用解決辦法是,在使用記憶體之前檢查指標是否為NULL 。如果指標p 是函式的引數,那麼在函式的入口處用assert (p!=NULL) 進行檢查。如果是用malloc或new 來申請記憶體,應該用if( p==NULL ) 或if(p!=NULL) 進行防錯處理。
       2)記憶體分配雖然成功,但是尚未初始化就引用它
       犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為記憶體的預設初值全為零,導致引用初值錯誤(例如陣列)。
       記憶體的預設初值究竟是什麼並沒有統一的標準,儘管有些時候為零值,我們寧可信其無不可信其有。所以無論用何種方式建立陣列,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。
      3)記憶體分配成功並且已經初始化,但操作越過了記憶體的邊界

       例如在使用陣列時經常發生下標“多1 ”或者“少1 ”的操作。特別是在for 迴圈語句中,迴圈次數很容易搞錯,導致陣列操作越界。 
      4)忘記了釋放記憶體,造成記憶體洩露
       含有這種錯誤的函式每被呼叫一次就丟失一塊記憶體。剛開始時系統的記憶體充足,你看不到錯誤。終有一次程式突然死掉,系統出現提示:記憶體耗盡。
       動態記憶體的申請與釋放必須配對,程式中malloc與free 的使用次數一定要相同,否則肯定有錯誤(new/delete 同理)。 
      5)釋放了記憶體卻繼續使用它。
       有三種情況:
       1. 程式中的物件呼叫關係過於複雜,實在難以搞清楚某個物件究竟是否已經釋放了記憶體,此時應該重新設計資料結構,從根本上解決物件管理的混亂局面。
       2. 函式的return 語句寫錯了,注意不要返回指向“棧記憶體”的“指標”或者“引用”,因為該記憶體在函式體結束時被自動銷燬。
       3. 使用free 或delete 釋放了記憶體後,沒有將指標設定為NULL。導致產生“野指標”。
       針對以上這些稍不留神,一疏忽就容易犯的錯誤,這裡提出以下建議:
              1)用malloc 或new 申請記憶體之後,應該立即檢查指標值是否為NULL。防止使用指標值為NULL的記憶體。
              2)不要忘記為陣列和動態記憶體賦初值。防止將未被初始化的記憶體作為右值使用。
              3)避免陣列或指標的下標越界,特別要當心發生“多1 ”或者“少1 ”操作。
              4)動態記憶體的申請與釋放必須配對,防止記憶體洩漏。
              5)用free 或delete 釋放了記憶體之後,立即將指標設定為NULL ,防止產生“野指標”
6.3 指標與陣列
       C++/C程式中,指標和陣列在不少地方可以相互替換著用,讓人產生一種錯覺,以為兩者是等價的。
      陣列要麼在靜態儲存區被建立(如全域性陣列),要麼在棧上被建立。陣列名對應著(而不是指向)一塊記憶體,其地址與容量在生命期內保持不變,只有陣列的內容可以改變。
       指標可以隨時指向任意型別的記憶體塊,它的特徵是“可變”,所以我們常用指標來操作動態記憶體。指標遠比陣列靈活,但也更危險。下面從一些角度說明指標和陣列的不同。
       1)關於兩者的內容修改
       一個例子,以下程式:

[cpp]
view plaincopy

char a[] = “hello”; 

  a[0] = ‘X’;  

 cout << a << endl;  

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

       字元陣列a的容量是6個字元,其內容為hello\0(位於棧上)。a的內容可以改變,如a[0]=‘X’。指標p指向常量字串“world”(位於靜態儲存區,內容為world\0),常量字串的內容是不可以被修改的。從語法上看,編譯器並不覺得語句p[0]= ‘X’有什麼不妥,但是該語句企圖修改常量字串的內容而導致執行錯誤。
       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來比較。
       具體的例子如下程式所示:

[cpp]
view plaincopy


// 陣列…  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可以計算出陣列的容量(位元組數)。如下例程式中,sizeof(a)的值是12(注意別忘了’\0’)。指標p 指向a,但是sizeof(p)的值卻是4。這是因為sizeof(p)得到的是一個指標變數的位元組數,相當於sizeof(char*),而不是p所指的記憶體容量。C++/C語言沒有辦法知道指標所指的記憶體容量,除非在申請記憶體時記住它。

[cpp]
view plaincopy


char a[] = "hello world";  

char *p = a;  

cout<< sizeof(a) << endl; // 12位元組 

cout<< sizeof(p) << endl; // 4位元組

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

[cpp]
view plaincopy

void Func(char a[100]) 

{

   cout<< sizeof(a) << endl; // 4位元組而不是100位元組 

}  

6.4 指標引數怎麼傳遞記憶體
       如果函式的引數是一個指標,不要指望用該指標去申請動態記憶體。如下例中,Test函式的語句GetMemory(str,200)並沒有使str獲得期望的記憶體,str依舊是NULL。

[cpp]
view plaincopy

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釋放記憶體。
       那如果筆試題或者面試官問,我一定要用指標引數去申請記憶體,我們怎麼做呢?好吧,那就耍個小花招,改用“指向指標的指標”好啦。如下程式所示:

[cpp]
view plaincopy

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);  

}

      什麼,你覺得“指向指標的指標”不是很容易理解?好吧,那換一種方式,我們可以用函式返回值來傳遞動態記憶體。如下程式所示:

[cpp]
view plaincopy


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語句返回指向“棧記憶體”的指標,因為該記憶體在函式結束時自動消亡!如下程式所示:

[cpp]
view plaincopy

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”而是垃圾。
       寫到這裡,又有人提出問題了,如果上例中的hello world是常字串,那會怎麼樣呢,就如下程式所示:

[cpp]
view plaincopy

char *GetString2(void)  

{

   char *p = "hello world";

   return p;

}  

void Test5(void)  

{

   char *str = NULL; 

   str = GetString2();

   cout<< str << endl;  

}

       那這裡要說明的是,事實上這段程式執行不會出錯,但是函式GetString2的設計概念卻是錯誤的。因為GetString2 內的“hello world”是常量字串,位於靜態儲存區,它在程式生命期內恆定不變。無論什麼時候呼叫GetString2,它返回的始終是同一個“只讀”的記憶體塊,所以沒有太大的意義。
6.5 關於free和delete
      別看free和delete的名字惡狠狠的(尤其是delete),它們只是把指標所指的記憶體給釋放掉,但指標本身卻被留下來了。
       用偵錯程式跟蹤下例,發現指標p被free以後其地址仍然不變(非NULL),只是該地址對應的記憶體是垃圾,p成了“野指標”。如果此時不把p設定為NULL,會讓人誤以為p是個合法的指標。

[cpp]
view plaincopy

char *p = (char *) malloc(100);  

strcpy(p, “hello”);

free(p);  // p  所指的記憶體被釋放,但是p所指的地址仍然不變  … 

if(p != NULL)  // 沒有起到防錯作用  {   strcpy(p, “world”); // 出錯  }

      如果程式比較長,我們有時記不住p所指的記憶體是否已經被釋放,在繼續使用p之前,通常會用語句if(p!=NULL)進行防錯處理。很遺憾,此時if語句起不到防錯作用,因為即便p不是NULL指標,它也不指向合法的記憶體塊。
6.6 動態記憶體會隨程式結束自動釋放嗎
       現在有這樣一個例子:

[cpp]
view plaincopy

void Func(void)  

 {

   char *p = (char *) malloc(100);  // 動態記憶體會自動釋放嗎? 

}

       我們都知道函式體內的區域性變數會在函式結束時自動消亡。這裡p是區域性的指標變數,程式結束後它會消亡,但是它消亡時它所指的動態記憶體會一起完蛋嗎?答案是:並不會...
       這裡有兩條非常有趣的結論,分別是:
              (1)指標消亡了,並不表示它所指的記憶體會被自動釋放。
              (2)記憶體被釋放了,並不表示指標會消亡或者成了NULL指標。
6.7 關於“野指標”
       “野指標”是最可怕的一類指標,它不是NULL指標,是指向“垃圾”記憶體的指標。人們一般不會錯用NULL指標,因為用if語句很容易判斷。但是“野指標”是很危險的,if語句對它不起作用。
       “野指標”的成因主要有兩種:
       (1)指標變數沒有被初始化。任何指標變數剛被建立時不會自動成為NULL指標,它的預設值是隨機的,它會亂指一氣。所以,指標變數在建立的同時應當被初始化,要麼將指標設定為NULL,要麼讓它指向合法的記憶體。例如

[cpp]
view plaincopy

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

       (2)指標p被free或者delete之後,沒有置為NULL,讓人誤以為p是個合法的指標。上一節也提到了這個問題。
       (3)指標操作超越了變數的作用範圍。這種情況讓人防不勝防,示例程式如下:現在有這樣一個例子:

[cpp]
view plaincopy

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就成了“野指標”。
6.8 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實現物件的動態記憶體管理的方法是不同的,下面是一個示例:

      [cpp]
view plaincopy


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也一樣。
6.9 關於“記憶體耗盡”問題
       如果在申請動態記憶體時找不到足夠大的記憶體塊,malloc和new將返回NULL指標,宣告記憶體申請失敗。通常有三種方式處理“記憶體耗盡”問題。
       1)判斷指標是否為NULL,如果是則馬上用return語句終止本函式。例如:

[cpp]
view plaincopy

void Func(void)  

{

     A *a = new A;

    if(a == NULL) 

   {   return;   } 

      … 

}

       2)判斷指標是否為NULL,如果是則馬上用exit(1)終止整個程式的執行。例如:

[cpp]
view plaincopy

void Func(void)  

{

      A *a = new A;

      if(a == NULL)

     {

            cout << “Memory Exhausted”<< endl;

            exit(1);

   } 

             … 

}

         3)為new和malloc設定異常處理函式。例如VisualC++可以用_set_new_hander函式為new設定使用者自己定義的異常處理函式,也可以讓malloc享用與new相同的異常處上述(1)(2)方式使用最普遍。如果一個函式內有多處需要申請動態記憶體,那麼方式(1)就顯得力不從心(釋放記憶體很麻煩),應該用方式(2)來處理。
       很多人不忍心用exit(1),問:“不編寫出錯處理程式,讓作業系統自己解決行不行?”
       不行。如果發生“記憶體耗盡”這樣的事情,一般說來應用程式已經無藥可救。如果不用exit(1) 把壞程式殺死,它可能會害死作業系統。道理如同:如果不把歹徒擊斃,歹徒在老死之前會犯下更多的罪。
       雖然對於32 位以上的應用程式而言,無論怎樣使用malloc與new,幾乎不可能導致“記憶體耗盡”。但是:不加錯誤處理將導致程式的質量很差,千萬不可因小失大。
6.10 再說malloc/free
       函式malloc的原型如下:

[cpp]
view plaincopy

void * malloc(size_t size);

       用malloc申請一塊長度為length的整數型別的記憶體,程式如下:

[cpp]
view plaincopy

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

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

[cpp]
view plaincopy

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))這樣的程式來。
       3)函式free的原型如下:

[cpp]
view plaincopy

void free( void * memblock );

      為什麼free函式不象malloc函式那樣複雜呢?這是因為指標p的型別以及它所指的記憶體的容量事先都是知道的,語句free(p)能正確地釋放記憶體。如果p是NULL指標,那麼free對p無論操作多少次都不會出問題。如果p不是NULL指標,那麼free對p連續操作兩次就會導致程式執行錯誤。
6.11 再說new/delete
       運算子new使用起來要比函式malloc簡單得多,例如:

[cpp]
view plaincopy

int *p1 = (int *)malloc(sizeof(int) * length);  

int *p2 = new int[length];

       這是因為new內建了sizeof、型別轉換和型別安全檢查功能。對於非內部資料型別的物件而言,new在建立動態物件的同時完成了初始化工作。如果物件有多個建構函式,那麼new的語句也可以有多種形式。例如

[cpp]
view plaincopy

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建立物件陣列,那麼只能使用物件的無引數建構函式。例如

[cpp]
view plaincopy

Obj *objects = new Obj[100]; // 建立100個動態物件

    不能寫成

    [cpp] 
    view plaincopy

    Obj *objects = new Obj[100](1);// 建立100個動態物件的同時賦初值1

     在用delete釋放物件陣列時,留意不要丟了符號‘[]’。例如

    [cpp]
view plaincopy

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

    後者相當於delete objects[0],漏掉了另外99個物件。
6.12 總結
   知識點在上已經一一敘述過了,這裡總結兩句話:
          1)越是怕指標,就越要使用指標。不會正確使用指標,肯定算不上是合格的程式設計師。
          2)必須養成“使用偵錯程式逐步跟蹤程式”的習慣,只有這樣才能發現問題的本質。