?????????在C語言中我們通常會使用malloc/realloc/calloc來動態開辟的空間,malloc是只會開辟你提供的空間大小,并不會初始化內容;calloc不但會開辟空間,還會初始化;realloc是專門來擴容的,當你第一次開辟的空間不夠用的時候,就要使用realloc;如果你第一次使用realloc的時候,前面沒有開辟過空間,那么realloc的行為會跟malloc一樣,之后再發揮realloc自己的行為。而我們的C++是面向對象的編程,當開辟空間失敗了malloc只會返回一個空指針,我們還需要自己來判斷。所以在C++這里就將malloc升級成為了new,new在開辟空間失敗的時候會拋出異常,這跟我們面向對象的理念是一致的。
? ? ? ? 其實我在C語言階段學習動態開辟空間的時候,是有點迷糊的,就是不知道如何去開辟,給誰開辟?現在想想其實不難理解,假設我們要開辟10個int類型的空間大小,我們有兩種辦法,一種就是直接定義一個int類型的數組:int arr[10],但是這樣的空間是定長的空間,我們無法對定長數組進行擴容。第二種就是用到動態開辟的空間了,我們肯定是先要malloc10個int類型的空間,但是我們應該如何取到這段空間呢?用指針變量接收是吧,但是為什么是指針變量呢?我們繼續講解,因為我們先開辟了一段空間,正常情況下是無法取到這段空間的,只有拿到這段空間的地址,才可以訪問這段空間。這也就是為什么我們會用指針變量來接收malloc開辟的空間了。
????????下面是malloc的函數聲明:通過malloc的返回值也不難看出要用指針接收。所以我們要開辟空間,一種就是直接定義類型,比如int a、int arr[10]、char ch等等,一種就是用malloc/realloc/calloc開辟空間,用指針接收。
一、介紹
1. malloc、realloc、calloc的介紹
1. malloc只是動態開辟空間,并沒有初始化這段空間的內容;
2. realloc是用來擴容的,如果沒有預先開辟空間,直接使用realloc,realloc的作用相當于malloc;
3. calloc相當于是malloc的加強版,不僅可以開辟空間,也順便初始化我們的內容;
4. 面對內置類型可以直接開辟空間,但是對于自定義類型的話,比如我們的棧,則無法一步到位,因為棧里面還有數組空間的開辟,需要兩層開辟。
????????下面就是棧的舉例:因為我們僅僅是用的malloc來開辟一個棧的空間,所以棧內部的數組空間還需要開辟,如果我們要是寫一個構造函數去初始化,在主函數中是無法調用的,也就完成不了對數組空間的開辟,不過在了解定位new之后是可以調用的,我們這里使用寫一個成員函數Init來完成對數組空間的開辟,這樣我們在主函數中也可以調用。然后其實這樣的方法有點麻煩,而且在malloc失敗的時候只是返回空指針,判斷條件也需要我們自己去寫,這很不滿足面向對象的要求,所以C++基于這樣的原因創造了new這個操作符。
#include <iostream>
using namespace std;
class Stack
{
public:void Init(int capacity){_a = (int*)malloc(sizeof(int) * capacity);if(_a == nullptr){perror("malloc fail");exit(-1);}_size = 0;_capacity = capacity;}
private:int *_a;int _size;int _capacity;
};
int main()
{Stack *st = (Stack*)malloc(sizeof(Stack));st->Init(4);return 0;
}
2. free的介紹
????????free專門釋放動態開辟的空間,如果釋放的不是動態開辟的空間,就會報錯,所以這里一定要多加注意!!!
????????一般就是free( ),括號里面就是指向那段空間的指針就行。
3. new、operator new的介紹(內置類型和自定義類型)
????????new是在C++中才有的操作符,因為C++兼容C語言,所以在C++中可以寫C,但是在C中寫不了C++。new也是用來動態開辟空間的,也可以初始化,即可以開辟內置類型的空間,也可以開辟自定義類型的空間,這兩種類型都可以初始化;
1. new對內置類型開辟空間:直接開辟
int *a = new int; //單純開辟空間
int *b = new int(3); //開辟空間并初始化int *arr1 = new int[5]; //單純開辟5個整型的空間
int *arr2 = new int[5]{1, 2, 3, 4, 5}; //開辟5個整型空間并初始化
2. new對自定義類型開辟空間:第一步:開辟這個自定義類型需要的空間;
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 第二步:調用這個自定義類型的構造函數;
所以我們上面對于棧的開辟,可以改善為下面這樣:
#include <iostream>
using namespace std;
class Stack
{
public:Stack(int capacity = 4, int size = 0, int num = 0): _a(new int[capacity]{0}), _size(size), _capacity(capacity){cout << "Stack(int capacity = 4)" << endl; //方便觀察確實調用了構造函數}private:int *_a;int _size;int _capacity;
};
int main()
{Stack *st = new Stack;//Stack *st = new Stack(4, 0, 0);這種就是對于顯式構造函數的寫法,或者是你想傳入的初始化內容return 0;
}
3. 那operator new 又是什么呢?
????????相信大家對這個并不熟悉,所以這部分知識了解即可,operator new 是一種全局函數,對malloc進行了封裝,也就是讓malloc函數更加面向對象,而不是面向過程了。因為我們在C語言的時候,malloc開辟空間失敗,會返回個空指針,我們會根據空指針去找過程中的錯誤,然后C++是面向對象編程,所以就必須知道是哪個對象出錯了,new出錯就報new。所以operator new就是對malloc進行了一個包裝,底層還是通過malloc實現,只不過在出錯的時候,operator new是拋異常,不是返回空指針了。下面就是operator new這個函數的定義:
所以operator new 還是通過malloc實現的,看不懂下面的也沒關系,記住operator new 開辟空間失敗拋異常,malloc失敗是返回NULL;
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytesvoid *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);
}
4. delete、operator delete的介紹
delete是用來釋放動態開辟的空間,如果釋放的不是動態開辟的空間會報錯;
他跟free的區別是在自定義類型這里,free僅僅釋放他括號里指向的空間;
delete在釋放自定義類型的空間時候,會做兩件事:
1. 先調用自定義類型的析構函數;
2. 再去調用operator delete函數;
int *a = new int;char *ch = new char;double *d = new double[5];delete a;delete ch;delete []d;
而operator delete 我們會在new、delete針對數組這里拿出來講解
二、new、delete的原理
1. new對于自定義類型:
1. new 會先調用 operator new 開自定義類型的空間;
2. 在申請的空間上,再去調用自定義類型的構造函數?;
這里解答一下為什么new步調用malloc,而是調用的operator new
因為還是C++是面向對象編程,所以我們不使用malloc這套判斷錯誤的方式,而是選擇operator new 這個函數去封裝一下new,再讓new來調用自己,一切都是為了面向對象,所以new的產生原因也是這樣的,因為面向對象。
2. delete對于自定義類型
1. 在空間上執行析構函數,完成對象中資源的清理工作2. 調用operator delete函數釋放對象的空間
需要注意的就是為什么delete要先調用析構函數?
因為我們要知道,再對于棧這樣的數據結構,他里面數組的空間是被棧空間的指針指著的,如果先釋放棧的空間,就相當于把數組的地址釋放了,然后再去調用析構函數就會找不到要析構的位置了,所以就會出錯;
基于這樣的原因一定要先析構,再釋放自定義類型的空間;
3.?new、delete對于數組、自定義類型的數組(重點,難點)
????????這里其實又涉及到一些不一樣的知識,我們知道,在new一個數組的時候,我們是告訴他開辟多少個元素的空間的,如下圖,我們是知道開5個空間的大小的,那在釋放的時候,delete又是如何知道需要釋放多大的空間的呢?我們保持這個問題,繼續往下看。
#include <iostream>
using namespace std;
class Stack
{
public:Stack(int capacity = 4, int size = 0, int num = 0): _a(new int[capacity]{0}), _size(size), _capacity(capacity){cout << "Stack(int capacity = 4)" << endl; //方便觀察確實調用了構造函數}private:int *_a;int _size;int _capacity;
};
int main()
{Stack *st = new Stack[5];delete []st;return 0;
}
其實,是因為在開辟空間之前,我們多開辟了一個int的空間,這個空間就是用來存放數組的個數的,相當于是圖中這樣開辟的空間:
????????所以delete面對數組這樣的空間,他先通過st指針指向的位置,先調用多次析構函數,連續釋放完5個_a指針指向的空間之后,返回到a位置處,再用通過調用operator delete釋放自定義類型數組的這段空間。
????????換句話說delete 的[ ] 是識別這個類型是數組,然后知道數組會多開辟4個字節的空間,進而從多開辟的空間的地址處釋放。而單獨的delete就不會認為這個類型是數組類型,也就不會尋找這段空間,直接在數組首元素這里釋放空間,當然一定要注意自定義類型,還需要調用析構函數。
new --> 內置類型數組
new --> operator new[ ]?函數? --> operator new 函數 --> malloc??
這里要知道的是,上面都是調用了一次,因為開辟的空間是提前算好的,開辟一次就行
delete --> 內置類型數組
delete --> operator delete[ ] 函數? --> operator delete?函數 --> free??
new --> 自定義類型數組
1.?new --> operator new[ ] 函數? --> operator new 函數 --> malloc??
2. 每一個自定義類型再調用構造函數
開辟的空間都是一次性開辟好的,所以只需要new一次,而每個自定義類型都需要調用構造函數,所以是多個。
delete --> 自定義類型數組
1. 每一個自定義類型先調用自己的析構函數
2. delete?--> operator delete[ ] 函數? --> operator delete?函數 --> free
三、面試題:new/delete 和?malloc/free 的 異同?
共同點:都是從堆上申請空間,并且需要用戶手動釋放。
不同點:1. malloc和free是函數,new和delete是操作符;2. malloc申請的空間不會初始化,new可以初始化;3. malloc申請空間時,需要手動計算空間大小并傳遞,new只需在其后跟上空間的類型即可, 如果是多個對象,[ ]中指定對象個數即可;4. malloc的返回值為void*, 在使用時必須強轉,new不需要,因為new后跟的是空間的類型5. malloc申請空間失敗時,返回的是NULL,因此使用時必須判空,new不需要,但是new需要捕獲異常;6. 申請自定義類型對象時,malloc/free只會開辟空間,不會調用構造函數與析構函數,而new在申請空間后會調用構造函數完成對象的初始化,delete在釋放空間前會調用析構函數完成空間中資源的清理;
四、練習題
1. C++中,類ClassA的構造函數和析構函數的執行次數分別為( )
ClassA *p = new ClassA[5];delete p;
A.5,1
B.1,1
C.5,5
D.程序可能崩潰
答案及解析:D
大家可以參考下面這個圖:
????????delete p這就相當于先調用b位置的析構函數,然后直接在b位置釋放了。因為是delete p;并不是delete [ ]p;所以delete并不知道說這是一個數組,并不會跳到最前面來釋放,釋放的位置不對,導致了程序的崩潰;
new和delete一定要匹配,避免未定義行為;
new —— delete
new [ ] —— delete[ ]
malloc —— free