C++ 動態內存管理詳情解說

寫在前面

我們知道C++是支持C語言的,也就是說,C語言裡面的malloc等函數都可以在C++中使用,但是C++有支持瞭另外兩個關鍵字,這是很有用的,我們需要看看C++的動態內存.

C/C++ 內存分佈

我記得,在初識C語言那裡就和大傢分享瞭程序虛擬地址空間的概念,無論是C語言的nalloc函數,還是我們現在要分享的new,都是在堆區開辟空間,這一點是我們要首先記得的。

C語言內存管理方式

C語言是通過函數來經行動態的內存開辟的,標準庫裡面提供三個函數,這裡我就不加贅述瞭,大傢應該都是知道的。我麼看看用法就可以瞭。

#include <stdio.h>
#include <assert.h>

int main()
{
// malloc 開辟空間 不初始化
int* p1 = (int*)malloc(sizeof(int)* 4);
assert(p1);

//calloc 開辟空間 初始化 為 0
int* p2 = (int*)calloc(4, sizeof(int));
assert(p2);
// 追加 空間
p1 = (int*)relloc(p1, sizeof(int)* 8);

free(p1);
free(p2);
return 0;
}

C++內存管理方式

C++是支持C語言的,也是說C++是可以使用這些函數的,但是除瞭這些函數外,C++有增加瞭new和delete這個兩個關鍵字,分別對標的malloc/calloc和free,而且C++的方式比C的好用.

C++為何增加瞭new 和 delete

我們都知道,C語言的結構體裡面不支持函數,所以大佬們提出瞭類的概念,出現瞭class,又害怕自己有時後可能忘記初始化和清除掉內存,就出現瞭構造函數和析構函數,讓編譯器自動調用,可以說,所有的事物的出現都是為瞭我們更好的使用語言,new和delete也似乎如此,C語言的動態內存開辟是有一定的麻煩的,而且對於自動類型很不友好,後面我們就會比較他們的優劣.

我們還發現一個很直接問題,每一次開辟空間我們都要強制類型轉換,而且還需要判斷內存是不是究竟開出來瞭,這也太麻煩瞭,new卻不會出現這種事,如果沒有開辟出,編譯器會拋異常,我們就不需要再自己手動檢測瞭.

new 一個對象

這樣,我先和大傢演示內置類型,自定義類型那裡我準備專門和malloc比較一下.

#include <iostream>
using namespace std;

int main()
{
int* p1 = new int;
*p1 = 10;
cout << *p1 << endl;
return 0;
}

我們也知道,再C++中,內置類行也被作為類瞭,我們可以再new的時候對它進行初始化.

int main()
{
int* p = new int(0);
cout << *p << endl;

return 0;
}

new 一個數組

new一個數組更是簡單,我們直接寫出來就可以瞭.

int main()
{
int* p = new int[10]; // new 一個 10 個int 類行的空間
return 0;
}

我們也可以在new空間的時候進行實例化,不過要顯示實例化

int main()
{
int* p = new int[10]{1,2,3};
return 0;
}

delete

大傢可能發現,我上面都沒有釋放空間,這會造成內存泄漏,這裡我們用另一個關鍵字delete,這裡就比較簡單瞭.

大傢可能疑惑delete[],這裡我們記住就可以瞭,如果你要清除數組的空間,最好使用這種方式,或許對於內置類行,使用delete也可以,但是對於自定義類行可能會報錯,這裡我也放在後面談.

int main()
{
int* p1 = new int;
int* p2 = new int[10]{1,2,3};
delete p1;
delete[] p2;
return 0;
}

malloc & new

我們需要對比一下malloc和new它們之間的區別,這樣就可以知道C++為何這麼喜歡new瞭.

內置類型

我們先下一個結論,它們兩個對於內置類行除瞭報錯之外是沒有任何區別的,都不會經行初始化,這裡我們現不談報錯的信息,異常和沒有和大傢分享.

int main()
{
int* p1 = new int[10];

int* p2 = (int*)malloc(sizeof(int)* 10);
assert(p2);

delete[] p1;
free(p2);
return 0;
}

自定義類型

對於自定義類型,它們的差別可大瞭去瞭.

我們先來準備一個類:

class A
{
public:
A(int a = 0,int b=0)
:_a(a)
, _b(b)
{
cout << "構造函數" << endl;
}
~A()
{
cout << "析構函數" << endl;
}
private:
int _a;
int _b;
};

malloc是直接開辟空間,對於裡面的構造函數是不會調用的,free的時候也不會調用析構函數

int main()
{
A* aa = (A*)malloc(sizeof(A));
free(aa);
return 0;
}

new 和 delete會分別調用構造函數和析構函數,完成初始化

int main()
{
A* aa = new A;
delete aa;
return 0;
}

operator new與operator delete函數

這裡一看像是new和delete的重載,記住,這不是,就是名字有點奇怪罷瞭.這是C++裡面的全局函數,它的使用方法和malloc一樣,而且作用也是有一樣的,不會調用構造函數和析構函數.

new和delete是用戶進行動態內存申請和釋放的操作符,operator new 和operator delete是系統提供的全局函數,new在底層調用operator new全局函數來申請空間,delete在底層通過operator delete全局函數來釋放空間

int main()
{
A* aa = (A*)operator new(sizeof(A));
operator delete (aa);
return 0;
}

原理

通過源碼我們就會發現,實際上operator new與operator delete函數 本質上是malloc和free的封裝,就是報錯的信息有點不同,封裝的報錯的信息是異常.

operator new 的原理是 malloc

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 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); // 註意 C語言的free 就是這個函數 __FINALLY _munlock(_HEAP_LOCK); /* release other threads */ __END_TRY_FINALLY return; }

為何出現這兩個函數

這兩個函數不是給我們調用的,是為瞭new的底層調用的,我們new一個對象,就相當於call operator new 和 call 對象的構造函數,這才是它們出現的原因.

大傢可以看看反匯編.

delete & delete[]

這個我們可以這麼理解,對於內置類型,它們就沒必要討論的,作用差不多.但是對於自定義類型就有很大的問題.

  • 在釋放的對象空間上執行N次析構函數,完成N個對象中資源的清理
  • 調用operator delete[]釋放空間,實際在operator delete[]中調用operator delete來釋放空間

大傢先看看結果:

delete[] 析構相應的的次數

int main() { A* aa = new A[3]; delete[] aa; return 0; }

delete 析構一次,還會報錯

int main() { A* aa = new A[3]; delete aa; return 0; }

內存池

這裡我想提一個概念,我們都知道malloc和new都是在堆上開辟空間,如果我們要是多次的去開辟空間,效率是不是有點慢,想一想,我們一次開辟一次,開瞭個上千次,每次都要去申請,我們在想,能不能單獨的劃分出一塊區域,專門提供我們想要的對象來開辟空間,這就是內存池最初的想法,大傢可能會感到疑惑,內存池和堆有什麼不同嗎,簡單來說,內存池離你近,可以提高效率。我們可以這麼類比,堆就像每噸你在學校吃飯就和你老爸要錢,每頓都要,那麼內存池就像月初你直接和你爸要好這個月的生活費,一月要一次,肯定是後者的效率比較高的。

那麼我們該如何使用內存池,標準庫裡面也提供瞭一個,這裡我們需要在類內重寫operator new與operator delete函數函數,大傢先來瞭解一下用法就可以瞭,我們先不來細究,後面可能會有一個高並發內存池的項目要和大傢分享,不過這個時間就有點長瞭。

struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _data;

// 申請空間的是後去內存 池
void* operator new(size_t n)
{
void* p = nullptr;
p = allocator<ListNode>().allocate(1);
cout << "memory pool allocate" << endl;
return p;
}
void operator delete(void* p)
{
allocator<ListNode>().deallocate((ListNode*)p, 1);
cout << "memory pool deallocate" << endl;
}
};
class List
{
public:
List()
{
_head = new ListNode;
_head->_next = _head;
_head->_prev = _head;
}
~List()
{
ListNode* cur = _head->_next;
while (cur != _head)
{
ListNode* next = cur->_next;
delete cur;
cur = next;
}
delete _head;
_head = nullptr;
}
private:
ListNode* _head;
};

int main()
{
List l1;
return 0;
}

定位 new

我們已經知道瞭,使用operator new開辟出的空間是不會初始化的,而且現在我們是無法通過對象來顯式調用構造函數的,這也就意味著我們要是向修改成員變量,一定會破壞封裝.但是C++這裡也提供瞭一個定位new的技術可以幫助我們再次實例化,我們先來看看用法.

class A
{
public:
A(int a = 0)
:_a(a)
{
}
private:
int _a;
};

int main()
{
A* a = (A*)operator new(sizeof(A));

// 定位 new
new(a)A (1);
return 0;
}

從這裡我們就可以知道瞭,定位new有下面兩種用法

  • new(要初始化的指針) 指針解引用對應的類行 直接調用默認構造函數
  • new(要初始化的指針) 指針解引用對應的類行 (構造函數要傳的參數) 調用相應的構造函數

到此這篇關於C++ 動態內存管理詳情解說的文章就介紹到這瞭,更多相關C++ 動態內存管理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: