C/C++內(nèi)存管理之new與delete的使用及原理解析
一、C/C++中程序內(nèi)存區(qū)域劃分
內(nèi)存區(qū)域相關(guān)作用:
- 棧又叫堆棧:非靜態(tài)局部變量、函數(shù)參數(shù)、返回值等等,棧是向下增長的
- 內(nèi)存映射段時(shí)高效的I/O映射方式,用于裝載一個(gè)共享的動(dòng)態(tài)內(nèi)存庫,用戶可以使用系統(tǒng)接口創(chuàng)建共享共享內(nèi)存,做進(jìn)程間通信
- 堆用于程序運(yùn)行時(shí)動(dòng)態(tài)內(nèi)存分配,堆時(shí)可以上增長的
- 數(shù)據(jù)段:存儲(chǔ)全局?jǐn)?shù)據(jù)和靜態(tài)數(shù)據(jù)
- 代碼段:可執(zhí)行的代碼、只讀常量
在語法上將數(shù)據(jù)段稱為靜態(tài)區(qū)、代碼段稱為常量區(qū),而以上操作系統(tǒng)的命名。
提出相關(guān)思考:
- 為什么要分不同的區(qū)域
- 哪個(gè)區(qū)域是我們需要重點(diǎn)關(guān)注的
回答:
- 根據(jù)對(duì)象不同的生命周期和作用域,分配到不同的區(qū)域中,統(tǒng)一管理,高效地對(duì)對(duì)象進(jìn)行處理
- 堆是我們要需要重點(diǎn)關(guān)注的,這是系統(tǒng)留給我們控制的內(nèi)存,其他系統(tǒng)是自動(dòng)的
1.1 相關(guān)練習(xí)測試
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); }
答案:
- 選擇題:C、C、C、A、A 。A、A、A、D、A、B
- 填空題:40、5、4、4/8、4、4/8
這里容易混洗的char str1[] ="abcd"
與const char* str2 ="abcd"
。這里str1
是個(gè)數(shù)組將常量拷貝到數(shù)組,而str2
是直接指向常量區(qū)中常量。
二、C語言中動(dòng)態(tài)內(nèi)存管理方式
C語言中,系統(tǒng)通過一系列函數(shù)賦予了我們對(duì)堆上空間的控制
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 ); }
提出思考:
- malloc/calloc/realloc的區(qū)別是什么?
- 這里使用realloc是否還需要free(p2)
- malloc的實(shí)現(xiàn)原理?
第一個(gè)問題的回答:
對(duì)于malloc/calloc/realloc
是系統(tǒng)為我們提供在堆上申請(qǐng)空間的途徑。在功能上大體是相同的,對(duì)于malloc
與calloc
這兩個(gè)函數(shù),除了參數(shù)部分及其是否完成初始化,其他功能是相同的。
relloc
比較特別,屬于擴(kuò)容時(shí)使用的函數(shù)。擴(kuò)容有兩種方式:原地?cái)U(kuò)容和異地?cái)U(kuò)容。如果realloc
第一個(gè)參數(shù)部分為空,可以當(dāng)作malloc
使用)。具體還是參考下這篇博客有詳細(xì)解釋內(nèi)存管理
第二個(gè)問題的回答:
由于realloc
進(jìn)行了擴(kuò)容操作。如果是原地?cái)U(kuò)容,在原來開辟空間上完成擴(kuò)容操作,這里p3會(huì)同p2指向這塊空間,只需要free(p3)
;如果是異地?cái)U(kuò)容,將p2空間中數(shù)據(jù)拷貝一份,在堆上找一塊空間充足地方,完成擴(kuò)容和拷貝操作,p2指向原空間,會(huì)被系統(tǒng)自動(dòng)收回,不需要對(duì)p2進(jìn)行free操作。對(duì)此無論是原地還是異地,只需要free(p3)即可
第三個(gè)問題的回答:
可以通過該鏈接進(jìn)行學(xué)習(xí)GLibc堆利用入門
三、C++內(nèi)存管理方式
在C++中,雖然可以繼續(xù)使用C語言對(duì)于內(nèi)存管理方式,但是在有些地方就無能為力,而且使用起來比較麻煩。對(duì)此因此C++又提出了自己的內(nèi)存管理方式:通過new和delete操作符進(jìn)行動(dòng)態(tài)內(nèi)存管理
3.1 使用new/delete進(jìn)行數(shù)據(jù)操作
3.1.1 new/delete 操作內(nèi)置類型
int main() { //動(dòng)態(tài)申請(qǐng)一個(gè)int類型的空間 int* ptr1 = new int; //動(dòng)態(tài)申請(qǐng)一個(gè)int類型的空間并且初始化為10 int* ptr2 = new int(10); //動(dòng)態(tài)申請(qǐng)10個(gè)int類型的空間 int* ptr3 = new int[3]; //動(dòng)態(tài)申請(qǐng)10個(gè)int類型的空間并且完成初始化 int* ptr4 = new int[10]{ 1,2,3 };//剩下沒有明確給值,默認(rèn)為0 delete ptr1; delete ptr2; delete []ptr3; delete[]ptr4; return 0; }
注意需要匹配使用new和delete操作符:
- 申請(qǐng)和釋放單個(gè)元素的空間:new、delete
- 申請(qǐng)和釋放多個(gè)元素的空間:new[]、delete[]
3.1.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; //內(nèi)置類型 int* p3 = (int*)malloc(sizeof(int)); int* p4 = new int; free(p3); delete p4; //開辟連續(xù)自定義類型空間 A* p5 = (A*)malloc(sizeof(A) * 10); A* p6 = new A[10]; free(p5); delete[] p6; return 0; }
從結(jié)果上來看,對(duì)于new
與malloc
最大差別在于對(duì)自定義類型除了開辟空間以外,還會(huì)調(diào)用構(gòu)造函數(shù)和析構(gòu)函數(shù)及其進(jìn)行良好的初始化和控制。對(duì)于malloc
而言無法對(duì)自定義類型進(jìn)行好的初始化和控制,只負(fù)責(zé)開辟內(nèi)存,除此之外內(nèi)置類型幾乎相同(初始化不同)
對(duì)于new優(yōu)于malloc的幾點(diǎn):
- 用法上進(jìn)行調(diào)正,更簡潔好用
- 可以控制初始化
- 對(duì)于自定義類型,new可以開空間+構(gòu)造函數(shù)
- new配合構(gòu)造函數(shù),可以更加便捷創(chuàng)建節(jié)點(diǎn)等
- new失敗了以后拋異常,不需要手動(dòng)檢查
第一點(diǎn):
int* p0 = (int*)malloc(sizeof(int)); int* p1 = new int;
第二點(diǎn):
int* p2 = new int[10]; int* p3 = new int(10); int* p4 = new int[10]{ 1,2,3 };
第三點(diǎn):
struct ListNode { ListNode* _next; int _val; ListNode(int val) :_val(val) ,_next(nullptr) {} }; //創(chuàng)建不帶哨兵位,同時(shí)如果是插入數(shù)據(jù),new ListNode(3)即可 ListNode* CreateList(int n) { ListNode head(-1); ListNode* tail = &head; int val; printf("請(qǐng)依次輸入%d個(gè)節(jié)點(diǎn)的值:>", n); for (size_t i = 0; i < n; i++) { cin >> val; tail->_next = new ListNode(val); tail = tail->_next; } return head._next; }
第四點(diǎn):
void func() { int n = 1; while (1) { int* p = new int[1024 * 1024*100]; cout <<n<<"->"<< p << endl; ++n; } } int main() { try { func(); } catch (const exception& e) { cout << e.what() << endl; } return 0; }
這里try和catch就是捕捉異常,這一點(diǎn)到后面有涉及。以上種種都是new的優(yōu)點(diǎn),所以我們不推薦再使用malloc/free
系列。
四、 new和delete原理及其兩個(gè)全局函數(shù)的實(shí)現(xiàn)(operator new/operator delerte)
new和delete是用戶進(jìn)行動(dòng)態(tài)內(nèi)存申請(qǐng)和釋放的操作符,operator new和operator delete是系統(tǒng)提供的全局函數(shù),new再底層調(diào)用operator new全局函數(shù)來申請(qǐng)空間,delete在底層通過operator delete全局函數(shù)來釋放空間(operator new與operator delete不是對(duì)new和delete的重載)
int* p1 = (int*)operator new(10 * 4); //int* p1=new int(10*4) operator delete(p1); //delete(p1)
從代碼中可以看出來,new/delete和operator new/operator delete效果上是相同的。那么直接使用new/delete就行,operator new/operator delete對(duì)于我們來說是沒用的,但是有這個(gè)東西說明在系統(tǒng)中有它們的一席之地的。
/* operator new:該函數(shù)實(shí)際通過malloc來申請(qǐng)空間,當(dāng)malloc申請(qǐng)空間成功時(shí)直接返回;申請(qǐng)空間 失敗,嘗試執(zhí)行空 間不足應(yīng)對(duì)措施,如果改應(yīng)對(duì)措施用戶設(shè)置了,則繼續(xù)申請(qǐng),否則拋異常。 */ void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // try to allocate size bytes void *p; while ((p = malloc(size)) == 0) //通過上述兩個(gè)全局函數(shù)的實(shí)現(xiàn)知道,operator new 實(shí)際也是通過malloc來申請(qǐng)空間,如果 //malloc申請(qǐng)空間成功就直接返回,否則執(zhí)行用戶提供的空間不足應(yīng)對(duì)措施,如果用戶提供該措施 //就繼續(xù)申請(qǐng),否則就拋異常。operator delete 最終是通過free來釋放空間的。 if (_callnewh(size) == 0) { // report no memory // 如果申請(qǐng)內(nèi)存失敗了,這里會(huì)拋出bad_alloc 類型異常 static const std::bad_alloc nomem; _RAISE(nomem); } return (p); } /* operator delete: 該函數(shù)最終是通過free來釋放空間的 */ 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的實(shí)現(xiàn) */ #define free(p) _free_dbg(p, _NORMAL_BLOCK)
可以通過上述兩個(gè)全局函數(shù)的實(shí)現(xiàn),可以知道,operator new實(shí)際是通過malloc來申請(qǐng)空間,如果malloc申請(qǐng)空間成功就直接返回,否則執(zhí)行用戶提供的空間不足應(yīng)對(duì)措施;如果用戶提供該措施就繼續(xù)申請(qǐng),否則就拋異常。operator delete最終是通過free來釋放空間的。
內(nèi)置類型:
如果申請(qǐng)的是內(nèi)置類型的空間,new/malloc與delete/free基本類似,不同的地方是new在申請(qǐng)空間失敗時(shí)會(huì)拋異常,malloc會(huì)返回NULL
自定義類型:
new的原理:
- 調(diào)用operator new函數(shù)申請(qǐng)空間
- 在申請(qǐng)的空間上執(zhí)行構(gòu)造函數(shù),完成對(duì)象的構(gòu)造
delete的原理:
- 在空間上執(zhí)行析構(gòu)函數(shù),完成對(duì)象中資源的清理工作
- 調(diào)用operator delete含函數(shù)釋放對(duì)象的空間
new T[N]的原理:
- 調(diào)用operator new[]函數(shù),在operator new[]中實(shí)際調(diào)用operator new函數(shù)完成N個(gè)對(duì)象空間的申請(qǐng)
- 在申請(qǐng)的空間上執(zhí)行N次構(gòu)造函數(shù)
delete[]的原理:
- 在釋放的對(duì)象空間上執(zhí)行N次析構(gòu)函數(shù),完成N個(gè)對(duì)象中資源的清理
- 調(diào)用operator delete[]釋放空間,實(shí)際在operator delete[]中調(diào)用operator delete來釋放空間
通過匯編,深入立即其中
對(duì)于自定義類型轉(zhuǎn)換指令只有兩個(gè)核心動(dòng)作調(diào)用全局函數(shù)及其構(gòu)造或析構(gòu),而內(nèi)置類型只有調(diào)用全局函數(shù)。
對(duì)此可得:
- operator new是對(duì)malloc的封裝,如果失敗拋異常,實(shí)現(xiàn)new
- operator newp[]封裝operator new,最終還是malloc
- operator delete對(duì)free的封裝
- operator delete[]封裝operator delete
同時(shí)這里需要注意調(diào)用順序上的問題
五、深入了解new和delete工作原理
new是個(gè)操作符,在編譯時(shí)new A會(huì)轉(zhuǎn)化為匯編指令調(diào)用malloc,一般來說malloc失敗會(huì)返回空,由于C++是面向?qū)ο蟮倪^程,malloc失敗返回空是不太合適,一般采用拋異常。全局函數(shù)operator new來封裝malloc,去調(diào)正失敗的返回情況。
int main() { A* p1 = new A;//operator new+1次構(gòu)造 A* p2 = new A[10];//operatorn new[]+10構(gòu)造 int* p3=new int[10];//operator new[](占用40個(gè)字節(jié)) delete p1;//1次析構(gòu)+operator delete delete[] p2;//?次析構(gòu)+operator delete delete[] p3;//operator delete return 0; }
結(jié)合匯編和代碼提供的信息,提出以下問題:
- 編譯器如何開始確定所需開辟空間大小
- 為什么p2指向大小為44字節(jié)空間,而不是40字節(jié)空間
- 為什么編譯器知道p2需要調(diào)用10次析構(gòu)函數(shù)
回答:
- 由于new屬于操作符,在編譯時(shí)就計(jì)算出了所需空間的大小。
- 編譯器在所開辟空間位置前面,也是調(diào)用operator new函數(shù)多開四個(gè)字節(jié),用于記錄對(duì)象個(gè)數(shù)(針對(duì)自定義類型)
- 由于內(nèi)置類型不需要調(diào)用析構(gòu)函數(shù),對(duì)此不需要記錄對(duì)象個(gè)數(shù),而自定義類型需要記錄對(duì)象個(gè)數(shù)。delete[]需要通過對(duì)象個(gè)數(shù)才知道調(diào)用多少次析構(gòu)函數(shù)。如果將析構(gòu)函數(shù)注釋,p2占用空間為40字節(jié)。由于編譯器會(huì)自動(dòng)生成析構(gòu)函數(shù),而該析構(gòu)函數(shù)沒有發(fā)揮占用,編譯器會(huì)優(yōu)化導(dǎo)致不需要四個(gè)字節(jié)記錄對(duì)象個(gè)數(shù),具體需要看編譯器是否優(yōu)化
六、malloc/free系列和new/delete系列的區(qū)別
我們將通過用法和底層特性兩點(diǎn)說明:
共同點(diǎn):
- 都是從堆上申請(qǐng)空間,并且需要用戶手動(dòng)釋放
不同點(diǎn):
- malloc和free屬于函數(shù),new和delete屬于操作符
- malloc申請(qǐng)的空間不會(huì)初始化,new可以初始化
- malloc申請(qǐng)空間時(shí),需要手動(dòng)計(jì)算空間并且傳遞,new只需在其后跟上空間的類型即可,如果是多個(gè)對(duì)象,[]中指定對(duì)象個(gè)數(shù)即可
- malloc的返回值為void*,在使用事必須強(qiáng)轉(zhuǎn),new不需要,因?yàn)閚ew后跟的是空間的類型
- malloc申請(qǐng)空間失敗時(shí),返回的是NULL,因此使用時(shí)必須判空,new不需要的,但是new需要捕獲異常
- 申請(qǐng)自定義類型對(duì)象時(shí),malloc/free只會(huì)開辟空間,不會(huì)調(diào)用構(gòu)造函數(shù)與析構(gòu)函數(shù),而new在申請(qǐng)空間后會(huì)調(diào)用構(gòu)造函數(shù)完成對(duì)象的初始化,delete在釋放空間前會(huì)調(diào)用析構(gòu)函數(shù)完成空間中資源的清理
七、delete最好匹配使用
解析說明:圖中delete沒有匹配使用,導(dǎo)致可能報(bào)錯(cuò)。這里p2指向并不是申請(qǐng)空間的第一個(gè)位置,第一個(gè)位置是operator new[]實(shí)現(xiàn)存放對(duì)象個(gè)數(shù)申請(qǐng)的空間。由于空間是不能一塊塊釋放,對(duì)此p2釋放的位置是錯(cuò)誤的,并且不明確需要調(diào)用多少次析構(gòu)函數(shù),可能會(huì)造成內(nèi)存泄漏。如果是delete[] p2,會(huì)將p2指針偏移前面四個(gè)字節(jié)。
但是以上種種情況,導(dǎo)致這個(gè)問題是否報(bào)錯(cuò),具體需要看編譯器是否進(jìn)行優(yōu)化(編譯器是否調(diào)用析構(gòu)函數(shù)),對(duì)此我們只需要正確的使用delete就行,上面只是了解就行了
八、定位new表達(dá)式(placement -new)(了解)
定位new表達(dá)式時(shí)在已分配的原始內(nèi)存空間中調(diào)用構(gòu)造函數(shù)初始化一個(gè)對(duì)象。
new(指針->空間)類型() :顯式調(diào)用構(gòu)造函數(shù)對(duì)已經(jīng)有的空間初始化
構(gòu)造函數(shù)不能顯式調(diào)用,析構(gòu)可以顯式調(diào)用(一般不會(huì)去調(diào)用兩次析構(gòu)的)
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; } / 定位new/replacement new int main() { // p1現(xiàn)在指向的只不過是與A對(duì)象相同大小的一段空間,還不能算是一個(gè)對(duì)象,因?yàn)闃?gòu)造函數(shù)沒 有執(zhí)行 A* p1 = (A*)malloc(sizeof(A)); new(p1)A; // 注意:如果A類的構(gòu)造函數(shù)有參數(shù)時(shí),此處需要傳參 p1->~A(); free(p1); A* p2 = (A*)operator new(sizeof(A)); new(p2)A(10); p2->~A(); operator delete(p2); return 0; }
一般沒有人會(huì)使用,因?yàn)檫@里就是把new分成兩部分,那么干嘛不直接使用new更加方便。
使用場景:定位new表達(dá)式在實(shí)際中,一般是配合內(nèi)存池使用。因?yàn)閮?nèi)存池分配出的內(nèi)存沒有初始化。如果是自定義類型的對(duì)象,需要使用new的定義表達(dá)式進(jìn)行顯式調(diào)構(gòu)造函數(shù)進(jìn)行初始化。
C++基本放棄了malloc/free系列。關(guān)于realloc擴(kuò)容解決措施,在C++相關(guān)容器中它們會(huì)自動(dòng)處理內(nèi)存的擴(kuò)容,使得開發(fā)者可以更加方便地使用動(dòng)態(tài)大小的數(shù)據(jù)集合。
九、內(nèi)存泄漏(了解)
9.1 內(nèi)存泄漏概念
內(nèi)存泄漏指因?yàn)槭韬龌蛘咤e(cuò)誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存的情況。內(nèi)存泄漏并不是指內(nèi)存在物理上的消失,而是應(yīng)用程序分配某段內(nèi)存后,因?yàn)樵O(shè)計(jì)錯(cuò)誤,失去了對(duì)該段內(nèi)存的控制,因而造成了內(nèi)存的浪費(fèi)
9.2 內(nèi)存泄漏的危害
長期運(yùn)行的程序出現(xiàn)內(nèi)存泄漏,影響很大,如操作系統(tǒng)、后臺(tái)服務(wù)等等,出現(xiàn)內(nèi)存泄漏會(huì)導(dǎo)致響應(yīng)越來越慢,最終卡死。
9.3 內(nèi)存泄漏分類
C/C++程序中一般我們關(guān)心兩種方面的內(nèi)存泄漏
1.堆內(nèi)存泄漏(Heap leak)
堆內(nèi)存指的是程序執(zhí)行種依據(jù)須要分配通過malloc/calloc/realloc/new等從堆中分配的一塊內(nèi)存,用完后必須通過調(diào)用相應(yīng)的free或者delete刪除。假設(shè)程序的設(shè)計(jì)錯(cuò)誤導(dǎo)致這部分內(nèi)存沒有被釋放,那么以后這部分空間將無法再被使用,就會(huì)產(chǎn)生Heap Leak。
2.系統(tǒng)資源泄漏
指程序使用系統(tǒng)分配的資源,比方套接字,文件描述符,管道等沒有使用對(duì)應(yīng)的函數(shù)釋放掉,導(dǎo)致系統(tǒng)資源的浪費(fèi),嚴(yán)重可導(dǎo)致系統(tǒng)效能減少,系統(tǒng)執(zhí)行不穩(wěn)定。
到此這篇關(guān)于C/C++內(nèi)存管理:new與delete的使用及原理的文章就介紹到這了,更多相關(guān)C++內(nèi)存管理new與delete內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
簡單比較C語言中的execl()函數(shù)與execlp()函數(shù)
這篇文章主要介紹了C語言中的execl()函數(shù)與execlp()函數(shù)的簡單比較,是C語言入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-08-08visual studio code 配置C++開發(fā)環(huán)境的教程詳解 (windows 開發(fā)環(huán)境)
這篇文章主要介紹了 windows 開發(fā)環(huán)境下visual studio code 配置C++開發(fā)環(huán)境的圖文教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03