首頁 > 軟體

一文詳解C++中動態記憶體管理

2022-07-27 22:01:53

前言

在我們日常寫程式碼的過程中,我們對記憶體空間的需求有時候在程式執行的時候才能知道,這時候我們就需要使用動態開闢記憶體的方法。

1、C/C++程式的記憶體開闢

首先我們先了解一下C/C++程式記憶體分配的幾個區域:

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}

  • 1. 棧區(stack):在執行函數時,函數內區域性變數的儲存單元都可以在棧上建立,函數執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內建於處理器的指令集中,效率很高,但是分配的記憶體容量有限。 棧區主要存放執行函數而分配的區域性變數、函數引數、返回資料、返回地址等。
  • 2. 堆區(heap):一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回收 。分配方式類似於連結串列。
  • 3. 資料段(靜態區)(static)存放全域性變數、靜態資料。程式結束後由系統釋放。
  • 4. 程式碼段:存放函數體(類成員函數和全域性函數)的二進位制程式碼。

這幅圖中,我們可以發現普通的區域性變數是在棧上分配空間的,在棧區中建立的變數出了作用域去就會自動銷燬。但是被static修飾的變數是存放在資料段(靜態區),在資料段上建立的變數直到程式結束才銷燬,所以資料段上的資料生命週期變長了。

2.C語言中動態記憶體管理方式:malloc/calloc/realloc/free

在C語言中,我們經常會用到malloc,calloc和realloc來進行動態的開闢記憶體;同時,C語言還提供了一個函數free,專門用來做動態記憶體的釋放和回收。其中他們三個的區別也是我們需要特別所強調區別的。

2.1malloc、calloc、realloc區別?

malloc函數是向記憶體申請一塊連續可用的空間,並返回指向這塊空間的指標。

calloc與malloc的區別只在於calloc會在返回地址之前把申請的空間的每個位元組初始化為0。

realloc函數可以做到對動態開闢記憶體大小的調整。

我們通過這三個函數的定義也可以進行功能的區分:

void Test ()
{
int* p1 = (int*) malloc(sizeof(int));
free(p1);
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);

free(p3 );
}

3.C++記憶體管理方式

我們都知道,C++語言是相容C語言的,因此C語言中記憶體管理方式在C++中可以繼續使用。但是有些地方就無能為力了,並且使用起來也可能比較麻煩。因此,C++擁有自己的內管管理方式:通過new和delete操作符進行動態記憶體管理。

3.1 new/delete操作內建型別

int main()
{
// 動態申請一個int型別的空間
int* ptr1 = new int;
// 動態申請一個int型別的空間並初始化為10
int* ptr2 = new int(10);
// 動態申請3個int型別的空間(陣列)
int* ptr3 = new int[3];
// 動態申請3個int型別的空間,初始化第一個空間值為1
int* ptr4 = new int[3]{ 1 };
delete ptr1;
delete ptr2;
delete[] ptr3;
delete[] ptr4;
return 0;
}

  我們首先通過畫圖分析進行剖析程式碼:

我們在監視視窗看看這3個變數

注意:申請和釋放單個元素的空間,使用new和delete操作符,申請和釋放連續的空間,使用new[]和delete[],要匹配起來使用。

3.2 new和delete操作自定義型別

class A {
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(1);
free(p1);
delete p2;
return 0;
}

在這段程式碼中,p1是我們使用malloc開闢的,p2是通過new來開闢的。我們編譯執行這段程式碼。

發現輸出了這兩句,那這兩句是誰呼叫的呢?我們通過偵錯逐語句來分析這個過程

內建型別區別

注意:在申請自定義型別的空間時,new會自動呼叫建構函式,delete時會呼叫解構函式,而malloc和free不會。

3.3new和malloc處理失敗

int main()
{
void* p0 = malloc(1024 * 1024 * 1024);
cout << p0 << endl;

//malloc失敗,返回空指標
void* p1 = malloc(1024 * 1024 * 1024);
cout << p1 << endl;
try
{
//new失敗,拋異常
void* p2 = new char[1024 * 1024 * 1024];
cout << p2 << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}

 我們能夠發現,malloc失敗時會返回空指標,而new失敗時,會丟擲異常。

4.operator new與operator delete函數

4.1 operator new與operator delete函數

C++標準庫還提供了operator new和operator delete函數,但是這兩個函數並不是對new和delete的過載,operator new和operator delete是兩個庫函數。(這裡C++大佬設計時這樣取名確實很容易混淆)

4.1.1 我們看看operator new庫裡面的原始碼

void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) {
// try to allocate size bytes
void* p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申請記憶體失敗了,這裡會丟擲bad_alloc 型別異常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}

庫裡面operator new的作用是封裝了malloc,如果malloc失敗,丟擲異常。

4.1.2 operator delete庫裡面的原始碼

該函數最終是通過free來釋放空間的

//operator delete 原始碼
void operator delete(void* pUserData) {
_CrtMemBlockHeader* pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg(pUserData, pHead->nBlockUse);
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}

/*
free的實現
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

4.1.3 operator new和operator delete的價值(重點)

class A {
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
//跟malloc功能一樣,失敗以後丟擲異常
A* ps1 = (A*)operator new(sizeof(A));
operator delete(ps1);

A* ps2 = (A*)malloc(sizeof(A));
free(ps2);
A* ps3 = new A;
delete ps3;
return 0;
}

我們使用new的時候,new要開空間,要呼叫建構函式。new可以轉換成call malloc,call 建構函式。但是call malloc 一旦失敗,會返回空指標或者錯誤碼。在物件導向的語言中更喜歡使用異常。而operator new相比較malloc的不同就在於如果一旦失敗會丟擲異常,因此new的底層實現是呼叫operator new,operator new會呼叫malloc(如果失敗丟擲異常),再呼叫建構函式。

我們通過組合看一下ps3

operator delete同理。

總結:通過上述兩個全域性函數的實現知道,operator new 實際也是通過malloc來申請空間,如果malloc申請空間成功就直接返回,否則執行使用者提供的空間不足應對措施,如果使用者提供該措施就繼續申請,否則就拋異常。operator delete 最終是通過free來釋放空間的

4.2 過載operator new 與 operator delete(瞭解)

專屬的operator new技術,提高效率。應用:記憶體池

class A {
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}

// 專屬的operator new
void* operator new(size_t n)
{
void* p = nullptr;
p = allocator<A>().allocate(1);
cout << "memory pool allocate" << endl;
return p;
}
void operator delete(void* p)
{
allocator<A>().deallocate((A*)p, 1);
cout << "memory pool deallocate" << endl;

}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
int n = 0;
cin >> n;
for (int i = 0; i < n; ++i)
{
A* ps1 = new A; //operator new + A的建構函式
}
return 0;
}

注意:一般情況下不需要對 operator new 和 operator delete進行過載,除非在申請和釋放空間時候有某些特殊的需求。比如:在使用new和delete申請和釋放空間時,列印一些紀錄檔資訊,可以簡單幫助使用者來檢測是否存在記憶體漏失。

5.new 和 delete 的實現原理

5.1 內建型別

如果申請的是內建型別的空間,new和malloc,delete和free基本類似,不同的地方是:new/delete申請和釋放的是單個元素的空間,new[]和delete[]申請的是連續空間,而且new在申請空間失敗時會拋異常,malloc會返回NULL。

5.2 自定義型別

5.2.1 new原理

  • 1、呼叫operator new函數申請空間
  • 2、再呼叫建構函式,完成對物件的構造。

5.2.2 delete原理

  • 1、先呼叫解構函式,完成對物件中資源的清理工作。
  • 2、呼叫operator delete函數釋放物件的空間

5.2.3 new T[N]原理

  • 1、先呼叫operator new[]函數,在operator new[]中世紀呼叫operator new函數完成N個物件空間的申請
  • 2、在申請的空間上執行N次建構函式

5.2.4 delete[]原理

  • 1、在釋放的物件空間上執行N次解構函式,完成對N個物件中資源的清理
  • 2、呼叫operator delete[]釋放空間,實際在operator delete[]中呼叫operator delete來釋放空間。

6.malloc/free和new/delete的異同

6.1malloc/free和new/delete的共同點

都是從堆上申請空間,都需要使用者手動釋放空間。

6.2malloc/free和new/delete的不同點

  • 1:malloc和free是函數,new和delete是操作符
  • 2:malloc申請的空間不會初始化,new可以初始化
  • 3:malloc申請空間時,需要手動計算空間大小並傳遞,new只需在其後跟上空間的型別即可,如果是多個物件,[]中指定物件個數即可
  • 4:malloc的返回值為void*, 在使用時必須強轉,new不需要,因為new後跟的是空間的型別
  • 5:malloc申請空間失敗時,返回的是NULL,因此使用時必須判空,new不需要,但是new需要捕獲異常
  • 6:申請自定義型別物件時,malloc/free只會開闢空間,不會呼叫建構函式與解構函式,而new在申請空間後會呼叫建構函式完成物件的初始化,delete在釋放空間前會呼叫解構函式完成空間中資源的清理

到此這篇關於一文詳解C++中動態記憶體管理的文章就介紹到這了,更多相關C++動態記憶體管理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com