天天看點

C/C++動态記憶體管理

先來看下C語言中記憶體配置設定的方式(有三種)

C/C++動态記憶體管理
  • 在靜态存儲區配置設定記憶體:記憶體在程式編譯的時候就已經配置設定好,這塊記憶體在程式的整個運作期間都存在。
  • 全局的未初始化的變量配置設定在BSS段,全局的已初始化的變量分布在data段即已初始化資料段。這兩者都屬于從靜态存儲區配置設定。
  • BBS段是不占用可執行檔案空間的,它隻記錄資料所需空間的大小,其内容由作業系統初始化(清零);
  • 而data段卻需要占用,其内容由程式初始化。
  • 全局的static變量也是存放在資料段,具體放在哪看它有沒有初始化。
  • 全局的static變量相比于全局變量而言,不同點就是别的檔案看不到static變量,注意靜态全局變量盡量不要包含在頭檔案中,它是相對于不同的源檔案而言(即如果靜态全局變量定義在頭檔案中,隻要源檔案包含了這個頭檔案,還是可以通路到這個靜态全局變量的)。
  • 而靜态局部變量也是會在編譯階段就配置設定好記憶體,并且{}塊執行完之後,不會被銷毀,因為它也是存儲在資料段的并不是在棧中,雖然沒有被銷毀,但也不可用,隻能在該函數内部用。了解全局變量與靜态全局變量差別的一個例子如下      

test1.cpp  

int a;

static int b = 6;

test2.cpp

#include <iostream>

using namespace std;

extern int a;//extern表示去别的檔案找定義,隻在本檔案聲明

extern int b;

int main()

{

        cout << a;//a的輸出結果是0,有作業系統初始化(清零)

        cout << b;

        return 0;

}

上述的程式時會出現編譯錯誤的,報錯原因是因為b是一個無法解析的外部指令,即在源檔案test1中,b被定義成了static靜态全局變量,隻在本檔案中有效,test2源檔案是看不到b這個變量的。

  • 在棧上配置設定記憶體:棧上存放的是一些臨時建立的局部變量,即{}中建立的變量(但不包括static變量,它存儲在資料段)。除此以外,函數在被調用時,其參數也會被壓入發起調用的棧中,并且待到調用結束後,函數的傳回值也會被存放回棧中。棧記憶體配置設定運算内置于處理器的指令集中,效率很高,但是配置設定的記憶體是有限的。
  • 在堆上配置設定記憶體(也叫動态記憶體配置設定):程式在運作的時候用malloc和new申請任意多少的記憶體,程式員需要負責自己在何時用free和delete釋放記憶體。堆的大小不固定,可以伸縮,使用非常的靈活

C語言中的動态記憶體配置設定

C語言中使用跟記憶體申請有關的函數有alloca,malloc,calloc,realloc來開辟空間,先來看看他們的差別

  • alloca(很多系統中會宏定義成_alloca使用):向棧申請記憶體,是以無需釋放
  • malloc:malloc配置設定的記憶體是在堆中的,申請出來的記憶體沒有初始化,是以基本上malloc之後,調用函數memset來初始化這部分的記憶體空間
  • calloc:calloc則将初始化這部分的記憶體,并将其中的内容全部初始化為0
  • realloc:更愛以前配置設定區的長度(增加或減少)

當程式運作過程中malloc了,但是沒有free的話,會造成記憶體洩漏.一部分的記憶體沒有被使用,但是由于沒有free,是以系統認為這部分記憶體還在使用,造成不斷的向系統申請記憶體,使得系統可用記憶體不斷減少.但是記憶體洩漏僅僅指程式在運作時,程式退出時,OS将回收所有的資源。但是一般伺服器上的程序不後悔輕易重新開機程式,是以在malloc之後一定要記得負責free掉。

malloc、calloc、realloc的聲明(它們封裝在頭檔案malloc.h中)

void* malloc(unsigned size);

void* calloc(size_t numElements,size_t sizeOfElement);

void* realloc(void* ptr,unsigned newsize);

它們的傳回值都是請求出的記憶體的位址,需要進行指針強轉才能轉化為指定類型的指針。

  • malloc:在記憶體的動态存儲區配置設定一塊長度為size的連續區域,size為需要記憶體空間的長度,傳回該區域的首位址。用malloc配置設定記憶體并且用memset初始化的例子如下

int *ptr = (int*)malloc(sizeof(int) * 10);

memset(ptr, 0, sizeof(int) * 10);

  • calloc:與malloc相似,參數sizeOfElement為申請位址的機關元素長度,numElements為元素個數,即在記憶體中申請(numElements*sizeOfElement)位元組大小的連續位址空間,并且初始化為0。

int *ptr = (int*)calloc(10, 4);//與上面的malloc開辟的一樣,并且都是初始化為0了

  • realloc:對給定的指針所指的空間進行擴大或者縮小,參數ptr為原有的位址空間的指針,newsize是重新申請的位址長度。當擴大一塊記憶體空間時,realloc()試圖直接從堆上現存的資料後面的那些位元組中獲得附加的位元組,如果能夠滿足,就不變動指針位址;如果資料後面的位元組不夠,那麼就使用堆上第一個有足夠大小的自由塊(該自由塊可能是剛剛free出來的),現存的資料然後就被拷貝至新的位置,并釋放原來的指針所指向的那塊空間,原來那塊空間則放到存儲區上。
  • realloc的一個參數指針可以為空,那麼此時它的作用就和malloc一樣。

int *ptr = (int*)calloc(10, sizeof(int));

realloc(ptr, 20*sizeof(int));

//realloc新開辟了20個int大小的空間,前十個int還是0,并沒有變值,後十個就是沒有初始化的随機值

free掉上面三個函數所申請的空間,被釋放的空間通常被送入可用存儲區池,可在調用上述三個配置設定函數時再配置設定。

  • 以上三個alloc函數都會調用sbrk系統調用(是以說,malloc底下還有一層系統調用sbrk),該系統調用用來擴充(或縮小)程序的堆。
  • 雖然sbrk系統調用可以擴充或縮小程序的存儲空間,但是大多數malloc和free的實作都不減小程序的存儲空間。釋放的空間可以供以後再配置設定,但通常将他們保持在malloc池上而不傳回給核心。

malloc函數的工作原理

        malloc函數的實質展現在,它有一個将可用的記憶體塊連接配接為一個長長的清單的所謂空閑連結清單。調用malloc函數時,它沿連接配接表尋找一個大到足以滿足使用者請求所需要的記憶體塊(這也就是為什麼malloc申請出來的空間為什麼會比實際大小要大一點的原因)。然後,将該記憶體塊一分為二(一塊的大小與使用者請求的大小相等,另一塊的大小就是剩下的位元組,用來記錄管理資訊——配置設定塊的長度、指向下一個配置設定塊的指針等等)。接下來,将配置設定給使用者的那塊記憶體傳給使用者,并将剩下的那塊(如果有的話)傳回到連接配接表上。調用free函數時,它将使用者釋放的記憶體塊連接配接到空閑鍊上。到最後,空閑鍊會被切成很多的小記憶體片段,如果這時使用者申請一個大的記憶體片段,那麼空閑鍊上可能沒有可以滿足使用者要求的片段了。于是,malloc函數請求延時,并開始在空閑鍊上翻箱倒櫃地檢查各記憶體片段,對它們進行整理,将相鄰的小空閑塊合并成較大的記憶體塊。如果無法獲得符合要求的記憶體塊,malloc函數會傳回NULL指針,是以在調用malloc動态申請記憶體塊時,一定要進行傳回值的判斷。

常見的記憶體洩漏

void DoSomething()

{}

void MemoryLeaks()

{

        //1.記憶體配置設定了忘記釋放

        int *ptr = (int*)malloc(sizeof(int) * 10);

        assert(ptr);

        DoSomething();

       //這樣導緻了記憶體洩漏,但并沒有造成程式崩潰

        //2.程式邏輯不清,以為釋放了,實際上沒有釋放

        int *ptr1 = (int*)malloc(sizeof(int) * 10);

        int *ptr2 = (int*)malloc(sizeof(int) * 10);

        DoSomething();

        ptr1 = ptr2;

        free(ptr1);

        free(ptr2);//實際兩個釋放的是同一塊記憶體,會導緻程式崩潰,注意一塊記憶體釋放過了再釋放會導緻程式崩潰

        //3.程式誤操作,将堆破壞

        char* ptr3 = (char*)malloc(5);

        strcpy(ptr3, "Memory Leaks!");

       //實際上申請的空間不夠存放這些字元,破壞了堆,之後的空間可能已經在用了

        free(ptr3);//再去嘗試free掉這一塊空間的時候,會導緻程式崩潰

        //4.釋放時傳入的位址和申請時的位址不同

        int* ptr4 = (int*)malloc(sizeof(int) * 10);

        assert(ptr4);

        ptr4[0] = 0;

        ptr4++;

        DoSomething();

        free(ptr4);

        //此處的ptr4是原先的ptr4++了,這個時候它在free掉10個int類型的空間,最後一個int不是申請出來的,導緻程式崩潰

}

C++中動态記憶體管理

C++中使用new和delete來進行動态記憶體管理

void test()

{

        int* ptr1 = new int;//申請一個int大小的空間,但沒有初始化,是以是随機值

        int* ptr2 = new int(3);//申請一個int大小的空間,并初始化為3

        int* ptr3 = new int[3];//申請三個int大小的空間

        delete ptr1;

        delete ptr2;

        delete[] ptr3;

}

new、delete和new[]、delete[]必須要配對使用,不然可能出現記憶體洩漏甚至是程式崩潰的問題。

operator new 和 operator delete(C++語言的标準庫函數)

void *operator new(size_t);     //allocate an object

void *operator delete(void *);    //free an object

void *operator new[](size_t);     //allocate an array

void *operator delete[](void *);    //free an array

注意這些都是個函數!!!并且不是一個重載,都是可以直接使用的

我們通過一個執行個體來看一下new和delete幹了什麼

class A

{

private:

        int _a;

public:

        A(int a)

               :_a(a)

        {}

};

int main()

{

        A* pa = new A(10);

        delete pa;

        return 0;

}

new幹的事

  • 調用operator new标準庫函數,傳入的參數是class A的大小,在上例中是四個位元組(int _a),并且傳回的是配置設定的記憶體的起始位址
  • 上面的配置設定式未初始化的,也是未類型化的,第二步就在這一塊原始的記憶體上對類對象進行初始化,調用的是類對象的構造函數
  • 最後一步就是傳回新配置設定并構造好的對象的指針,這裡就是傳回了pa,pa是一個指向A類型對象的指針

delete幹的事

  • 先調用pa指向對象的析構函數,
  • 再調用operator delete函數來釋放該對象的記憶體,傳入的參數為pa的值,也就是該對象的位址

那麼如何申請和釋放一個數組呢?也就是一塊連續的區間

string *psa = new string[10];

int *pia = new int[10];

在申請一個數組的時候用到了new []這個表達式來完成,第一個數組是string類型的,配置設定了儲存對象的空間後,将調用string類型的預設構造函數依次初始化數組中的每個元素;第二個是申請具有内置類型int的數組,配置設定了存儲10個int類型的空間,但并沒有初始化

釋放的時候用下面的語句

delete [] psa;

delete [] pia;

第一句話對10個string對象分别調用它們的析構函數,然後再釋放掉為對象配置設定的記憶體空間

第二句話因為内置類型沒有析構函數,直接釋放為10個int類型資料配置設定的記憶體空間

那麼在這裡我們如何知道psa指向對象的數組的大小?怎麼知道調用幾次析構函數

C++的做法是在為數組配置設定空間的時候多配置設定了4個位元組大小的空間,專門儲存數組的大小,在delete []時就可以取出這個數

A* pa = new A[3];

C/C++動态記憶體管理

從上圖可以看出最終傳回的是實際指向第一個對象的位址并不是多配置設定的4個位元組大小空間的位址。

這樣delete []在做什麼就很好解釋了

  • 那麼在delete []的時候,我們給operator delete []傳的指針其實并不是第一個對象的位址,而是該位址-4,也就是儲存數組空間大小的位址,這個函數會在這個位址第一個四個位元組中取出數組的大小,并給這些對象調用它們的析構函數
  • 同時,在釋放完數組中的對象的時候,也會釋放掉最開始的四個位元組儲存大小的空間

為什麼new delete和 new []  delete []要配對使用呢?

    介于這兩者的工作原理,是以它們需要配對使用?但并不是不配對使用就一定會出問題的。

int *pia = new int[10];

delete []pia;

上面這樣寫肯定不會出問題,但是把delete []pia換成delete pia呢?

這就涉及到了一個問題。我們知道了new []中要加入4個位元組數組大小的原因,是為了要在delete中調用析構函數的時候需要知道數組的大小。

但是如何我們不用調用析構函數呢(如内置類型,int 類型的數組)?我們在new []的時候就沒有必要多配置設定那4個位元組,delete []時不用執行第一步先調用對象的析構函數,直接跳到第二部釋放掉為int數組配置設定的空間。如果這裡使用detele pia ,那麼将調用operator delete函數而不是operator delete []函數,傳入的參數就是配置設定給數組的起始空間,所做的事情就是釋放掉這一塊記憶體。

這裡使用new []建立對象并能用delete來釋放對象的前提是:對象的類型是内置類型或者是無自定義的析構函數的類類型。

如果我們是帶有自定義析構函數的類類型,用new []建立,并用delete析構會發生什麼

A *pia = new A[10];

delete pia;

那麼delete pia做了兩件事

  • 調用一次pia指向的對象析構函數
  • 調用operator delete(pia)釋放記憶體。

出現的問題

  • 這裡隻對數組的第一個類對象調用了析構函數。這樣釋放的時候,後面兩個對象就沒有進行析構,會造成記憶體洩漏。
  • 并且!直接釋放pia指向記憶體的空間,程式必然會崩潰。因為配置設定的空間的起始位址是第一個對象的位址減去4個位元組。但是傳給operator delete的其實是第一個對象的位址。

new使用delete []為什麼會失敗

       new的時候不會像new []一樣多開辟4個位元組來存儲大小,delete時,會将傳入的指針減去4個位元組的位址傳給operator delete [],這樣編譯器在進行釋放記憶體的時候,基本上會導緻記憶體越界而失敗

配置設定方式 删除方式 結果
new delete 成功
new delete [] 失敗
new [] delete  内嵌類型和沒構造函數的自定義類型成功;自定義類型失敗
new [] delete [] 成功

總結operator new和operator delete

  • operator new/operator delete operator new[]/operator delete[] 和 malloc/free用法一 樣。
  • 他們隻負責配置設定空間/釋放空間,不會調用對象構造函數/析構函數來初始化/清理對象。 
  • 實際operator new和operator delete隻是malloc和free的一層封裝。
C/C++動态記憶體管理

new_handler機制

        在new中的底層實作如果擷取不到更多的記憶體,會觸發new_handler機制,留有一個set_new_handler句柄,看看使用者是否設定了這個句柄,如果設定了就去執行。句柄的目的是看看能不能嘗試着從作業系統釋放點記憶體,找點記憶體,如果實在不行就抛出bad_allloc異常;而malloc就沒有這種類似的嘗試

void out_of_memory()

{

        cout << "out of memory!" << endl;

}

int main()

{

        //使用者自己設定的一個set_new_handler句柄

        //在超出記憶體的時候,會調用set_new_handler中的函數out_of_memory

        //并看看能不能從作業系統中釋放一些記憶體來使用

        //不能則抛出bad_alloc異常

        set_new_handler(out_of_memory);

        int *p = new int[536870911];

        return 0;

}

placement new(定位new表達式,使用前需要包含<new>頭檔案)

C++中new有三種用法

  • 建立一個對象
  • 建立一個對象數組。
  • 定位new 表達式
  • 前面二個我們之前已經講過了,下面就來看一看定位new表達式。placement new表達式,在使用時需要我們傳入一個指針,此時會在該指針指向的記憶體空間構造對象,該指針指向的位址可以是堆,可以是棧,也可以是靜态存儲區。說白了,這個操作符的作用就是建立對象,但是不配置設定記憶體,而是在已有的記憶體塊上建立對象。它經常用于需要反複建立和删除的對象,可以降低配置設定釋放記憶體的性能消耗。

class A

{

public:

        A(int a = 0)

               :_a(a)

        {}

private:

        int _a;

};

int main()

{

        // 預配置設定記憶體buf   

        char *buf = new char[sizeof(A) * 10];

        // 在buf中建立一個Foo對象  

        A *pa = new (buf) A;

        delete[] buf;

        return 0;

}

另外需要注意的一點是,并不存在與定位new表達式比對的定位delete表達式,即在上面的代碼中,并不需要 delete pa,因為實際上定位new表達式并沒有配置設定新的空間,而是用的已有的空間,是以不需要釋放,隻需要釋放已有的空間就可以了

關于malloc/free和new/delete之間的差別和聯系

  • 它們都是動态管理記憶體的入口
  • malloc/free是C/C++标準庫的函數,new/delete是C++操作符
  • malloc是從堆上開辟空間,而new是從自由存儲區開辟。(自由存儲區是C++抽象出來的概念,不僅可以是堆,還可以是靜态存儲區)
  • malloc/free隻是動态配置設定記憶體空間/釋放空間。而new/delete除了配置設定空間還會調用構造函數和析構函數進行初始化與清理
  • new可以通過new []來開辟一個連續的存儲空間,即開辟一個數組,而malloc隻是開辟一個固定大小的空間。​
  • malloc/free需要手動設定開辟空間的大小,new隻需要對象名,可以自己計算大小
  • malloc的傳回值是(void *),在使用時需要強轉,調用失敗則傳回NULL;new的傳回值是傳回對象的指針,調用失敗則抛出異常
  • malloc和free不會調用對象的構造和析構函數,而new和delete會調用對象的構造和析構函數​
  • new是類型安全的,而malloc不是,比如:
    • int* p = new float[2]; // 編譯時指出錯誤
    • int* p = malloc(2*sizeof(float)); // 編譯時無法指出錯誤
  • malloc開辟的記憶體如果太小,想要換一塊大一點的,可以調用relloc實作,但是new沒有直覺的方法來改變
  • 我們可以重載自己的operator new和operator delete,但是不可以重載new/delete/malloc/free​
  • new在記憶體不足時有new_handler機制來嘗試申請更多的記憶體,如果實在不行才會抛出異常,而malloc則沒有這種機制

繼續閱讀