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