天天看點

看完這篇你還能不懂C語言/C++記憶體管理?(二)

二、malloc 和 free

在 C 語言(不是 C++)中,malloc 和 free 是系統提供的函數,成對使用,用于從堆中配置設定和釋放記憶體。malloc 的全稱是 memory allocation 譯為“動态記憶體配置設定”。

2.1 malloc 和 free 的使用

在開辟堆空間時我們使用的函數為 malloc,malloc 在 C 語言中是用于申請記憶體空間,malloc 函數的原型如下:

void *malloc(size_t size);

在 malloc 函數中,size 是表示需要申請的記憶體空間大小,申請成功将會傳回該記憶體空間的位址;申請失敗則會傳回 NULL,并且申請成功也不會自動進行初始化。

細心的同學可能會發現,該函數的傳回值說明為 void *,在這裡 void * 并不指代某一種特定的類型,而是說明該類型不确定,通過接收的指針變量進而進行類型的轉換。在配置設定記憶體時需要注意,即時在程式關閉時系統會自動回收該手動申請的記憶體 ,但也要進行手動的釋放,保證記憶體能夠在不需要時傳回至堆空間,使記憶體能夠合理的配置設定使用。

釋放空間使用 free 函數,函數原型如下:

void free(void *ptr);

free 函數的傳回值為 void,沒有傳回值,接收的參數為使用 malloc 配置設定的記憶體空間指針。一個完整的堆記憶體申請與釋放的例子如下:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
 
int main() {
    int n, *p, i;
    printf("請輸入一個任意長度的數字來配置設定空間:");
    scanf("%d", &n);
    
    p = (int *)malloc(n * sizeof(int));
 if(p==NULL){
  printf("申請失敗\n");
  return 0;
 }else{
  printf("申請成功\n");
 } 
 
 memset(p, 0, n * sizeof(int));//填充0 
 
 //檢視 
    for (i = 0; i < n; i++)
        printf("%d ", p[i]);
    printf("\n");
 
    free(p);
    p = NULL;
    return 0;
}

      

以上代碼中使用了 malloc 建立了一個由使用者輸入建立指定大小的記憶體,判斷了記憶體位址是否建立成功,且使用了 memset 函數對該記憶體空間進行了填充值,随後使用 for 循環進行了檢視。最後使用了 free 釋放了記憶體,并且将 p 指派 NULL,這點需要主要,不能使指針指向未知的位址,要置于 NULL;否則在之後的開發者會誤以為是個正常的指針,就有可能再通過指針去通路一些操作,但是在這時該指針已經無用,指向的記憶體也不知此時被如何使用,這時若出現意外将會造成無法預估的後果,甚至導緻系統崩潰,在 malloc 的使用中更需要需要。

2.2 記憶體洩漏與安全使用執行個體與講解

記憶體洩漏是指在動态配置設定的記憶體中,并沒有釋放記憶體或者一些原因造成了記憶體無法釋放,輕度則造成系統的記憶體資源浪費,嚴重的導緻整個系統崩潰等情況的發生。

看完這篇你還能不懂C語言/C++記憶體管理?(二)

記憶體洩漏通常比較隐蔽,且少量的記憶體洩漏發生不一定會發生無法承受的後果,但由于該錯誤的積累将會造成整體系統的性能下降或系統崩潰。特别是在較為大型的系統中,如何有效的防止記憶體洩漏等問題的出現變得尤為重要。例如一些長時間的程式,若在運作之初有少量的記憶體洩漏的問題産生可能并未呈現,但随着運作時間的增長、系統業務處理的增加将會累積出現記憶體洩漏這種情況;這時極大的會造成不可預知的後果,如整個系統的崩潰,造成的損失将會難以承受。由此防止記憶體洩漏對于底層開發人員來說尤為重要。

C 程式員在開發過程中,不可避免的面對記憶體操作的問題,特别是頻繁的申請動态記憶體時會及其容易造成記憶體洩漏事故的發生。如申請了一塊記憶體空間後,未初始化便讀其中的内容、間接申請動态記憶體但并沒有進行釋放、釋放完一塊動态申請的記憶體後繼續引用該記憶體内容;如上所述這種問題都是出現記憶體洩漏的原因,往往這些原因由于過于隐蔽在測試時不一定會完全清楚,将會導緻在項目上線後的長時間運作下,導緻災難性的後果發生。

如下是一個在子函數中進行了記憶體空間的申請,但是并未對其進行釋放:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
void m() { 
 char *p1; 
 p1 = malloc(100); 
 printf("開始對記憶體進行洩漏...");
}
 
int main() {
    m();
    return 0;
}

      

如上代碼中,使用 malloc 申請了 100 個機關的記憶體空間後,并沒有進行釋放。假設該 m 函數在目前系統中調用頻繁,那将會每次使用都将會造成 100 個機關的記憶體空間不會釋放,久而久之就會造成嚴重的後果。理應在 p1 使用完畢後添加 free 進行釋放:

free(p1);

以下示範一個讀取檔案時不規範的操作:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *filename) { 
 FILE* f;
 int key; 
 f = fopen(filename, "r"); 
 fscanf(f, "%d", &key); 
 return key; 
}
int main() {
    m("number.txt");
    return 0;
}      

以上檔案在讀取時并沒有進行 fclose,這時将會産生多餘的記憶體,可能一次還好,多次會增加成倍的記憶體,可以使用循環進行調用,之後在任務管理器中可檢視該程式運作時所占的記憶體大小,代碼為:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *filename) { 
 FILE* f;
 int key; 
 f = fopen(filename, "r"); 
 fscanf(f, "%d", &key); 
 return key; 
}
int main() {
 int i;
 for(i=0;i<500;i++) {
     m("number.txt");
 }
    return 0;
}      

可檢視添加循環後的程式與添加循環前的程式做記憶體占用的對比,就可以發現兩者之間添加了循環的代碼将會成本增加占用容量。

未被初始化的指針也會有可能造成記憶體洩漏的情況,因為指針未初始化所指向不可控,如:

int *p;

*p = val;

包括錯誤的釋放記憶體空間:

pp=p;

free(p);

free(pp);

釋放後使用,産生懸空指針。在申請了動态記憶體後,使用指針指向了該記憶體,使用完畢後我們通過 free 函數釋放了申請的記憶體,該記憶體将會允許其它程式進行申請;但是我們使用過後的動态記憶體指針依舊指向着該位址,假設其它程式下一秒申請了該區域内的記憶體位址,并且進行了操作。當我依舊使用已 free 釋放後的指針進行下一步的操作時,或者所進行了一個計算,那麼将會造成的結果天差地别,或者是其它災難性後果。是以對于這些指針在生存期結束之後也要置為 null。檢視一個示例,由于 free 釋放後依舊使用該指針,造成的計算結果天差地别:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *freep) { 
 int val=freep[0];
 printf("2*freep=:%d\n",val*2);
 free(freep);
 val=freep[0];
 printf("2*freep=:%d\n",val*2);
}
int main() {
 int *freep = (int *) malloc(sizeof (int));
 freep[0]=1;
 m(freep);
    return 0;
   
}      

以上代碼使用 malloc 申請了一個記憶體後,傳值為 1;在函數中首先使用 val 值接收 freep 的值,将 val 乘 2,之後釋放 free,重新指派給 val,最後使用 val 再次乘 2,此時造成的結果出現了極大的改變,而且最恐怖的是該錯誤很難發現,隐蔽性很強,但是造成的後顧難以承受。運作結果如下:

看完這篇你還能不懂C語言/C++記憶體管理?(二)

三、 new 和 delete

C++ 中使用 new 和 delete 從堆中配置設定和釋放記憶體,new 和 delete 是運算符,不是函數,兩者成對使用(後面說明為什麼成對使用)。

new/delete 除了配置設定記憶體和釋放記憶體(與 malloc/free),還做更多的事情,所有在 C++ 中不再使用 malloc/free 而使用 new/delete。

3.1 new 和 delete 使用

new 一般使用格式如下:

指針變量名 = new 類型辨別符;

指針變量名 = new 類型辨別符(初始值);

指針變量名 = new 類型辨別符[記憶體單元個數];

在C++中new的三種用法包括:plain new, nothrow new 和 placement new。

plain new 就是我們最常使用的 new 的方式,在 C++ 中的定義如下:

void* operator new(std::size_t) throw(std::bad_alloc);  

void operator delete( void *) throw();

plain new 在配置設定失敗的情況下,抛出異常 std::bad_alloc 而不是傳回 NULL,是以通過判斷傳回值是否為 NULL 是徒勞的。

char *getMemory(unsigned long size)   
{    
    char * p = new char[size];   
    return p; 
}   
void main(void)   
{
    try{   
        char * p = getMemory(1000000);    // 可能發生異常
        // ...   
        delete [] p;   
    }   
    catch(const std::bad_alloc &amp; ex)   
    {
        cout &lt;&lt; ex.what();
    }   
}      

nothrow new 是不抛出異常的運算符new的形式。nothrow new在失敗時,傳回NULL。定義如下:

void * operator new(std::size_t, const std::nothrow_t&) throw();
void operator delete(void*) throw();
void func(unsinged long length)   
{
    unsinged char * p = new(nothrow) unsinged char[length];   
    // 在使用這種new時要加(nothrow) ,表示不使用異常處理 。
    if (p == NULL)  // 不抛異常,一定要檢查
        cout << "allocte failed !";   
        // ...   
    delete [] p;
}      

placement new 意即“放置”,這種new允許在一塊已經配置設定成功的記憶體上重新構造對象或對象數組。placement new不用擔心記憶體配置設定失敗,因為它根本不配置設定記憶體,它做的唯一一件事情就是調用對象的構造函數。定義如下:

void* operator new(size_t, void*);

void operator delete(void*, void*);

palcement new 的主要用途就是反複使用一塊較大的動态配置設定的記憶體來構造不同類型的對象或者他們的數組。placement new構造起來的對象或其數組,要顯示的調用他們的析構函數來銷毀,千萬不要使用delete。

void main()   
{  
    using namespace std;   
    char * p = new(nothrow) char [4];   
    if (p == NULL)   
    {
        cout << "allocte failed" << endl;  
        exit( -1 );
    }   
    // ...   
    long * q = new (p) long(1000);   
    delete []p;    // 隻釋放 p,不要用q釋放。
}      

p 和 q 僅僅是首址相同,所建構的對象可以類型不同。所“放置”的空間應小于原空間,以防不測。當”放置new”超過了申請的範圍,Debug 版下會崩潰,但 Release 能運作而不會出現崩潰!

該運算符的作用是:隻要第一次配置設定成功,不再擔心配置設定失敗。

void main()   
{
    using namespace std;   
    char * p = new(nothrow) char [100];   
    if (p == NULL)   
    {  
        cout << "allocte failed" << endl;
        exit(-1);
    }   
    long * q1 = new (p) long(100);   
    // 使用q1  ...   
    int * q2 = new (p) int[100/sizeof(int)];   
    // 使用q2 ...   
    ADT * q3 = new (p) ADT[100/sizeof(ADT)];   
    // 使用q3  然後釋放對象 ...   
    delete [] p;    // 隻釋放空間,不再析構對象。
}      

注意:使用該運算符構造的對象或數組,一定要顯式調用析構函數,不可用 delete 代替析構,因為 placement new 的對象的大小不再與原空間相同。

void main()   
{  
    using namespace std;   
    char * p = new(nothrow) char [sizeof(ADT)+2];   
    if (p == NULL)   
    {  
        cout << "allocte failed" &lt;&lt; endl;
        exit(-1); 
    } 
    // ... 
    ADT * q = new (p) ADT; 
    // ... 
    // delete q; // 錯誤
    q->ADT::~ADT();  // 顯式調用析構函數,僅釋放對象
    delete [] p;     // 最後,再用原指針來釋放記憶體
}      

placement new 的主要用途就是可以反複使用一塊已申請成功的記憶體空間。這樣可以避免申請失敗的徒勞,又可以避免使用後的釋放。

特别要注意的是對于 placement new 絕不可以調用的 delete, 因為該 new 隻是使用别人替它申請的地方。釋放記憶體是 nothrow new 的事,即要使用原來的指針釋放記憶體。free/delete 不要重複調用,被系統立即回收後再利用,再一次 free/delete 很可能把不是自己的記憶體釋放掉,導緻異常甚至崩潰。

上面提到 new/delete 比 malloc/free 多做了一些事情,new 相對于 malloc 會額外的做一些初始化工作,delete 相對于 free 多做一些清理工作。

class A
{
 public:
     A()
     {
        cont<<"A()構造函數被調用"<<endl;
     }
     ~A()
     {
        cont<<"~A()構造函數被調用"<<endl;
     }
}      

在 main 主函數中,加入如下代碼:

A* pa = new A();  //類 A 的構造函數被調用

delete pa;        //類 A 的析構函數被調用

可以看出:使用 new 生成一個類對象時系統會調用該類的構造函數,使用 delete 删除一個類對象時,系統會調用該類的析構函數。可以調用構造函數/析構函數就意味着 new 和 delete 具備針對堆所配置設定的記憶體進行初始化和釋放的能力,而 malloc 和 free 不具備。

2.2 delete 與 delete[] 的差別

c++ 中對 new 申請的記憶體的釋放方式有 delete 和 delete[] 兩種方式,到底這兩者有什麼差別呢?

我們通常從教科書上看到這樣的說明:

delete 釋放 new 配置設定的單個對象指針指向的記憶體

delete[] 釋放 new 配置設定的對象數組指針指向的記憶體 那麼,按照教科書的了解,我們看下下面的代碼:

int *a = new int[10];

delete a;        //方式1

delete[] a;     //方式2

針對簡單類型 使用 new 配置設定後的不管是數組還是非數組形式記憶體空間用兩種方式均可 如:

delete a;

delete[] a;

此種情況中的釋放效果相同,原因在于:配置設定簡單類型記憶體時,記憶體大小已經确定,系統可以記憶并且進行管理,在析構時,系統并不會調用析構函數。

它直接通過指針可以擷取實際配置設定的記憶體空間,哪怕是一個數組記憶體空間(在配置設定過程中 系統會記錄配置設定記憶體的大小等資訊,此資訊儲存在結構體 _CrtMemBlockHeader 中,具體情況可參看 VC 安裝目錄下 CRTSRCDBGDEL.cpp)。

針對類 Class,兩種方式展現出具體差異

當你通過下列方式配置設定一個類對象數組:

class A

  {

   private:

     char *m_cBuffer;

     int m_nLen;

  `` public:

     A(){ m_cBuffer = new char[m_nLen]; }

     ~A() { delete [] m_cBuffer; }

  };

  A *a = new A[10];

  delete a;         //僅釋放了a指針指向的全部記憶體空間 但是隻調用了a[0]對象的析構函數 剩下的從a[1]到a[9]這9個使用者自行配置設定的m_cBuffer對應記憶體空間将不能釋放 進而造成記憶體洩漏

  delete[] a;      //調用使用類對象的析構函數釋放使用者自己配置設定記憶體空間并且   釋放了a指針指向的全部記憶體空間

是以總結下就是,如果 ptr 代表一個用new申請的記憶體傳回的記憶體空間位址,即所謂的指針,那麼:

delete ptr  代表用來釋放記憶體,且隻用來釋放 ptr 指向的記憶體。delete[] rg   用來釋放rg指向的記憶體,!!還逐一調用數組中每個對象的destructor!!

對于像 int/char/long/int*/struct 等等簡單資料類型,由于對象沒有 destructor ,是以用 delete 和 delete []是一樣的!但是如果是 C++ 對象數組就不同了!

關于 new[] 和 delete[],其中又分為兩種情況:

(1) 為基本資料類型配置設定和回收空間;

(2) 為自定義類型配置設定和回收空間;

對于 (1),上面提供的程式已經證明了 delete[] 和 delete 是等同的。但是對于 (2),情況就發生了變化。

我們來看下面的例子,通過例子的學習了解 C++ 中的 delete 和 delete[] 的使用方法

#include <iostream>
using namespace std;
class Babe
{
public:
    Babe()
    {
        cout << \"Create a Babe to talk with me\" << endl;
    }
    ~Babe()
    {
        cout << \"Babe don\'t Go away,listen to me\" << endl;
    }
};
int main()
{
    Babe* pbabe = new Babe[3];
    delete pbabe;
    pbabe = new Babe[3];
    delete[] pbabe;
    return 0;
}      

結果是:

Create a babe to talk with me

Babe don\'t go away,listen to me

大家都看到了,隻使用 delete 的時候隻出現一個 Babe don’t go away,listen to me,而使用 delete[] 的時候出現 3 個 Babe don’t go away,listen to me。不過不管使用 delete 還是 delete[] 那三個對象的在記憶體中都被删除,既存儲位置都标記為可寫,但是使用 delete 的時候隻調用了 pbabe[0] 的析構函數,而使用了 delete[] 則調用了 3 個 Babe 對象的析構函數。

你一定會問,反正不管怎樣都是把存儲空間釋放了,有什麼差別。

答:關鍵在于調用析構函數上。此程式的類沒有使用作業系統的系統資源(比如:Socket、File、Thread等),是以不會造成明顯惡果。如果你的類使用了作業系統資源,單純把類的對象從記憶體中删除是不妥當的,因為沒有調用對象的析構函數會導緻系統資源不被釋放,這些資源的釋放必須依靠這些類的析構函數。是以,在用這些類生成對象數組的時候,用 delete[] 來釋放它們才是王道。而用 delete 來釋放也許不會出問題,也許後果很嚴重,具體要看類的代碼了。

最後祝各位保持良好的代碼編寫規範降低嚴重錯誤的産生。

繼續閱讀