內(nèi)存管理)
前言本文圍繞 C/C 內(nèi)存管理核心知識點從分區(qū)、分配機制、實戰(zhàn)對比到避坑技巧幫開發(fā)者系統(tǒng)性掌握精髓規(guī)避內(nèi)存泄漏等常見問題。一、C/C 內(nèi)存分布1.1 介紹在 C 語言階段我們常說局部變量存儲在棧區(qū)動態(tài)內(nèi)存中的數(shù)據(jù)存儲在堆區(qū)靜態(tài)變量存儲在靜態(tài)區(qū)常量存儲在常量區(qū)其實這里我們所說的棧區(qū)、堆區(qū)、靜態(tài)區(qū)以及常量區(qū)都是虛擬進程地址空間的一部分其中具體內(nèi)存區(qū)域的劃分如下棧又叫堆棧 —— 非靜態(tài)局部變量/函數(shù)參數(shù)/返回值等等棧是向下增長的先使用高地址的內(nèi)存。內(nèi)存映射段是高效的 I/O 映射方式用于裝載一個共享的動態(tài)內(nèi)存庫。用戶可使用系統(tǒng)接口創(chuàng)建共享內(nèi)存做進程間通信?,F(xiàn)在只需要了解一下后續(xù) Linux 會講到堆用于程序運行時動態(tài)內(nèi)存分配堆是可以向上增長的先使用低地址的內(nèi)存。數(shù)據(jù)段 —— 存儲全局?jǐn)?shù)據(jù)和靜態(tài)數(shù)據(jù)。代碼段 —— 可執(zhí)行的代碼邏輯代碼/只讀常量。注虛擬進程地址空間、多進程多線程以及操作系統(tǒng)內(nèi)核等相關(guān)知識我們會在 Linux 中進行學(xué)習(xí)這里我們只需要學(xué)習(xí) C/C 的內(nèi)存區(qū)域劃分即可。1.2 題目在學(xué)習(xí)了 C/C 內(nèi)存區(qū)域的劃分后我們來做幾道題鞏固一下intglobalVar1;staticintstaticGlobalVar1;voidTest(){staticintstaticVar1;intlocalVar1;intnum1[10]{1,2,3,4};charchar2[]abcd;constchar*pChar3abcd;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);}選項:A.棧 B.堆 C.數(shù)據(jù)段(靜態(tài)區(qū)) D.代碼段(常量區(qū))globalVar 在哪里CstaticGlobalVar 在哪里CstaticVar 在哪里ClocalVar 在哪里Anum1 在哪里A【分析】:globalVar 全局變量在數(shù)據(jù)段staticGlobalVar 靜態(tài)全局變量在靜態(tài)區(qū)staticVar 靜態(tài)局部變量在靜態(tài)區(qū)localVar 局部變量在棧區(qū)num1 局部變量在棧區(qū)char2 在哪里A*char2 在哪里ApChar3 在哪里A*pChar3 在哪里Dptr1 在哪里A*ptr1 在哪里B【分析】:char2 局部變量在棧區(qū)char2 是一個數(shù)組把后面常量串拷貝過來到數(shù)組中數(shù)組在棧上所以 *char2 在棧上pChar3 局部變量在棧區(qū)*pChar3 得到的是字符串常量字符在代碼段ptr1 局部變量在棧區(qū)*ptr1 得到的是動態(tài)申請空間的數(shù)據(jù)在堆區(qū)填空sizeof(num1)40; // 數(shù)組大小10 個整型數(shù)據(jù)一共 40 字節(jié)sizeof(char2)5; // 包括 \0 的空間strlen(char2)4; // 不包括 \0 的長度sizeof(pChar3)4; // pChar3 為指針strlen(pChar3)4; // 字符串 “abcd” 的長度不包括 \0 的長度sizeof(ptr1)4; // ptr1 是指針簡答題malloc/calloc/realloc的區(qū)別malloc用于開辟一塊動態(tài)內(nèi)存使用時需要指定開辟的空間大小(字節(jié))如果開辟成功返回空間的起始地址如果開辟失敗返回NULL且不會初始化calloc的用法和malloc類似只是它有兩個參數(shù)第一個參數(shù)為元素個數(shù)第二個參數(shù)為每個元素的大小并且它會將該空間中的數(shù)據(jù)全部初始化為 0realloc用于空間的擴容/縮容它有兩個參數(shù)第一個參數(shù)為需要調(diào)整的動態(tài)內(nèi)存的起始地址第二個參數(shù)為調(diào)整后的空間大小如果第一個參數(shù)為NULL則它等價于malloc如果擴容編譯器會檢查原空間后是否有足夠的空間如果足夠就直接擴容并返回原空間的起始地址如果不夠就新開辟一塊空間然后將原空間的數(shù)據(jù)拷貝到新空間并返回新空間的地址最后再釋放原空間如果縮容編譯器會直接新開辟一塊空間然后拷貝原空間數(shù)據(jù)到新空間并返回新空間的地址再釋放原空間。這里需要free(p2)嗎voidTest(){int*p2(int*)calloc(4,sizeof(int));int*p3(int*)realloc(p2,sizeof(int)*10);free(p3);}不需要free(p2)無論是異地擴容還是原地擴容最終都不需要專門釋放p2如果是原地擴容p2和p3指向同一塊空間如果是異地的話將原內(nèi)容拷貝后會自動釋放原來p2指向的空間棧與堆對比特性棧區(qū) (Stack)堆區(qū) (Heap)管理方式編譯器自動分配釋放程序員手動通過new/delete等接口管理空間大小較小 (幾 MB)較大 (受系統(tǒng)內(nèi)存限制)訪問速度極快 (棧指針操作)較慢 (需內(nèi)存尋址)生命周期隨作用域自動銷毀顯式釋放前持續(xù)存在內(nèi)存碎片無頻繁分配釋放易產(chǎn)生碎片增長方向向下增長向上增長典型場景局部變量、函數(shù)參數(shù)大對象、跨作用域數(shù)據(jù)風(fēng)險點棧溢出 (過大局部變量)內(nèi)存泄漏未釋放、野指針二、C 動態(tài)內(nèi)存管理方式2.1 引入C 兼容 C 語言所以 C 語言的內(nèi)存管理方式在 C 中可以繼續(xù)使用但由于其使用起來比較麻煩且有些地方無能為力因此 C 又提出了自己的內(nèi)存管理方式通過new和delete操作符進行動態(tài)內(nèi)存管理。2.2 C/C 的使用1. C 語言malloc/calloc/realloc與freemalloc(size_t size)分配指定字節(jié)數(shù)的堆內(nèi)存返回void *指針需手動類型轉(zhuǎn)換。calloc(size_t num, size_t size)分配num個大小為size的連續(xù)內(nèi)存初始化為 0。realloc(void* ptr, size_t size)調(diào)整已分配內(nèi)存的大小可能會移動內(nèi)存塊。free(void* ptr)釋放通過上述函數(shù)分配的堆內(nèi)存ptr必須指向堆內(nèi)存起始地址。#includestdio.h#includestdlib.hintmain(){// 1. malloc分配內(nèi)存不初始化int*m(int*)malloc(2*sizeof(int));m[0]1;m[1]2;printf(malloc: %d, %d\n,m[0],m[1]);// 2. calloc分配并初始化為0int*c(int*)calloc(2,sizeof(int));printf(calloc: %d, %d\n,c[0],c[1]);// 3. realloc擴容malloc的內(nèi)存m(int*)realloc(m,3*sizeof(int));m[2]3;printf(realloc后: %d, %d, %d\n,m[0],m[1],m[2]);// 4. free釋放內(nèi)存free(m);free(c);return0;}2. Cnew/delete與new[]/delete[]voidTest(){//申請單個空間不初始化int*p1newint;//申請單個空間并初始化int*p2newint(10);//申請連續(xù)空間不初始化int*p3newint[10];//申請連續(xù)空間并初始化int*p4newint[10]{1,2,3,4,5};//釋放單個空間deletep1;deletep2;//釋放多個空間delete[]p3;delete[]p4;}注C 不支持?jǐn)U容要擴容都是自己開辟新空間、拷貝數(shù)據(jù)然后再銷毀原空間。三、區(qū)別3.1 new/delete 操作內(nèi)置類型對于內(nèi)置類型C 語言和 C 內(nèi)存管理方式?jīng)]有明顯區(qū)別只是 C 中使用new操作符來替代 C 語言中的malloc/calloc函數(shù)使用delete操作符來替代free函數(shù)同時由于new和delete是操作符/關(guān)鍵字而不是函數(shù)所以它們后面不需要跟括號而是直接跟類型即可另外new可以在開辟空間的同時進行初始化。3.2 new/delete 操作自定義類型C 動態(tài)內(nèi)存管理和 C 語言動態(tài)內(nèi)存管理最大的不同在于二者對自定義類型的處理C 語言malloc/calloc/realloc函數(shù)只負(fù)責(zé)開辟空間free函數(shù)只負(fù)責(zé)銷毀空間而 C 在申請自定義類型的空間時new會調(diào)用構(gòu)造函數(shù)delete會調(diào)用析構(gòu)函數(shù)。classA{public:A(inta0):_a(a){coutA 構(gòu)造thisendl;}~A(){coutA 析構(gòu)thisendl;}private:int_a;};intmain(){A*p1newA(1);deletep1;coutendl;A*p2newA[2];//生成2個A對象delete[]p2;return0;}newT分配單個 T 類型對象的內(nèi)存自動調(diào)用構(gòu)造函數(shù)初始化。newT[n]分配 n 個 T 類型對象的數(shù)組內(nèi)存自動調(diào)用每個對象的構(gòu)造函數(shù)。deleteptr自動調(diào)用析構(gòu)函數(shù)釋放單個對象內(nèi)存。delete[]ptr自動調(diào)用每個對象的析構(gòu)函數(shù)釋放數(shù)組內(nèi)存。注意接口必須成對使用new對應(yīng)deletenew[]對應(yīng)delete[]混用會導(dǎo)致內(nèi)存泄漏或程序崩潰。無需手動類型轉(zhuǎn)換new返回對應(yīng)類型指針且分配失敗時拋出bad_alloc異常而非返回NULL。四、深入 new 與 delete4.1 引入 operator new 和 operator delete在 C 中new和delete是用戶進行動態(tài)內(nèi)存申請和釋放的操作符operator new和operator delete是系統(tǒng)提供的全局函數(shù)new在底層調(diào)用operator new全局函數(shù)來申請空間delete在底層通過operator delete全局函數(shù)來釋放空間。需要特別注意的是operator new和operator delete函數(shù)不是運算符重載因為它們的參數(shù)沒有自定義類型而是庫里面實現(xiàn)的全局函數(shù)僅僅是將它們?nèi)∶麨閛perator而已很多 C 的初學(xué)者都會被二者的函數(shù)名所誤導(dǎo)。4.2 源碼區(qū)別C 底層的operator new和operator delete函數(shù)如下// operator new// 該函數(shù)實際通過malloc來申請空間當(dāng)malloc申請空間成功時直接返回// 申請空間失敗嘗試執(zhí)行空間不足應(yīng)對措施如果用戶設(shè)置了應(yīng)對措施則繼續(xù)申請否則拋異常。void*__CRTDECLoperatornew(size_t size)_THROW1(_STD bad_alloc){// try to allocate size bytesvoid*p;while((pmalloc(size))0)if(_callnewh(size)0){// report no memory// 如果申請內(nèi)存失敗了這里會拋出bad_alloc 類型異常staticconststd::bad_alloc nomem;_RAISE(nomem);}return(p);}// operator delete:// 該函數(shù)最終是通過free來釋放空間的voidoperatordelete(void*pUserData){_CrtMemBlockHeader*pHead;RTCCALLBACK(_RTC_Free_hook,(pUserData,0));if(pUserDataNULL)return;_mlock(_HEAP_LOCK);// block other threads__TRY// get a pointer to memory block headerpHeadpHdr(pUserData);// verify block type_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead-nBlockUse));_free_dbg(pUserData,pHead-nBlockUse);//此處調(diào)用free函數(shù)__FINALLY_munlock(_HEAP_LOCK);// release other threads__END_TRY_FINALLYreturn;}// free的實現(xiàn)#definefree(p)_free_dbg(p,_NORMAL_BLOCK)通過上述的實驗我們知道operator new實際也是通過malloc來申請空間如果malloc申請空間成功就直接返回否則執(zhí)行用戶提供的空間不足應(yīng)對措施如果用戶提供了該措施就繼續(xù)申請否則就拋異常。operator delete最終是通過free來釋放空間的。4.3 匯編區(qū)別【new底層匯編】從圖中可以看到是先調(diào)用operator new再調(diào)用相對應(yīng)的構(gòu)造函數(shù)【delete底層匯編】4.4 深入 deletedelete的底層是先調(diào)用析構(gòu)函數(shù)然后再調(diào)用operator delete為什么會這樣呢我們先了解一下下面這個【模擬棧對象如何在堆上申請/釋放空間】先為棧對象開空間然后調(diào)用該對象的構(gòu)造函數(shù)為_array開辟空間最終讓棧上的 p1 指針指向堆上的對象棧對象和棧內(nèi)存區(qū)域是兩個不同領(lǐng)域的概念釋放空間的話要先調(diào)用析構(gòu)函數(shù)如果你先調(diào)用operator delete了_array就成野指針了就會造成內(nèi)存泄露五、定位 new 表達式 (了解)5.1 定位 new 的用法定位new是一種 “在已分配的原始內(nèi)存中調(diào)用構(gòu)造函數(shù)” 的特殊用法主要配合內(nèi)存池使用內(nèi)存池分配的內(nèi)存未初始化需手動調(diào)用構(gòu)造函數(shù)new(place_address)type 或者new(place_address)type(initializer-list)其中place_address必須是一個指針initializer-list是類型的初始化列表如下classA{public:A(inta0):_a(a){coutA():thisendl;}~A(){cout~A():thisendl;}private:int_a;};intmain(){// p1現(xiàn)在指向的只不過是與A對象相同大小的一段空間還不能算是一個對象因為構(gòu)造函數(shù)沒有執(zhí)行A*pa(A*)malloc(sizeof(A));//定位new--對pa指向的空間顯式調(diào)用構(gòu)造函數(shù)new(pa)A(1);pa-~A();//析構(gòu)函數(shù)可以直接顯式調(diào)用或者直接使用deletefree(pa);}5.2 使用場景定位new表達式在實際中一般是配合內(nèi)存池使用 —— 因為內(nèi)存池分配出的內(nèi)存沒有初始化所以如果是自定義類型的對象需要使用new的定義表達式進行顯式調(diào)用構(gòu)造函數(shù)進行初始化而內(nèi)存池在后面我們會詳細(xì)學(xué)習(xí)此處我們了解一下即可。簡單理解一下內(nèi)存池假設(shè)半山腰有一個村子但由于各種原因村子中沒有水喝所以人們每次喝水都只能到山下的公共水井處排隊打水但是呢排隊很慢所以村長就用抽水機水管聯(lián)通水井在自己家建了一個蓄水池以后要用水就直接到蓄水池中去取即可而不用再到山下去排隊打水了大大提高了效率。上述例子中全村公用的水井就相當(dāng)于堆其他村民排隊打水就相當(dāng)于malloc/calloc/realloc函數(shù)向堆區(qū)申請空間而村長家的蓄水池就相當(dāng)于我們的主角 —— 內(nèi)存池內(nèi)存池的建立可以使得我們申請空間的效率變得很高。六、總結(jié)問題malloc/free和new/delete的區(qū)別malloc/freenew/delete本質(zhì)函數(shù)操作符內(nèi)置類型初始化不會初始化可初始化如new int(5)自定義類型只開辟空間不調(diào)用構(gòu)造/析構(gòu)先開辟空間再調(diào)用構(gòu)造函數(shù)釋放前先調(diào)用析構(gòu)函數(shù)大小指定手動計算字節(jié)數(shù)并傳入自動推導(dǎo)類型大小多個對象用new T[N]返回值void*必須強轉(zhuǎn)返回對應(yīng)類型指針無需強轉(zhuǎn)失敗處理返回NULL需手動判空拋出std::bad_alloc需捕獲異常什么是內(nèi)存泄漏內(nèi)存泄漏指因為疏忽或錯誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存的情況內(nèi)存泄漏并不是指內(nèi)存在物理上的消失而是應(yīng)用程序分配某段內(nèi)存后因為設(shè)計錯誤失去了對該段內(nèi)存的控制因而造成了內(nèi)存的浪費。內(nèi)存泄漏的危害短期運行的程序發(fā)生內(nèi)存泄漏危害不大因為當(dāng)程序結(jié)束時動態(tài)申請的空間全部都會被回收長期運行的程序出現(xiàn)內(nèi)存泄漏影響很大如操作系統(tǒng)、后臺服務(wù)等等出現(xiàn)內(nèi)存泄漏會導(dǎo)致響應(yīng)越來越慢最終卡死。如何避免內(nèi)存泄漏?工程前期良好的設(shè)計規(guī)范養(yǎng)成良好的編碼規(guī)范申請的內(nèi)存空間記著匹配的去釋放。ps這個理想狀態(tài)。但是如果碰上異常時就算注意釋放了還是可能會出問題。需要下一條智能指針來管理才有保證。采用 RAII 思想或者智能指針來管理資源。有些公司內(nèi)部規(guī)范使用內(nèi)部實現(xiàn)的私有內(nèi)存管理庫。這套庫自帶內(nèi)存泄漏檢測的功能選項。出問題了使用內(nèi)存泄漏工具檢測。ps不過很多工具都不夠靠譜或者收費昂貴。