當delete遭遇“失憶”:內存釋放謎團全解析
在C++的編程世界里,內存管理是開發者們繞不開的關鍵話題。當我們使用delete關鍵字來釋放內存時,一個有趣的問題出現了:delete在執行釋放操作時,它并不知道所操作的內存大小,那它究竟是如何準確無誤地完成內存釋放任務的呢?這就好比我們要清理一個倉庫,但卻不知道倉庫的大小,該從何下手呢?
對于C++開發者來說,內存管理的重要性不言而喻。正確地分配和釋放內存,是保障程序穩定運行、避免內存泄漏和提高性能的關鍵。如果把程序比作一座大廈,那么內存就是大廈的基石,而內存管理則是建造和維護這座大廈的關鍵技術。稍有不慎,就可能導致大廈搖搖欲墜,程序出現各種難以調試的問題。所以,深入探究delete釋放內存的機制,就顯得尤為重要。接下來,就讓我們一起揭開它神秘的面紗吧!
一、內存管理基礎概念
1.1程序內存區域劃分
在深入剖析delete關鍵字的內存釋放機制之前,我們有必要先理清 C++ 程序運行時的內存區域劃分邏輯 —— 這是理解內存管理的核心前提。通常情況下,一個正在執行的 C++ 程序,其內存空間會被系統性地劃分為以下幾個關鍵區域:
圖片
- 棧區(Stack):由編譯器自動分配和釋放,主要存放函數的參數值、局部變量等。它的特點是內存分配和釋放效率高,就像一個先進后出的棧結構。比如我們在函數內部定義一個局部變量int num = 10;,這個num變量就存放在棧區,當函數執行結束,這個變量所占用的棧內存會被自動釋放。棧區的大小在編譯時就已經確定,并且相對較小,如果在棧區分配過大的內存,可能會導致棧溢出錯誤。
- 堆區(Heap):一般由程序員手動分配和釋放,用于存放動態分配的對象和數據。與棧區不同,堆區的內存分配和釋放比較靈活,但也容易出現內存泄漏等問題。例如我們使用new關鍵字來分配內存,int* p = new int;,這里分配的內存就在堆區。堆區的內存大小在程序運行時可以動態調整,但由于其分配和釋放需要程序員手動管理,所以如果不小心忘記釋放不再使用的內存,就會導致內存泄漏,使程序占用的內存越來越多,最終影響系統性能。
- 數據段(Data Segment):用于存放已初始化的全局變量和靜態變量。數據段又可以細分為已初始化數據段和未初始化數據段(BSS 段)。已初始化數據段存放那些在程序中已經被賦予初始值的全局變量和靜態變量,比如int globalVar = 10;,這個globalVar就存放在已初始化數據段;而未初始化數據段則存放那些聲明了但未初始化的全局變量和靜態變量,像int uninitGlobalVar; 。數據段的內存是在程序加載時由系統分配,程序結束時由系統釋放。
- 代碼段(Code Segment):存放函數體的二進制代碼,也就是程序執行的指令。代碼段通常是只讀的,以防止程序在運行過程中意外修改自身的代碼。在代碼段中,也可能包含一些只讀的常量,比如字符串常量"Hello, World!"。當多個進程運行相同的程序時,它們可以共享同一個代碼段,這樣可以節省內存空間 。
在這些內存區域中,堆內存是我們使用delete操作的主要對象,由于它需要開發者手動管理,所以也最容易出現內存釋放相關的問題。了解了內存區域的劃分,我們就可以更好地理解delete在內存管理中的角色和作用。
1.2C++ 內存管理方式
在 C++ 中,內存管理主要有兩種方式:C 語言風格的malloc/free和 C++ 特有的new/delete。
malloc和free是 C 語言標準庫提供的函數,用于動態分配和釋放內存。malloc函數用于從堆中分配指定大小的內存塊,它返回一個指向分配內存起始地址的指針,并且不會對分配的內存進行初始化。例如:
int* p = (int*)malloc(sizeof(int));
if (p != nullptr) {
*p = 10; // 手動賦值
}使用完內存后,需要調用free函數來釋放內存:
free(p);
p = nullptr; // 防止野指針如果忘記調用free釋放內存,就會導致內存泄漏。
而new和delete是 C++ 的運算符,它們不僅負責內存的分配和釋放,還會自動調用對象的構造函數和析構函數。對于自定義類型,這一點尤為重要。比如我們有一個自定義類MyClass:
class MyClass {
public:
MyClass() {
// 構造函數,初始化資源
std::cout << "MyClass constructor" << std::endl;
}
~MyClass() {
// 析構函數,釋放資源
std::cout << "MyClass destructor" << std::endl;
}
};使用new來創建對象:
MyClass* obj = new MyClass;這里new會先分配足夠的內存,然后調用MyClass的構造函數來初始化對象。當我們不再需要這個對象時,使用delete來釋放內存:
delete obj;delete會先調用MyClass的析構函數來釋放對象占用的資源,然后再釋放內存。
對比malloc/free和new/delete,可以發現new/delete在處理自定義類型時更加安全和方便,因為它能自動管理對象的生命周期。但需要注意的是,new和delete必須配對使用,如果使用new分配內存,卻使用free釋放,或者反之,都會導致未定義行為,可能引發程序崩潰或其他難以調試的問題。同時,在使用new[]分配數組內存時,必須使用delete[]來釋放,以確保數組中每個元素的析構函數都能被正確調用。
1.3堆(Heap)的分類與管理
堆,作為內存管理中的重要組成部分,就像是一個巨大的物資儲備庫,它為程序提供了動態分配內存的區域。在 Glibc 堆內存管理中,堆主要分為兩類,即主 Arena 的堆和子 Arena 的堆,它們各自有著獨特的特點和管理方式。
lang=c
/* 該數據結構只在子Arena中使用,用于記錄當前堆信息。 */
typedef struct _heap_info
{
mstate ar_ptr; /* Arena for this heap. */ // 指向該堆所在的Arena
struct _heap_info *prev; /* Previous heap. */ //由于一個子Arena管理多個堆,因此
size_t size; /* Current size in bytes. */ //當前堆分配給用戶使用的大小,剩余部分為預留區域
size_t mprotect_size; /* Size in bytes that has been mprotected
PROT_READ|PROT_WRITE. */ //從代碼來看,和size并無區別(本人意見)
/* Make sure the following data is properly aligned, particularly
that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
MALLOC_ALIGNMENT. */
char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK]; //用于對齊
} heap_info;主 Arena 的堆是通過brk系統調用從操作系統獲取內存,它只有一個,就像一個大型的中央倉庫,位于進程地址空間的特定區域,從低地址向高地址增長。主 Arena 的堆在初始化時,其大小通常是一個較小的值,但隨著程序運行過程中對內存的不斷需求,它可以通過brk系統調用動態擴展。例如,當一個程序需要分配更多內存時,brk系統調用會將堆的末尾地址移動到所需內存塊的末尾地址,從而為程序提供新的內存空間。這就好比中央倉庫在物資不足時,可以通過擴建來增加存儲容量。主 Arena 的堆在內存管理中承擔著重要的角色,它是許多小型內存分配的主要來源,由于其內存分配和釋放的操作相對頻繁,因此需要高效的管理機制來確保內存的合理使用。
子 Arena 的堆則是通過mmap系統調用創建的,與主 Arena 的堆不同,子 Arena 的堆可以有多個,并且這些堆之間通過鏈表進行連接。這就像是多個分散的小型倉庫,每個倉庫都有自己獨立的管理方式。子 Arena 的堆通常用于滿足一些特殊的內存分配需求,或者在多線程環境下,為不同的線程提供獨立的內存分配空間,以減少線程之間的內存競爭。當一個子 Arena 的堆空間用盡時,會申請新的堆,并將其加入到鏈表中,就像小型倉庫物資不足時,會新建倉庫并與原有倉庫連接起來。
①堆的申請
第一類的堆無需申請,只是調用brk進行堆邊界的拓展即可。這里主要對第二類堆的申請進行說明。
- 堆的大小和對齊:第二類堆在申請時,總是mmap大小為HEAP_MAX_SIZE的內存,多出來的部分將作為預留空間,防止頻繁申請。并且使其首地址對齊于HEAP_MAX_SIZE,這可以方便找到堆的起始地址。
- 什么時候申請堆:在兩種情況會進行第二類堆的申請,第一種情況是在創建子Arena時,會相應地進行堆的申請作為該Arena的第一個堆;第二種情況是在原來申請的堆已經分配完畢時,會重新進行堆的申請,并將該堆和原來的堆通過鏈表連接起來。
- 堆的可用部分:只將用戶所需要的部分分配出去,并使用size記錄,剩下的部分作為預留。
②堆的釋放
這里堆的釋放是指glibc將申請的堆內存歸還給內核。
對于第一類堆,可以認為只有堆大小的縮減,當堆的頂部空閑的內存滿足一定條件時,可以通過brk將堆的邊界下移,top chunk指向地址不變,但大小變小了。
對于第二類堆,當一個堆中的內存已經完全被釋放時,就會將該該堆通過munmap歸還給內核,同時將top chunk重新指向上一個堆內的可用內存地址。
可以這么理解,堆由兩部分組成,一部分是已經分配出去的內存,另一部分是預留的內存(top,因為它總是存在于地址最高部分),而已經分配出去的內存一部分由free釋放,成為了空閑內存(內存碎片),由此除預留部分部分之外,分為兩種內存,空閑內存和已使用內存。
無論是主 Arena 的堆還是子 Arena 的堆,在內存的申請、釋放與管理過程中,都遵循著一定的機制。當程序通過malloc函數申請內存時,堆管理器會首先在堆中查找合適的空閑內存塊。如果找到大小合適的空閑內存塊,就會將其分配給程序使用,并將該內存塊標記為已分配狀態;如果沒有找到合適的空閑內存塊,堆管理器會根據情況從操作系統申請更多的內存,或者對已有的內存塊進行合并和整理,以滿足程序的內存需求。
而當程序通過free函數釋放內存時,堆管理器會將釋放的內存塊標記為空閑狀態,并嘗試將其與相鄰的空閑內存塊進行合并,以減少內存碎片化,提高內存利用率。這就像是在倉庫中,當需要領取物資時,會先查找倉庫中是否有合適的物資,若沒有則會申請新的物資;當歸還物資時,會將物資放回倉庫,并整理倉庫,使物資擺放更加整齊。
1.4堆內存管理的分配
glib中堆內存分配的基本思路就是,首先找到本線程的Arena,然后優先在Arena對應的回收箱中尋找合適大小的內存,在內存箱中所有內存塊均小于所需求的大小,那么就會去top chunk分割,但是如果top chunk的大小也不足夠,此時不一定要拓展top,檢查所需的內存是否大于128k,若大于,則直接使用系統調用mmap分配內存,如果小于,就進行top chunk的拓展,即堆的拓展,拓展完成后,從top chunk中分配內存,剩余部分成為新的top chunk。
(1)malloc函數
malloc 函數是 C 語言標準庫中用于動態內存分配的核心函數,其函數原型為:void* malloc(size_t size);。在這個原型中,size參數表示需要分配的內存塊的字節數,它是一個無符號整數類型(size_t),這意味著我們可以根據實際需求,精確地指定所需內存的大小。
malloc 函數的主要功能就是從堆內存中分配一塊指定大小的連續內存空間,并返回一個指向該內存塊起始地址的指針。這個返回的指針類型是void*,也就是無類型指針。這是因為 malloc 函數在分配內存時,并不知道這塊內存將來會被用于存儲什么類型的數據,所以它返回一個通用的無類型指針,需要我們在使用時將其強制轉換為實際所需的數據類型指針。例如,如果我們需要分配一塊內存來存儲整數,就需要將 malloc 返回的指針轉換為int*類型;如果要存儲字符,就轉換為char*類型。
當程序調用 malloc 函數請求分配內存時,其背后的分配機制涉及到操作系統與程序之間的協同工作。操作系統為了有效地管理堆內存,通常會維護一個空閑內存鏈表,這個鏈表就像是一個記錄著所有空閑 “房間”(內存塊)的清單。鏈表中的每個節點都代表著一塊空閑的內存區域,節點中包含了該內存塊的大小、前后指針等信息,以便操作系統能夠快速地查找和管理這些空閑內存。
當 malloc 函數被調用時,操作系統會按照一定的算法,通常是首次適應算法、最佳適應算法或最差適應算法等,開始遍歷這個空閑內存鏈表。以首次適應算法為例,操作系統會從鏈表的頭部開始,依次檢查每個空閑內存塊,尋找第一個大小大于或等于所需分配大小size的內存塊。一旦找到這樣的內存塊,操作系統就會將其從空閑鏈表中移除,并根據需要對該內存塊進行分割。如果找到的空閑內存塊比請求的size大,那么操作系統會將多余的部分重新插入到空閑鏈表中,以便后續的內存分配請求使用。而分割出來的正好滿足size大小的內存塊,就會被標記為已分配,并返回其起始地址給程序,這個地址就是 malloc 函數的返回值。通過這樣的方式,malloc 函數能夠在堆內存中靈活地為程序分配所需的內存空間,以滿足各種動態內存需求。
下面通過一段簡單的 C 語言代碼示例,來展示 malloc 函數的具體用法。假設我們要動態分配一個包含 10 個整數的數組,并對其進行初始化和輸出:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr;
int n = 10;
// 使用malloc分配內存,為n個整數分配空間
arr = (int *)malloc(n * sizeof(int));
// 檢查內存分配是否成功
if (arr == NULL) {
printf("內存分配失敗\n");
return 1;
}
// 初始化數組
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
// 輸出數組內容
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 釋放內存,避免內存泄漏
free(arr);
return 0;
}(2)free函數
free 函數與 malloc 函數緊密配合,是 C 語言中用于釋放動態分配內存的關鍵函數。其函數原型為:void free(void *ptr);,這里的ptr參數是一個指向先前通過 malloc、calloc 或 realloc 等函數動態分配的內存塊的指針。free 函數的主要功能就是將ptr所指向的內存塊歸還給系統,使其重新成為可供分配的空閑內存,以便后續其他內存分配請求使用。
當程序調用 free 函數釋放內存時,其內部的釋放機制如下:free 函數首先會根據傳入的指針ptr,找到對應的內存塊。在 malloc 分配內存時,除了分配用戶請求大小的內存空間外,還會在該內存塊的頭部或其他特定位置,記錄一些額外的管理信息,如內存塊的大小等。free 函數通過這些管理信息,能夠準確地確定要釋放的內存塊的邊界和大小。然后,free 函數會將該內存塊標記為空閑狀態,并將其重新插入到操作系統維護的空閑內存鏈表中。
如果相鄰的內存塊也是空閑狀態,free 函數通常會將它們合并成一個更大的空閑內存塊,這一過程被稱為內存合并。內存合并可以有效地減少內存碎片的產生,提高內存的利用率。例如,在一個頻繁進行內存分配和釋放的程序中,如果不進行內存合并,隨著時間的推移,內存中可能會出現大量零散的小空閑內存塊,這些小內存塊由于無法滿足較大的內存分配請求,而導致內存資源的浪費。通過內存合并,這些相鄰的小空閑內存塊可以合并成一個較大的空閑內存塊,從而提高內存的使用效率。
接著上面 malloc 函數的示例代碼,我們來看一下 free 函數的使用:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr;
int n = 10;
// 使用malloc分配內存,為n個整數分配空間
arr = (int *)malloc(n * sizeof(int));
// 檢查內存分配是否成功
if (arr == NULL) {
printf("內存分配失敗\n");
return 1;
}
// 初始化數組
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
// 輸出數組內容
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 釋放內存,避免內存泄漏
free(arr);
// 將指針置空,避免懸空指針
arr = NULL;
return 0;
}在這段代碼中,當我們使用 malloc 函數分配內存并完成對數組的操作后,調用 free (arr) 來釋放之前分配的內存。需要特別注意的是,在調用 free 函數之后,我們將指針arr賦值為NULL 。這是一個非常重要的操作,因為如果不將指針置空,arr就會成為一個懸空指針(Dangling Pointer)。懸空指針指向的是一塊已經被釋放的內存,繼續使用懸空指針進行內存訪問,會導致未定義行為,可能引發程序崩潰、數據損壞等嚴重問題。將指針置空后,就可以避免不小心對已釋放內存的訪問,提高程序的穩定性和安全性。
二、delete 釋放內存原理剖析
2.1 delete 的基本用法
在 C++ 的世界里,delete 是釋放內存的關鍵操作符,就像一把神奇的 “內存掃帚”,能夠清掃程序中不再需要的內存空間。不過,這把 “掃帚” 的使用方法可是有不少講究的。
(1)單個對象的 delete 操作
當我們使用 new 操作符在堆上創建一個單個對象時,就需要使用 delete 來釋放它所占用的內存。來看一段簡單的代碼示例:
#include <iostream>
int main() {
int* ptr = new int;
*ptr = 10;
std::cout << "The value of ptr is: " << *ptr << std::endl;
delete ptr;
return 0;
}在這段代碼中,int* ptr = new int;這行代碼在堆上分配了一個整型大小的內存空間,并將其地址賦給了指針ptr。接著,我們給這個內存空間賦值為 10。當我們不再需要這個內存空間時,就使用delete ptr;來釋放它。這樣,這塊內存就可以被系統重新分配給其他需要的地方,就像把一個不再使用的物品放回倉庫,以便其他人可以再次使用。
(2)數組對象的 delete [] 操作
如果我們創建的是一個數組對象,情況就稍有不同了,這時需要使用delete[]來釋放內存。delete[]專門用于釋放由new[]申請的動態內存,也就是對象數組指針指向的內存。例如:
#include <iostream>
int main() {
int* arr = new int[5];
for (int i = 0; i < 5; ++i) {
arr[i] = i + 1;
}
std::cout << "The elements of the array are: ";
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
delete[] arr;
return 0;
}在這個例子中,int* arr = new int[5];創建了一個包含 5 個整數的數組。然后我們對數組進行了初始化,并輸出了數組的元素。最后,使用delete[] arr;來釋放整個數組所占用的內存空間。這里使用delete[]而不是delete是非常重要的,因為delete[]會正確地調用數組中每個元素的析構函數(如果是自定義類型的數組),并釋放整個數組的內存。
如果錯誤地使用delete來釋放數組內存,雖然對于基本數據類型的數組可能不會立即出現明顯的問題,但對于自定義數據類型的數組,就只會調用數組第一個對象的析構函數,而其他對象的資源可能無法正確釋放,從而導致內存泄漏等嚴重問題。就好比你要拆除一排房子(數組),delete[]會一間一間地拆除(調用每個對象的析構函數),而delete可能只拆除第一間房子,剩下的房子就會一直留在那里,占用著寶貴的土地資源(內存) 。
2.2 delete 簡單類型原理
當delete用于釋放簡單類型(如基本數據類型int、char、double等)的內存時,其原理相對簡單。在這種情況下,delete實際上默認只是調用了free函數來釋放內存 。這是因為簡單類型沒有復雜的資源管理需求,它們只占用固定大小的內存空間,不需要額外的清理操作。例如:
int* p = new int;
*p = 10;
delete p;在這段代碼中,new int分配了一個int類型大小的內存空間,并返回指向該空間的指針p。當執行delete p時,delete會調用free函數,將p所指向的內存歸還給系統,從而完成內存的釋放。從功能上來說,在釋放簡單類型內存時,delete和free的作用是類似的。但需要注意的是,delete是 C++ 的運算符,而free是 C 語言標準庫函數,它們的使用場景和語義還是存在一些差異的,在 C++ 中,對于由new分配的內存,應該使用delete來釋放,以保證代碼的一致性和安全性。
2.3 delete復雜類型原理
當面對復雜類型(如自定義類、結構體等)時,delete的工作原理就變得更加復雜和精細。對于復雜類型,delete在釋放內存時,首先會調用對象的析構函數,然后再調用operator delete來釋放內存。析構函數在這個過程中起著至關重要的作用,它負責清理對象內部所占用的資源,比如動態分配的內存、打開的文件句柄、網絡連接等。例如我們有一個自定義類MyClass:
class MyClass {
public:
MyClass() {
// 構造函數,初始化資源
data = new int[10];
std::cout << "MyClass constructor" << std::endl;
}
~MyClass() {
// 析構函數,釋放資源
delete[] data;
std::cout << "MyClass destructor" << std::endl;
}
private:
int* data;
};當我們使用new創建MyClass對象并使用delete釋放時:
MyClass* obj = new MyClass;
delete obj;在執行delete obj時,首先會調用MyClass的析構函數~MyClass()。在析構函數中,我們看到delete[] data;這一行代碼,它負責釋放MyClass對象內部動態分配的數組data。只有在析構函數完成對對象內部資源的清理后,delete才會調用operator delete來釋放obj所指向的內存空間,將其歸還給系統堆管理器。如果在釋放復雜類型對象時不調用析構函數,那么對象內部的資源就無法得到正確釋放,從而導致內存泄漏。這也是delete在處理復雜類型時與簡單類型的最大區別,它通過析構函數實現了對復雜對象生命周期的完整管理,確保對象占用的所有資源都能被妥善清理和釋放。
2.4 delete [] 與數組內存釋放
當我們使用new[]來分配數組內存時,情況又有所不同。為了能夠正確地釋放數組中每個元素的資源并回收整個數組的內存,C++ 采用了一種巧妙的機制。在使用new[]分配數組內存時,實際上會多分配 4 個字節(在 32 位系統下,64 位系統可能不同),這 4 個字節被用來記錄數組的大小。例如:
int* arr = new int[5];在這個例子中,new int[5]不僅分配了 5 個int類型元素所需的內存空間,還額外在前面多分配了 4 個字節來保存數組的大小信息 5。
當使用delete[]來釋放數組內存時,它會首先讀取這 4 個字節中記錄的數組大小信息。然后,delete[]會根據這個大小信息,依次逆序調用數組中每個元素的析構函數(如果是自定義類型的數組),清理每個元素所占用的資源。最后,delete[]再調用operator delete[]函數(其內部調用free函數),釋放整個數組所占用的內存空間,包括前面多分配的 4 個字節。比如:
delete[] arr;delete[] arr通過讀取前面 4 字節記錄的數組大小,依次調用每個元素的析構函數(如果有),最后釋放整個數組內存。如果錯誤地使用delete而不是delete[]來釋放數組內存,對于自定義類型的數組,就只會調用數組第一個元素的析構函數,而其他元素的資源無法得到正確釋放,從而導致內存泄漏 。所以,在釋放數組內存時,一定要使用delete[],以確保內存的正確釋放和資源的有效回收 。
三、delete 如何知曉釋放內存大小
3.1 new [] 分配內存的特殊處理
當我們使用new[]來分配數組內存時,C++ 編譯器會進行一些特殊的處理。在實際存儲數組數據的內存塊之前,會額外多分配 4 個字節的空間 。這 4 個字節可不是隨便分配的,它們有著至關重要的作用,那就是用來記錄數組中元素的個數。比如我們使用int* arr = new int[10];來分配一個包含 10 個int類型元素的數組,在內存中,實際的布局是這樣的:首先是額外分配的 4 個字節,它們存儲著數值 10,表示數組元素的個數;接著才是真正用于存儲 10 個int類型數據的內存空間。這種在分配內存時預留空間記錄數組大小的方式,為后續delete[]釋放內存提供了關鍵的信息。
3.2 delete [] 讀取內存大小信息
當我們使用delete[]來釋放通過new[]分配的數組內存時,它會從內存塊的起始位置向前偏移 4 個字節(因為之前new[]分配內存時,在實際數據內存塊前多分配了 4 個字節來記錄數組元素個數) 。通過讀取這 4 個字節中的內容,delete[]就能準確得知數組中元素的個數。知道了元素個數,delete[]就可以按照正確的次數調用數組中每個元素的析構函數,將每個元素占用的資源都清理干凈。
比如對于一個包含自定義類型對象的數組,每個對象在創建時可能分配了一些資源,如動態分配的內存、打開的文件句柄等,通過調用析構函數,這些資源都能得到妥善釋放。在完成所有元素析構函數的調用后,delete[]再將整個內存塊(包括之前記錄元素個數的 4 個字節和存儲數據的內存空間)歸還給系統,完成內存的釋放操作。
3.3示例代碼演示
下面通過一段具體的代碼來直觀地展示new[]分配內存和delete[]釋放內存的過程:
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor" << std::endl;
}
};
int main() {
MyClass* arr = new MyClass[3];// 使用new[]分配包含3個MyClass對象的數組
// 這里可以對arr數組進行操作
delete[] arr; // 使用delete[]釋放數組內存
return 0;
}在這段代碼中,new MyClass[3]會分配一段內存,在實際存儲 3 個MyClass對象的內存塊前,會有 4 個字節用于記錄元素個數 3。當執行delete[] arr時,它會先讀取這 4 個字節中的數字 3,然后依次調用 3 次MyClass的析構函數,輸出 3 次MyClass destructor,最后將整個內存塊釋放。
如果我們錯誤地使用delete arr(而不是delete[] arr)來釋放這個數組內存,就會導致程序出現未定義行為,可能引發程序崩潰,因為delete不會去讀取前面記錄元素個數的 4 個字節,也就無法正確調用每個元素的析構函數,從而造成內存泄漏和資源未正確釋放的問題 。通過這個示例代碼,我們可以清晰地看到new[]和delete[]在處理數組內存時,是如何通過記錄和讀取內存大小信息來實現正確的內存管理的。
四、delete 釋放內存的常見問題與解決方案
4.1內存泄漏
在使用delete釋放內存時,最常見的問題之一就是內存泄漏。當我們使用new分配了內存,卻忘記使用delete來釋放它,這些未被釋放的內存就會一直占用系統資源,隨著程序的運行,占用的內存越來越多,最終可能導致系統內存不足,程序運行緩慢甚至崩潰。比如下面這段代碼:
void memoryLeakExample() {
int* ptr = new int;
// 一些操作
// 忘記釋放ptr指向的內存
}在這個函數中,我們使用new分配了一個int類型大小的內存,但在函數結束時,沒有調用delete來釋放這塊內存,從而導致內存泄漏。
為了避免內存泄漏,我們可以使用智能指針。C++ 標準庫提供了幾種智能指針,如std::unique_ptr、std::shared_ptr和std::weak_ptr 。以std::unique_ptr為例,它采用獨占所有權的方式管理對象,當std::unique_ptr對象被銷毀時,它所指向的對象也會被自動銷毀。使用智能指針可以大大簡化內存管理,減少人為忘記釋放內存的風險 。修改后的代碼如下:
#include <memory>
void properMemoryManagement() {
std::unique_ptr<int> ptr = std::make_unique<int>();
// 一些操作
// 無需手動調用delete,離開作用域時ptr會自動釋放內存
}在現代 C++ 編程中,推薦使用智能指針來管理動態內存,這樣可以提高代碼的安全性和可靠性。
4.2懸掛指針
當我們釋放了內存,但指向該內存的指針沒有被置為空指針(nullptr)時,就會產生懸掛指針。懸掛指針指向的是一塊已經被釋放的內存,再次使用懸掛指針會導致未定義行為,可能引發程序崩潰或數據損壞等嚴重問題。例如:
int* ptr = new int;
delete ptr;
// ptr現在是一個懸掛指針,如果再次使用可能導致未定義行為
std::cout << *ptr;為了避免懸掛指針問題,在釋放內存后,我們應該立即將指針置為nullptr,這樣可以明確表示該指針不再指向有效的內存。修改后的代碼如下:
int* ptr = new int;
delete ptr;
ptr = nullptr;另外,使用智能指針也可以有效避免懸掛指針問題,因為智能指針在對象生命周期結束時會自動釋放內存,并且不會產生懸掛指針。
4.3誤用 delete 與 delete []
在 C++ 中,使用delete和delete[]時需要特別小心,因為它們的使用場景是不同的。delete用于釋放單個對象的內存,而delete[]用于釋放數組的內存。如果使用delete來釋放數組內存,或者使用delete[]來釋放單個對象的內存,都會導致未定義行為 。例如:
int* arr = new int[5];
delete arr; // 錯誤,應使用delete[]在這段代碼中,我們使用new[]分配了一個包含 5 個int類型元素的數組,但卻使用delete來釋放內存,這是錯誤的做法。正確的做法是使用delete[]來釋放數組內存:
int* arr = new int[5];
delete[] arr;同樣,如果使用delete[]來釋放單個對象的內存,也是錯誤的:
int* num = new int;
delete[] num; // 錯誤,應使用delete為了避免這類錯誤,我們在分配內存時就要明確是分配單個對象還是數組,然后在釋放內存時使用與之匹配的操作符。
4.4重復釋放內存
對同一塊內存多次執行delete或delete[]操作會導致嚴重的錯誤,因為這塊內存可能已經被釋放,再次釋放會導致未定義行為,可能引發程序崩潰。例如:
int* ptr = new int;
delete ptr;
delete ptr; // 重復釋放,錯誤為了避免重復釋放內存,一種方法是在釋放內存后將指針置為nullptr,這樣再次調用delete時,delete會檢查指針是否為nullptr,如果是nullptr,則不會進行釋放操作,從而避免錯誤。修改后的代碼如下:
int* ptr = new int;
delete ptr;
ptr = nullptr;
delete ptr; // 不會產生錯誤,因為ptr是nullptr另一種更好的方法是使用智能指針,智能指針會自動管理內存的生命周期,避免手動管理內存帶來的重復釋放等問題 。例如使用std::unique_ptr:
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>();
// 無需擔心重復釋放問題,離開作用域時ptr會自動釋放內存通過以上幾種常見問題的分析和解決方案的介紹,希望大家在使用delete釋放內存時能夠更加謹慎,避免這些問題的出現,編寫出更加健壯、可靠的 C++ 程序 。
五、Linux內存管理面試題
面試題 1:delete 釋放內存時,它是如何知曉需釋放的內存大小的?
答案:系統通常會在分配內存時,于內存首地址附近記錄本次分配的內存大小等信息。當使用 delete 時,其能依據這些隱藏記錄定位需釋放的正確范圍。
面試題 2:用 new 分配數組內存,為何需用 delete[] 釋放?
答案:對于通過 new[] 分配的數組,delete[] 能識別內存中存儲的數組長度信息,針對自定義類型,其會正確調用數組每個元素的析構函數,之后釋放整塊內存。若誤用 delete,可能僅調用首個元素析構函數并釋放部分內存,引發內存泄漏或未定義行為。
面試題 3:delete 和 free 釋放內存的方式不同,delete 不必手動指定大小,free 卻需 malloc 明確大小,為什么?
答案:new 和 delete 是 C++ 操作符,分配時會依據數據類型記錄尺寸信息。malloc 和 free 是 C 標準庫函數,malloc 按用戶傳遞的字節數分配,釋放時無其他存儲大小的默認機制,需開發者確保邏輯合理,自行匹配已分配的內存大小。
面試題 4:若不知道是用 new 還是 new[] 分配的內存,該怎么釋放?
答案:必須確定分配方式以對應釋放,否則行為未定義。編程實踐應遵循清晰的內存管理規范,或者用智能指針等自動內存管理工具規避這種不確定性。
面試題 5:delete 釋放內存后,內存大小記錄信息會被怎么處理?
答案:delete 底層常調用類似 operator delete 函數(默認實現會關聯 free 等),釋放后,內存大小記錄信息所在區域歸還給空閑內存管理系統。其可能被覆蓋重寫,供后續分配操作重新利用該內存空間。
面試題 6:delete 能釋放 malloc 分配的內存嗎,它能正確獲取到其內存大小嗎?
答案:不能。malloc 和 free 需明確大小,無適配 delete 獲取大小的內置機制。new/delete 針對 C++ 類型管理含析構等邏輯,與 malloc/free 內存布局和操作邏輯不同,交叉使用會引發未定義行為。
面試題 7:delete 不知道內存大小時能否安全釋放基本數據類型數組?
答案:使用 delete[] 能安全釋放。基本類型無析構函數,delete 或 delete[] 都可釋放內存空間。不過,遵循規范應始終用 delete[] 匹配 new[],以避免基本類型后期被替換成自定義類型后出現風險。
面試題 8:delete 釋放內存時,不知道內存大小,遇到內存碎片會影響釋放嗎?
答案:一般不受影響。內存碎片指空閑內存分散成小碎片區域,其影響內存分配找連續可用塊的效率。delete 釋放按已記錄的分配范圍處理,釋放后內存管理系統或嘗試合并相鄰空閑區域減少碎片。
面試題 9:若重載 operator new 改變內存分配邏輯,delete 還能正常獲取內存大小并釋放嗎?
答案:若重載 operator new,需對應重載 operator delete 維持匹配邏輯。若按特殊方式存儲內存大小,重載的 operator delete 需實現對應讀取邏輯釋放。否則默認 delete 處理可能無法獲取準確大小,觸發未定義行為。
面試題 10:delete 是怎么知道釋放自定義類型對象內存大小的?
答案:new 分配自定義類型內存時,依其聲明結構計算大小。delete 依賴 new 階段存儲的對應類型內存分配信息,調用析構函數后,釋放與該類型尺寸匹配的由 new 保留的整塊內存。




















