1. 引言
在C++的世界里,動態內存管理是一個核心話題。對于從C語言過渡到C++的開發者來說,一個常見的困惑是:既然C語言的
malloc
和free
依然可以在C++中使用,為什么C++還要引入new
和delete
這兩個操作符?本文將深入探討這兩對內存管理機制的根本區別,幫助你理解為何在C++中更推薦使用
new
和delete
。
2、本質區別:運算符vs函數
這就最根本的區別,決定了他們的行為與能力。
new和delete:是c++內置的運算符(operator),編譯器直接理解它們的含義,并腐惡轉換為底層的內存分配和對象構造調用。
malloc和free:是c語言標準庫中定義的庫函數,位于<stdlib>,<stdlib.h>頭文件中,它們的功能由運行時庫提供。
特性 new/delete malloc/free 本質 c++操作符 c庫函數 內存分配 在堆上分配指定類型的內存 在堆上分配指定字節數的內存 析構函數/構造函數 會調用 不會調用 返回值 返回明確類型的指針 返回void*,需手動強制轉換 分配失敗 拋異常 返回NULL 重載 可以進行類成員重載或全局重載 不可以重載 計算大小 編譯器自動計算所需內存大小 需手動使用sizeof計算字節數 初始化 支持顯式初始化 只能分配未初始化的內存
3、類型安全與返回值:
malloc的返回值是void*,所以在使用的時候必須使用強制轉換,如果忘記進行強制類型轉換,那么就會發生錯誤,這也是malloc函數的弊端或者說是缺點之一。
// C style (類型不安全)
int *p = (int*)malloc(sizeof(int)); // 必須進行強制類型轉換
*p = 10;
free(p);
new
?直接返回所需類型的指針,是類型安全的。
// C++ style (類型安全)
int *p = new int; // 直接返回 int* 類型
*p = 10;
delete p;
接著往下看,這也是malloc與free函數與new和delete運算符的本質區別:
#include<iostream>
#include<stdlib.h>
using namespace std;
class Myclass
{
public:Myclass(){cout << "成功調用構造函數:Myclass()函數" << endl;}~Myclass(){cout << "成功調用析構函數:~Myclass()函數" << endl;}
};
int main()
{cout << "using malloc/free:" << endl;Myclass* obj1 = (Myclass*)malloc(sizeof(Myclass));free(obj1);//析構函數并不會被調用cout << "using new/delete:" << endl;Myclass* obj2 = new Myclass;delete obj2;//1、調用Myclass的析構函數,對于自定義類型,會調用它的析構函數//2、釋放內存,將對象本身的內存還給操作系統。
}
從輸出結構可以清晰地看到,malloc僅僅分配了足夠大的內存空間,而new在分配內存后,還調用了類的構造函數來對實例化出的對象進行初始化操作,delete在釋放內存前也會調用析構函數來清理資源(如關閉文件、釋放內存),而free則直接釋放內存,有可能導致內存泄漏,所以在c++里,十分建議寫new和delete,不僅僅安全還很方便,malloc與free有的,new和delete都有,malloc與free沒有的,new和delete也有。
4 、對于數組的處理:
使用free不需要關心是否是數組,但是如果你使用new[ ]進行內存分配,那么必須使用delete[ ]來釋放對應的內存,去掉這個[ ]可能會報錯!
接著看下面的代碼:
#include<iostream>
#include<stdlib.h>
using namespace std;
class Myclass
{
public:Myclass(){cout << "成功調用構造函數:Myclass()函數" << endl;}~Myclass(){cout << "成功調用析構函數:~Myclass()函數" << endl;}
};
int main()
{Myclass* ptr = new Myclass[10];delete [] ptr;
}
在c++中,使用new[ ]動態分配數組時,對于內置類型,比如int,char,float,等等,new int[10]不會調用其構造函數,因為內置類型沒有用戶定義的構造函數,但是對于自定義類型而言,使用new[ ]來定義數組,必須我這里自定義的類型Myclass,這里的new Myclass[10]會調用10次Myclass類的構造函數和析構函數。
下面介紹的才是這篇博客的核心內容,也是本人從淺至深的一個過程。
5、operator? new和operator delete:
提到new和delete,那么就不得不談到operator new和operator delete,那么operator new和operator delete有什么聯系或者說是區別呢?
為了方便我把代碼直接截圖下來,當我們寫出一句 Myclass* ptr=new Myclass[10]的時候,編譯器會將其分解為三個步驟:
1、分配內存:調用operator new(sizeof(Myclass)函數(這一點我等下會通過觀察匯編來進行驗證),申請一塊足夠大的,并且未初始化的原始內存。
2、構造對象:
在上述內存地址上調用Myclass::myclass()構造函數,初始化這塊內存,使其成為一個真正的對象。
3、返回指針:返回構造好的對象的地址,所以用指向這個類的指針來接收也就是Myclass*。
那么同理,delete obj也做了兩件事情:
1、析構對象:調用obj->~Myclass()析構函數,清理對象占用的內存
2、釋放資源:調用operator delete(obj)函數,釋放對象所占用的原始內存塊。
不僅僅如此,operator new與operator delete與new和delete:
new和delete是操作符,而operator new和operator delete是函數,換句說,operator new和operator delete就是C語言中malloc函數與free函數的加強版,它們不僅僅會開辟空間,還會在開辟空間的基礎上進行拋異常。而opertaor new和operator delete底層上也是調用了malloc函數與free函數。
下面我將通過匯編來驗證,new會通過調用operator new來開辟空間。
operator new
?和?operator delete
?做了什么?(單一職責)這兩個函數只負責第一步和最后一步,即原始內存的分配與釋放。它們和?
malloc
/free
?是同一級別的概念,但屬于C++的體系。
void* operator new(size_t size)
:它的唯一任務就是接受一個字節數?
size
,找到一塊足夠大的連續內存空間,并返回指向這塊內存的?void*
?指針。如果失敗,它默認拋出?std::bad_alloc
?異常。
void operator delete(void* ptr)
:它的唯一任務是接受一個由?
operator new
?返回的?void*
?指針,釋放這塊內存。標準庫已經提供了默認的全局?
operator new
?和?operator delete
,它們通常就是基于?malloc
?和?free
?實現的。你可以把它們想象成是C++世界里“高級的”、“會拋異常的”?
malloc
?和?free
。
6、?重載 (Overloading)
這才是重載的意義所在。我們可以提供我們自己版本的?operator new
?和?operator delete
?函數,來接管內存分配和釋放的過程。
第一種方式:
全局重載:
這種方式非常不推薦,因為程序中所有的new和delete都會調用我們自己寫的版本,這么做可以說是不安全的一種行為。
#include <iostream>
#include <stdlib.h> // for malloc, free
using namespace std;// 全局重載 operator new
void* operator new(size_t size) {cout << "Global new called, size: " << size << endl;void* p = malloc(size);if (!p) throw bad_alloc(); // 遵循規范,分配失敗拋異常return p;
}// 全局重載 operator delete
void operator delete(void* p) noexcept {cout << "Global delete called" <<endl;free(p);
}class MyClass { int data;
};
int main() {int* p1 = new int(42); // 會調用我們重載的全局 operator newdelete p1; // 會調用我們重載的全局 operator deleteMyClass* obj = new MyClass; // 同樣會調用我們的版本delete obj;return 0;
}
第二種方式:
類特定重載 (非常有用且推薦):
#include <iostream> #include <stdlib.h> using namespace std;class MyClass { public:int _data;// 類特定的 operator newstatic void* operator new(size_t size) {cout << "MyClass::new called, size: " << size <<endl;void* p = malloc(size);if (!p) throw bad_alloc();return p;}// 類特定的 operator deletestatic void operator delete(void* p) noexcept {cout << "MyClass::delete called" <<endl;free(p);} };int main() {MyClass* obj = new MyClass; // 調用 MyClass::operator newdelete obj; // 調用 MyClass::operator deleteint* p = new int; // 仍然使用全局的 ::operator new,不受影響delete p;return 0; }
我們只為我們特定的類重載?
operator new
?和?operator delete
。這樣,只有分配和釋放這個類的對象時,才會使用我們自定義的版本,不會影響程序的其他部分。
下面將使用一表來把operator new與operator delete和new和delete之間的區別做個總結:
總結如下:
特性 | new/delete | operator new/operator delete |
身份 | 操作符 | 函數 |
職責 | 完成的對象生命周期管理(分配內存加上構造或者析構) | 僅負責原始內存的分配和釋放 |
可重載性 | 不可重載 | 可以重載 |
調用關系 | 調用operator new和構造函數 | 被new所調用(已通過觀察匯編進行了對應的證明) |
7、new
?和?delete
?表達式的編譯期魔法
最后一個部分:對上面的內容做一個更深層次的理解:
當編譯器看到?MyClass *obj = new MyClass;
?這行代碼時,它會進行一個固定的分解動作。這個過程是理解重載底層原理的關鍵。
// 我們自己寫的代碼:
MyClass *obj = new MyClass(arg1, arg2);// 編譯器在背后實際生成的代碼:
void* __memory = nullptr; // 1. 先申請原始內存
try {__memory = MyClass::operator new(sizeof(MyClass)); // 尋找分配函數obj = static_cast<MyClass*>(__memory);obj->MyClass::MyClass(arg1, arg2); // 2. 在內存上構造對象(調用構造函數)
} catch (...) {if (__memory)MyClass::operator delete(__memory); // 如果構造失敗,釋放申請的內存throw; // 重新拋出異常
}
同理,delete也是一樣。
// 我們自己寫的代碼: delete obj;// 編譯器在背后實際生成的代碼: obj->~MyClass(); // 1. 先調用析構函數 MyClass::operator delete(obj); // 2. 再釋放內存
重載?
operator new
?和?operator delete
,本質上就是告訴編譯器,在執行上述流程的第一步和最后一步時,不要用標準庫提供的默認函數,而是用我自定義的函數。
底層做了什么?
1、編譯時:編譯器看到?
new MyClass
,知道要去?MyClass
?的作用域內尋找?operator new
?函數。2、運行時:
new:調用我們自己重寫的Myclass::operator new來獲取內存,然后調用構造函數完成初始化。
delete:調用析構函數,然后調用我們自己重寫的Myclass::operator delete來釋放內存。
3、內存布局:重載函數本質是類的靜態成員函數,他們不屬于任何一個對象實例,因此沒有this指針,通俗地講,他們只是為對象的誕生(創建)和消亡(銷毀)提供場地管理的后勤部門!
截止到這里,本文的所有內容就完成了,在寫的時候難免有所不足之處,可在評論區指出,本人會及時進程更新,如對您有所幫助可以點贊加收藏,本作者持續更新c/c++或數據結構或linux有關的內容!