?作者主頁
??lovewold少個r博客主頁
? ???本文重點:c++內存管理部分知識點梳理
??【C-C++入門系列專欄】:博客文章專欄傳送門
??每日一言:花有重開日,人無再少年!
目錄
C/C++的內存分配機制
內存分區(qū)
1. 內核空間(Kernel Space):
2. ??臻g(Stack):
3. 內存映射段(Memory Mapping Segment):
4. 堆(Heap):
5. 數據段(Data Segment):
6. 代碼段(Code Segment):
C與C++的動態(tài)內存管理方法
malloc,calloc,realloc的內存開辟函數
內存泄露??
C++的內存管理方式
new/delete操作內置類型
?new的基本用法
delete的基本用法
malloc/free和new/delete的區(qū)別
operator new與operator delete函數
重載operator new和operator delete(了解)
new和delete的底層原理
? ? ? ?new的底層原理:
? ? ? ?delete的底層原理:
定位new表達式
總結
前言
? ? ? ? 在c語言的學習過程中,我們學習了c語言的動態(tài)管理內容。本章的內容主要是從C/C++的內存分配區(qū)域入手,深入淺出的講解C++的內存分配機制和new與delete。
C/C++的內存分配機制
內存分區(qū)
1. 內核空間(Kernel Space):
? ?位置: 在內存的頂部,通常是最高的地址空間。
? ?用途:內核空間用于存放操作系統的內核代碼和數據結構。這部分內存通常是受保護的,只有內核態(tài)的代碼可以訪問。
2. ??臻g(Stack):
? ?棧是向下增長的:對于棧來說,棧內存的地址是從高地址向低地址增長的。新的棧幀會被放置在當前棧頂的上方。棧幀的釋放會導致棧頂向低地址移動
????位置:通常位于用戶空間和內核空間之間。
? ? 用途:棧用于存儲函數的局部變量、函數參數、返回地址和函數調用的上下文信息。棧是一個后進先出(LIFO)的數據結構。
? ? 自動分配和釋放:棧內存是由系統自動分配和釋放的,函數調用時分配,函數返回時釋放。
3. 內存映射段(Memory Mapping Segment):
? ? 位置:通常位于用戶空間和內核空間之間。
? ? 用途:內存映射段包括可執(zhí)行文件的映射、共享庫的映射以及動態(tài)鏈接器的映射。這些映射將磁盤上的二進制文件映射到內存中,以便執(zhí)行。
4. 堆(Heap):
? ?堆是向上增長:堆的內存地址是由低地址向高地址增長的,新分配的內存位于已經分配的內存的上方。
? ?位置:通常位于內核空間和棧空間之間。
? ?用途:堆用于動態(tài)分配內存,程序員可以通過函數如 malloc、calloc、realloc 來在堆上分配內存。堆上的內存需要手動釋放,否則可能導致內存泄漏。
? ?動態(tài)大?。?/strong>堆的大小不是固定的,可以根據需要動態(tài)分配和釋放。
5. 數據段(Data Segment):
? ?位置:通常位于內核空間和??臻g之間。
? ?用途:數據段包括已初始化的全局變量和靜態(tài)變量。這些變量在程序啟動時就被初始化,并在整個程序的執(zhí)行周期內保持不變。
6. 代碼段(Code Segment):
? ?位置:通常位于內核空間和??臻g之間。
? ?用途:代碼段包含程序的機器代碼,即可執(zhí)行指令。這部分內存是只讀的,用于存儲程序的執(zhí)行代碼。
只讀數據段中,包含了兩種常量:
1. 字符串常量:
? ?例如,C/C++中的字符串字面值(如"Hello, World!")。
? ?這些字符串常量存儲在只讀數據段中,因為它們在程序執(zhí)行期間是不可修改的。
2. 其他常量:
? ?例如,C/C++中的全局常量(使用 const 關鍵字聲明的常量)。
? ?這些常量也通常存儲在只讀數據段中。
C與C++的動態(tài)內存管理方法
malloc,calloc,realloc的內存開辟函數
malloc, calloc, 和 realloc 是在C語言中用于分配內存的三個常見函數。
malloc(Memory Allocation):
示例:
void Test() { int* p1 = (int*)malloc(sizeof(int)); if (p1 == NULL) { perror("malloc"); exit(EXIT_FAILURE); } free(p1); }
原型:void* malloc(size_t size);
功能:用于動態(tài)分配指定大小的內存塊。
參數:size 表示要分配的字節(jié)數。
返回值:如果分配成功,返回指向分配內存的指針;如果失敗,返回 NULL。
calloc(Contiguous Allocation):
示例:
#include<stdlib.h> void Test() { int* p1 = (int*)calloc(4,sizeof(int)); if (p1 == NULL) { perror("calloc"); exit(EXIT_FAILURE); } free(p1); }
原型:void *calloc(size_t num_elements, size_t element_size);
功能:用于動態(tài)分配指定數量和大小的內存塊,并將每個字節(jié)初始化為零。
參數:num_elements表示要分配的元素數量,element_size表示每個元素的大小(字節(jié)數)。
返回值:如果分配成功,返回指向分配內存的指針;如果失敗,返回 NULL。
realloc(Re-allocation):
示例:
#include<stdlib.h> void Test() { int* p1 = (int*)calloc(4,sizeof(int)); if (p1 == NULL) { perror("calloc"); exit(EXIT_FAILURE); } int* p2 = (int*)calloc(p1, sizeof(int)*10); if (p1 == NULL) { perror("malloc"); exit(EXIT_FAILURE); } free(p2); }
原型:void *realloc(void *ptr, size_t new_size);
功能:用于更改先前分配的內存塊的大小。
參數:ptr是之前由 malloc, calloc?或 realloc 返回的指針;new_size 是新的內存塊大小。
返回值:如果分配成功,返回指向重新分配內存的指針;如果失敗,返回 NULL。如果 ptr?為 NULL,則其行為等同于 malloc(new_size)。
????????以上函數主要要從函數功能進行區(qū)分,要注意的是無論是哪一種函數開辟或者擴容的空間,后面都需要手動free進行釋放。同時對于realloc我們擴容的空間釋放只需要釋放后者,他的作用是根據需要重新分配內存,可能會將現有的內存塊的內容移動到新的位置,然后釋放原來的內存塊。如果realloc內部決定在現有塊的末尾或相鄰位置分配新的內存,它將返回原始塊的指針,而不是分配新的塊。因此,如果新的內存塊被分配在了原來的內存塊上,釋放原來的內存塊也就釋放了新的內存塊,因為它們實際上是同一塊內存。對于用戶來說,只需保留realloc返回的指針即可,而不需要顯式釋放原來的內存塊,需要避免多次釋放。
內存泄露??
????????內存泄漏是指在程序運行時,動態(tài)分配的內存沒有被釋放,導致系統無法再次使用這些內存塊。當一個程序在運行過程中分配了內存,但在不再需要這些內存時沒有釋放,就會發(fā)生內存泄漏。這對一個操作系統或者服務器的話影響是非常巨大的。
????????內存泄漏可能導致程序運行時占用的內存不斷增加,最終耗盡系統的可用內存,從而導致程序性能下降或崩潰。內存泄漏是一種常見的編程錯誤,因此在開發(fā)過程中應該特別注意及時釋放不再需要的內存。就比如我們經常使用一個軟件的時候,長時間運轉我們會發(fā)現程序消耗的內存越來越大,手機或者電腦越來越卡頓,這可能就存在內存泄露的問題,內存不能及時釋放,只能新開辟空間用于后續(xù)進程,反復疊加下對性能的消耗會越來越大。合理的掌握內存分配和管理,能極大的提高程序運行的效率進而提高用戶使用的體驗。
????????因此無論我們是使用方式的什么開辟的空間,動態(tài)內存開辟的空間需要程序員自己去掌控,要及時的free開辟的空間。
C++的內存管理方式
????????在C++中,由于C++向下兼容,雖然你仍然可以使用malloc和free,但是推薦使用new和delete。首先我們知道,C++是一門面向對象的語言,和C面向過程不同,我們要體現C++的優(yōu)越性就一定要對底層問題的處理上進行封裝調用。
? ? ? ? malloc很顯然能完成對于內存開辟問題,但是在C++中,我們通常是構建一個自定義的類,而構建一個類后我們需要完成對類的初始化。在使用malloc的時候我們能清晰的感覺到,首先對void*的返回值我們要進行強制類型轉換,并且還需要傳遞空間內存大小完成開辟的工作。然后我們還需要手動調用構造函數進行構造么,這也太麻煩了。
????????因此對于這些地方使用極為不便和無能無力我們就需要使用新的操作方式。
new/delete
????????在C++中,new和delete是用于動態(tài)內存管理操作符,它們分別用于在堆上分配和釋放內存。這兩個操作符提供了對動態(tài)內存的靈活控制,特別適用于需要在運行時確定內存大小的情況。
?new的基本用法
????????new用于在堆上動態(tài)分配內存,并返回指向新分配內存的指針。它同時會調用對象的構造函數(如果有的話),對新分配的內存進行初始化。(一個對象就輕松被new出來咯!?。?
void Test()
{
//動態(tài)申請一個int類型的空間
int* ptr1 = new int;
//動態(tài)申請一個int類型的空間并初始化為10
int* ptr2 = new int(10);
//動態(tài)申請10個int類型的空間
int* ptr3 = new int[10];
}
delete的基本用法
delete用于釋放通過new分配的內存。在釋放內存之前,delete會調用對象的析構函數(如果有的話),對內存中的對象進行清理工作。?(一個對象的終點)
void Test()
{
//動態(tài)申請一個int類型的空間
int* ptr1 = new int;
//動態(tài)申請一個int類型的空間并初始化為10
int* ptr2 = new int(10);
//動態(tài)申請10個int類型的空間
int* ptr3 = new int[10];
delete ptr1;//釋放單個對象的空間
delete ptr2;
delete[] ptr3;//釋放數組空間
}
注意:要時刻注意一一對應
- 對于每個new,應該有一個相應的delete,對于每個new[],應該有一個相應的delete[]。
- 不要混合使用new和delete與malloc和free,也不要混合使用new[]和delete[]與malloc和free。
- 在使用new和new[]時,應該使用相應的delete和delete[]來釋放內存,以確保正確調用析構函數。
malloc/free和new/delete的區(qū)別
????????我們知道,malloc并不會去調用構造函數,free也不會去調用析構函數。對于自定義類型的空間處理上new和delete不僅能完成空間的開辟還能完成調用構造函數和析構函數。對于自定義類來講顯然new和delete更甚一籌,而對于非自定義類型的時候,兩者并無太大區(qū)別,不過new和delete在使用方式上也相對更加簡略了。
class A { public: A(int date = 0) { _date = date; printf("a() "); printf("_date=%d\n", _date); } ~A() { printf("~a()\n"); } private: int _date; }; int main() { printf("____________a__________________\n"); A* a = new A; delete a; printf("____________b__________________\n"); A* b = new A(10); delete b; printf("____________c__________________\n"); A* c = new A[10]; delete[] c; printf("____________d__________________\n"); A* d = new A[5]{ 1,2,3,4 }; delete[] d; return 0; }
在內存管理上來講當分配內存失敗的時候兩者也有區(qū)別
- 在分配內存失敗時,malloc返回 NULL,需要手動檢查。
- new運算符在分配內存失敗時拋出異常,可以通過異常處理機制來處理。
operator new與operator delete函數
? ? ?在 C++ 中,new是一個關鍵字,它用于動態(tài)分配內存并構造對象,而operator new是與new相關的一個全局函數,用于執(zhí)行實際的內存分配。delete同理。我們這里進一步剖析new和delete的底層原理。
/*
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)
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與operator delete是兩個全局函數,在上述代碼中是其函數的實現原理,我們不難發(fā)現,其函數實際上也是通過malloc來申請空間的,成功申請后就直接返回,否則執(zhí)行用戶提供的空間應對不足的措施,如果用戶繼續(xù)申請就會拋異常。operator最終也是通過調用free來釋放空間的。
????????拋異常是c++中一種在程序執(zhí)行過程中遇到錯誤或異常情況時,通過特殊的語句將程序的控制權傳遞給異常處理機制。這個過程通常稱為"拋出異常"。異常是一種表示程序錯誤或不正常情況的機制。當某個異常條件發(fā)生時,程序會停止當前執(zhí)行路徑,然后查找能夠處理這個異常的代碼塊。如果找到了匹配的異常處理代碼塊,程序會跳轉到該處進行處理。如果找不到對應的處理代碼,程序可能會終止運行。(后面會詳細講解)
????????在main函數中,我們使用try塊來包裹可能拋出異常的代碼,然后使用catch塊來捕獲并處理異常。如果異常被拋出,控制流將跳轉到匹配的catch塊。
????????這里我們演示一下拋異常和捕獲異常:
#include <iostream> using namespace std; int main(void) { char* p2 = nullptr; try { char* p2 = new char[1024u * 1024u * 1024u * 2u - 1]; } catch (const exception& e) { cout << e.what() << endl; } printf("%p\n", p2); return 0; }
????????在上述演示中,我們申請了大面積的內存空間,程序并沒有出現C語言的崩潰現象,而是以拋異常的方式優(yōu)雅的退出并展示了錯誤原因。
重載operator new和operator delete(了解)
????????一般情況下,不對這兩個函數進行重載,但是在一些特定的情況下,我們可以完成一些特殊的請求。比如在申請和釋放空間的時候有一些特殊的需求,打印一些日志信息,幫助用戶檢測是否存在內存泄露的情況之類的場景需求。
void* operator new(std::size_t size)
{
void* ptr = std::malloc(size);
std::cout << "自定義new分配了 " << size << " 字節(jié)的內存,其地址空間為 " << ptr << std::endl;
return ptr;
}
void operator delete(void* ptr) noexcept
{
std::cout << "自定義 delete:釋放了地址為 " << ptr << " 的內存" << std::endl;
std::free(ptr);
}
int main()
{
int* p = new int;
delete p;
int* arr = new int[5];
delete[] arr;
return 0;
}
new和delete的底層原理
new的底層原理:
計算分配的總內存大?。簄ew首先計算要分配的總內存大小,包括對象的大小以及額外的管理信息。
調用operator new分配內存:new調用operator new函數進行實際的內存分配??梢灾剌d它以提供自定義的分配邏輯。
調用對象的構造函數: 如果new用于分配單個對象,它會調用對象的構造函數,完成對象的初始化。
返回分配的內存地址: 最后,new返回指向新分配對象。
delete的底層原理:
調用對象的析構函數: 在使用delete釋放對象之前,會調用對象的析構函數,進行對象資源的清理。
調用operator delete釋放內存:delete調用operator delete函數釋放之前分配的內存??梢灾剌d它以提供自定義的釋放邏輯。
返回釋放的內存: 最終,操作系統可以重新利用被釋放的內存。
數組的原理即是調用相應的operator new[]對多個對象的空間申請和構造,銷毀同理。
定位new表達式
定位new表達式是在一個已經分配了原始的內存空間中顯示的調用構造函數完成對對象的初始化操作。
new (ptr) Type(init);
其中,prt是指向已分配內存的指針,Type是對象的類型,init是可選的初始化參數(可以帶參也可以不帶參)。
這種形式的new主要用于在預分配的內存區(qū)域中創(chuàng)建對象,配合內存池使用,因為內存池分配的內存沒有初始化。而不是在堆上分配新的內存。這對于一些特殊的需求,如實現自定義內存池或在固定地址處構造對象等情況非常有用。
手動調用析構函數: 當你不再需要這個對象時,需要手動調用析構函數來釋放資源
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;
}
總結
? ? ? ? 本章主要從C/C++的不同內存管理機制入手,深入淺出的講解new和delete的底層原理和一些擴展知識。對于文章中涉及到內存池和拋異常等機制我們會在后續(xù)單獨講解,本章點到為止。希望能對大家深入理解new和delete有所幫助!
? ? 作者水平有限,如有錯誤歡迎指正!
文章來源:http://www.zghlxwxcb.cn/news/detail-754565.html
? ??文章來源地址http://www.zghlxwxcb.cn/news/detail-754565.html
到了這里,關于【C++破局】C++內存管理之new與deleted剖析的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!