🔥 本文專欄:c++
🌸作者主頁:努力努力再努力wz
💪 今日博客勵志語錄:
“生活不會向你許諾什么,尤其不會向你許諾成功。它只會給你掙扎、痛苦和煎熬的過程。但只要你堅持下去,終有一天,你會站在最亮的地方,活成自己曾經渴望的模樣。”
★★★ 本文前置知識:
類和對象(上)
類和對象(中)
類和對象(下)
new
1.認識new運算符
那么我們之前在學習c語言的時候,如果我們要在堆上申請空間,那么我們可以調用malloc函數,那么malloc函數會在堆上開辟連續的空間并且返回這個空間的首元素的地址,那么此時在堆上申請的這片空間的生命周期就和程序是一樣長,并且它的生命周期可以由我們用戶在自己控制,通過調用free函數來釋放在堆上申請的空間
那么在學習了c++之后,引入了類和對象,那么我們此時在堆上申請開辟的空間存儲的數據類型不僅只有內置類型,還有自定義類型
那么此時就要注意創建堆對象和創建棧對象的區別,那么所謂的堆對象,就是我們調用malloc函數在堆上申請了一片空間,那么這個空間存儲的數據類型是自定義類型,而棧對象就是我們在函數內部所定義的局部的自定義類型的變量
//創建堆對象
type* A=(type*)malloc(sizeof(type));
//創建棧對象
type A;
那么對于棧對象來說,那么棧對象的生命周期則是由編譯器來控制,那么一旦你定義了一個局部的自定義類型的變量,那么在對象實例化的同時,那么編譯器會自動調用該對象的無參或者全缺省的構造函數,而一旦函數調用結束,那么函數棧幀會被銷毀,那么此時編譯器會清理在函數內定義的局部變量,其中就包括該局部對象,那么編譯器會自動的調用其析構函數來清理該局部對象的資源
而對于堆對象來說,那么堆對象的生命周期則是由我們用戶自己來決定,那么我們可以在任意一個時刻來調用free函數來釋放在堆上開辟的對象,而對于堆對象來說,編譯器便不會在自動調用該堆對象的無參或者全缺省的構造函數,那么意味著當我們創建好一個堆對象的時候,那么此時這個對象的內容是隨機值,未初始化的,那么為了解決這個問題,我們可以去定義一個完成對象內容的初始化的函數,調用malloc創建完對象之后再調用該初始化的函數來完成初始化
那么這個方式肯定是沒問題,但是如果硬要說一個缺點的話,那么就是需要我們程序員去自己定義一個初始化函數,并且還要手動調用它,所以c++在malloc的基礎上優化得到了new運算符,雖然new運算符的作用和malloc一樣,那么就是在堆上申請空間,但是其中對于在堆上創建對象的時候,那么new與malloc的不同的地方就體現出來了,那么new運算符在申請開辟完空間的時候,會自動調用該對象的構造函數來完成該對象的初始化,也就意味著我們如果采取的原始的malloc的方式來創建一個堆對象的話,那么我們還得分為兩步,也就是先malloc創建出一個對象,然后再調用比如init初始化函數,但是對于new來說,那么它則是一步到位,完成空間的開辟以及對象的初始化
我們知道c++是兼容c語言的,你可以在你的c++代碼中繼續調用malloc在堆上申請空間,但是隨著new的誕生,那么malloc就可以逐漸淡出歷史舞臺了,那么接下來我將會從new如何使用以及其使用的相關細節和new調用的本質等多個維度帶你全面剖析new運算符,那么相信你看完之后一定會對new愛不釋手的
2.new運算符如何使用
那么知道了什么是new運算符之后,那么接下來我們就得知道在語法層面上如何使用new,那么我們使用new在堆上開辟空間,那么就要指明開辟空間的類型,那么就需要在new運算符后面跟上你要開辟的數據類型,那么這個數據類型可以是內置類型也可以是自定義類型,那么開辟成功之后會返回得到一個你在堆上開辟的連續空間的首元素的地址
那么我們認識最基本的,也就是如何new一個內置類型:
//new一個int類型
int* ptr=new int;
//new一個double類型
double* ptr=new double;
//new一個float類型
float* ptr=new float;
那么new相比于malloc強大的就是我們可以在new的同時對申請的空間的內容進行初始化,而對于malloc來說,這些內容都在malloc開辟之后來完成,那么初始化的方式就是后面跟上一個括號,括號里面的內容就是你要初始化的值:
int* ptr=new int(10);
doublue* ptr=new double(20.5);
而我們知道有時候我們在堆上開辟單個變量的需求,還要堆上開辟一個動態的數組,那么這里我們的new也可以實現,那么語法上實現的方式則是:
//開辟一個int類型的長度為10的動態的數組
int* ptr=new int[10];
//開辟一個double類型的長度為10的動態的數組
double* ptr=new double[10];
并且對于數組來說,new也同樣支持創建的同時給值進行初始化,那么只不過這里對于數組的各個元素的初始化的值是在花括號當中:
//int
int* ptr=new int[10]{1,2,3,4,5,6,7,8,9,10};
//double
double* ptr=new double[10]={1.2,2.2,3.2,4.2,5.2,6.2,7.2,8.2,9.2,10.2};
那么對于內置類型的數組的初始化不完全,只給部分長度的初始值,那么剩余未初始化的元素的值便是隨機值
那么說完了內置類型,我們再來說自定義類型:
那么自定義類型同樣可以分為開辟單個自定義類型以及一個自定義類型的數組,那么首先對于開辟單個自定義類型的話,那么語法聲明則是和之前內置類型一樣,沒有什么差別:
#include<iostream>
using namespace std;
class person
{
public:int _age;int _height;const char* _name;person(int age=20, int height=168, const char* name="WangZhe"):_age(age), _height(height), _name(name){}
};
int main()
{person* a = new person;cout << a->_name << endl;return 0;
}
但是要注意的是一旦我們的類中沒有提供無參數或者全缺省的構造函數的話,那么此時我們在new的同時就得帶參數的構造函數傳遞參數:
#include<iostream>
using namespace std;
class person
{
public:int _age;int _height;const char* _name;person(int age, int height, const char* name):_age(age), _height(height), _name(name){}
};
int main()
{person* a = new person;cout << a->_name << endl;return 0;
}
正確做法:
person* a=new person(20,166,"WangZhe");
其次我們也可以使用new運算符來創建一個自定義類型的一個數組,那么自定義類型的數組和內置類型數組的定義差別不大,關鍵是自定義類型的數組中,每一個元素的初始化,那么如果該自定義類型提供了無參數或者全缺省的構造函數,那么我們new一個自定義類型的數組,那么其會自動調用這個數組中每一個對象的無參數或者全缺省的構造函數,那么我們可以寫一段簡單的代碼來驗證這個行為:
那么我們這里在類中定義的全缺省的構造函數中定義一個打印語句,那么只要其執行了該全缺省的構造函數,那么便會執行了打印語句,所以如果我們new一個長度為10的自定義類型的數組,那么如果其自動調用該數組每一個元素的全缺省構造函數,那么便會執行10次打印語句:
#include<iostream>
using namespace std;
class person
{
public:int _age;int _height;const char* _name;person(int age=20 ,int height=166, const char* name="WangZhe"):_age(age), _height(height), _name(name){cout << "person()" << endl;}
};
int main()
{person* a = new person[10];return 0;
}
其次如果說該類沒有提供無參數或者全缺省的構造函數的話,那么我們定義一個自定義類型的數組就得顯示初始化每一個元素,那么初始化的方式就可以通過匿名對象來實現,那么此時它會先調用構造函數來生成每一個匿名對象,然后再調用拷貝構造函數來初始化數組中的每一個元素,但是現代的編譯器都會對此模式進行一個優化,也就是調用一次構造和拷貝構造函數優化成了直接調用構造函數,那么我們還是可以寫一個代碼來驗證一下:
#include<iostream>
using namespace std;
class person
{
public:int _age;int _height;const char* _name;person(int age,int height, const char* name):_age(age), _height(height), _name(name){cout << "person()" << endl;}person(const person& p1){_age = p1._age;_height = p1._height;_name = p1._name;cout << "const person()" << endl;}
};
int main()
{person* a = new person[2]{person(20,166,"WangZhe"),person(18,170,"luoyi")};return 0;
}
而如果我們類中既有無參數或者全缺省的構造函數又有帶參數的構造函數,那么這里我們初始化數組的每一個元素,那么可以不用初始化數組的全員元素,可以顯示初始化數組的部分元素,然后剩下的元素就自動調用無參數或者全缺省的構造函數來完成
#include<iostream>
using namespace std;
class person
{
public:int _age;int _height;const char* _name;person(int age,int height, const char* name):_age(age), _height(height), _name(name){}person():_age(20),_height(180),_name("WangZhe"){cout << "person()" << endl;}
};
int main()
{person* a = new person[4]{person(20,166,"WangZhuo"),person(18,170,"luoyi")};return 0;
}
3.new運算符補充
1.new運算符的本質
那么知道了new運算符如何使用之后,我們知道在堆上new一個對象,那么開辟完空間的同時會對調用該對象的構造函數進行初始化,那么你感覺開辟空間以及調用構造函數這兩個動作是new的時候同時完成的,但是實則不是,其實new一個對象的本質還是分為了兩步來完成的,也就是先在堆上申請開辟空間然后再調用構造函數
而其中對于一個步驟,也就是先在堆上申請開辟空間則是交給了operator new運算符重載函數來完成,而operator new的作用就是在堆上開辟空間但是沒有進行初始化,那么一聽這個operator new的作用和malloc那么的相似,其實operator new運算符重載函數內部封裝了malloc函數,而對于malloc函數來說,那么一旦我們調用malloc函數失敗,那么malloc函數則是返回一個錯誤碼,也就是返回NULL,而對于c++這門面向對象的語言來說,那么它不再是采取返回錯誤碼的方式,而是采取拋異常的方式,那么拋出異常,意味著需要外界來接收,那么一旦收到異常,那么就需要對該異常進行特定邏輯的處理,那么其中就會涉及到try catch語句,那么本文肯定不會詳細介紹異常,而異常是我c++系列的博客之后的內容了,所以對于不了解的讀者來說,你只需知道operator new運算符重載函數內部封裝了malloc,但是由于malloc是返回錯誤碼而不是拋異常,所以operator new函數在內部對此進行了拋出異常的邏輯處理
那么我們也可以寫一段簡單的代碼,來驗證一下operator new運算符重載函數,那么注意的是operator new函數底層是封裝了malloc函數,那么也就意味著operator new函數內部會調用malloc函數,所以我們調用operator new函數的時候,那么一定要傳遞要開辟的空間的大小,如果operator new調用成功會返回開辟空間的首元素的地址,但是其類型是void* ,所以還要強制類型轉化:
#include<iostream>
using namespace std;
class person
{
public:int _age;int _height;const char* _name;person(int age,int height, const char* name):_age(age), _height(height), _name(name){}person():_age(20),_height(180),_name("WangZhe"){}
};
int main()
{person* a = (person*)operator new(sizeof(person)); return 0;
}
那么這里可以從調試窗口可以看到a指向的對象的值都是隨機值,那么驗證了operator new只是開辟空間而不進行初始化
而對于第二個步驟,也就是調用構造函數,那么這個過程就是交給了placement new來完成,那么所謂的placement new是一種特殊的new語法,那么它的作用就是為已經開辟好空間的對象顯示的調用其構造函數進行初始化,所以在剛才第一個過程,我們調用了operator new函數得到了開辟好的空間的首元素的地址,那么接下來,就可以調用placement new來調用其構造函數進行初始化
#include<iostream>
using namespace std;
class person
{
public:int _age;int _height;const char* _name;person(int age,int height, const char* name):_age(age), _height(height), _name(name){}person():_age(20),_height(180),_name("WangZhe"){}
};
int main()
{person* a = (person*)operator new(sizeof(person));a = new(a)person(20, 180, "WangZhuo");cout << a->_name << endl;return 0;
}
placement new的語法:
myclass* a=(myclass*)malloc(sizeof(myclass));myclass* b=new(a)myclass(type);
在你的視角下,你使用new運算符來創建一個對象,你看到的結果是既開辟了對象的空間又對開辟的空間進行了初始化,但是在編譯器的視角下,那么它則是將new運算符轉換成了兩步,第一步則是調用operator new函數,然后第二步再是調用placement new來完成初始化
//你的視角
myclass* ptr= new myclass;
//編譯器的視角
myclass ptr=(myclass*)operator new(sizeof(myclass));
ptr=new(ptr)myclass();
2.placement new補充
而對于placement new來說,那么它不僅對堆對象顯示調用其構造函數,并且對于棧對象以及靜態對象來說,也同樣可以顯示調用其構造函數,那么之前我那期介紹類與對象的博客提到過,一旦你定義了一個局部對象,那么調用其構造函數的時機就是在對象剛被創建實例化的那一刻,并且由編譯器來完成調用,那么一旦實例化之后,便不允許再有二次調用構造函數的行為,并且我們無法通過對象以及指針都再來調用構造函數,那么現在引入placement new之后,其實我們可以創建一個局部的對象,然后通過使用placement new來達到二次調用構造函數的效果
#include<iostream>
using namespace std;
class person
{
public:int _age;int _height;const char* _name;person(int age=20,int height=180, const char* name="WangZhe"):_age(age), _height(height), _name(name){}
};
int main()
{person s1;new(&s1)person(25,180,"pengyuyan");cout << s1._name << endl;return 0;
}
而對象的初始化規定就是在實例化對象的那一刻完成的,你之后如果再調用構造函數會覆蓋之前的對象的內容,那么這樣做其實沒有任何意義,就好比幼兒園本應該是你小時候5到6歲的年紀去上,那么現在正在上大學的年紀的你已經沒必要去再讀一次幼兒園了,但是如果你說你就想再回味一下童年,那么其實也沒有人會阻攔你去重新上一遍幼兒園,而這里如果說你需要再調用構造函數,那么除非你是先對該對象調用了一次析構函數清理了該對象的資源,那么你可以用placement new來二次調用構造函數
并且對于析構函數來說,那么編譯器是允許我們多次調用析構函數,那么可以通過指針或者直接通過對象來調用析構函數然后清理對象的資源,接著在調用placement new對其進行所謂的二次初始化
但是placement new真正的應用場景其實不在這里,而是在內存池中,那么我們知道我們調用malloc以及new都是在堆上申請內存空間,而內存空間是由操作系統來進行管理,那么其中我們直接向操作系統去申請內存空間的話,那么會涉及操作系統會去查找相應的空閑的物理內存然后再建立虛擬內存到物理內存的頁表映射等等動作,意味著直接找操作系統申請內存是有一定的成本,所以我們會預先先向操作系統申請一批內存,那么malloc申請的內存不會再去找操作系統而是直接從這預先申請好的內存中獲取,那么我們只需要對這些已經獲取到的內存的內容進行初始化即可,所以就涉及到placement new,而內存池是一個具體的項目,那么我會在后面的博客講到,那么如果看不懂我剛才說的內容其實沒有關系,那么這部分內容只是作為了解,那么掌握我前面所講的知識便足矣
3.new和malloc的對比
那么new和malloc的對比是面試是十分喜歡問到的一個知識點,那么除了知道new和malloc最顯著的區別,也就是一個會調用構造函數一個只是開辟空間,那么new和malloc的不同其實不僅僅是在這一個方面上:
1.自動計算開辟的空間
那么對于malloc函數來說,那么它會以字節為單位來開辟空間,那么在調用malloc開辟空間的時候,那么我們就得在調用之前計算一下開辟空間的大小,那么雖然有sizeof運算符能夠簡化計算,但是總之我們還是要給malloc函數傳遞一個開辟空間的字節數,而對于new來說,那么我們只需要告訴它要開辟的類型的數量,比如是開辟是一個int類型還是10個int類型的數組,那么編譯器會自己識別到類型,然后自己會調用sizeof運算符,然后交給operator new函數作為參數,也就是說編譯器在轉化
2.可以同時進行初始化
那么new可以同時進行初始化,而malloc則只能開辟空間,然后在之后的代碼中來對開辟的空間進行初始化,而對于new來說,那么它則是一行代碼能夠辦到兩件事,雖然我們知道其實底層它這兩件事是分開來完成的,但是在語言層面上,我們還是邏輯上任務new可以同時完成空間的開辟以及對于空間內容的初始化
2.自動完成指針的類型
那么我們調用malloc函數,那么malloc函數是不知道我們要在堆上開辟的空間是給什么樣的數據類型來存儲的,所以到時候需要我們強制類型轉換,而對于new來說,那么編譯器識別到我們開辟的對象的數據類型,那么會自動的調用operator new來完成類型轉換,所以調用new運算符能夠精確的得到返回的指針類型
3.對于自定義類型會調用其構造函數
那么這個區別在上文就已經詳細的講到過了,那么底層是因為調用了placement new來做到的
delete
1.認識delete運算符
那么我們知道malloc會配套有free函數來釋放空間,那么new肯定也有對應運算符來釋放器開辟的空間,那么這個工作就是delete來完成,那么這里free和delete的作用都是釋放在堆上申請的空間,但是它們之間肯定有區別,不然又何必又創建一個delete呢,那么這里的區別其實就體現在釋放自定義類型上面,那么delete運算符會先調用其析構函數,然后在完成空間的釋放,這就是delete運算符
2.delete運算符如何使用
那么delete運算符的使用就很簡答,那么它的語法就是在delete后面跟上你之前保存new開辟的對象的指針
person* a = new person;
delete a;
而對于數組來說,那么delete語法上的使用就要不同一點的就是它后面要添加一個[],代表釋放的是一個數組
int* ptr = new int[10] {1,2,3,4};
delete[] ptr;
那么對于delete[]來說,如果你釋放的是一個自定義類型的數組,那么它會依次調用這個數組的每一個對象的析構函數,而如果你是用的delete而不是delete[],那么意味著它只會調用一次析構函數,那么就會造成內存泄漏的問題出現
那么我們可以用一段簡單的代碼來驗證,其中我在類中自定義了析構函數,并且在析構函數內部添加了一個打印語句,那么意味著調用一次析構函數,就要打印一次語句:
#include<iostream>
using namespace std;
class person
{
public:int _age;int _height;const char* _name;person(int age=20,int height=180, const char* name="WangZhe"):_age(age), _height(height), _name(name){}~person(){cout << "~person()" << endl;}
};
int main()
{person* ptr = new person[4];delete[] ptr;return 0;
}
#include<iostream>
using namespace std;
class person
{........
}
int main()
{person* ptr=new person[4];delete ptr;return 0;
}
那么這里直接運行崩潰了
delete運算符補充
1.delete運算符的本質
那么對于delete運算符的具體過程,那么根據上文的new,我們也可以大致猜測,這里delete運算符并不是同時調用析構以及釋放空間的
對于delete運算符來說,當我們調用delete運算符,那么編譯器首先會將其轉換為兩個步驟,那么第一個步驟就是調用創建出來的對象的析構函數,清理對象的資源,而對于析構函數來說,那么它不像構造函數,那么析構函數可以通過指針或者對象來二次調用,但是注意平常我們自己寫代碼的時候,我們不要自己去手動通過指針或者對象來調用析構函數,因為對于棧對象來說,那么程序執行了你手動調用的析構函數之后,當函數調用結束之后,那么編譯器又會調用一次該對象的析構函數,那么這就會導致析構函數被調用了兩次,那么如果類中含有動態的資源比如有一個指針成員變量指向了堆上開辟的空間,那么此時執行了兩次析構函數,那么意味著會連續釋放兩次空間,這是一個未定義行為,而手動二次調用析構函數的場景一般是上文所說的后面要進行placement new的時候,那么可以手動調用析構函數
而這里編譯器會先調用析構函數來清理資源,然后下一步便是調用operator delete函數,那么operator delete函數內部封裝了free函數,所以operator delete函數底層是調用free函數,那么意味著operator new函數會接收一個指針,將指針指向的空間給釋放,那么這就是delete運算符涉及到的底層的過程
//你的視角
person* ptr=new person;
//編譯器的視角
ptr->~person();
operator delete(ptr);
2.不要new和free以及malloc和delete混用
那么有的小伙伴喜歡new一個對象,然后調用free函數來釋放new出來的對象,或者調用malloc開辟出一個對象,然后調用delete來釋放對象,那么這樣混用,有些時候不會報錯,但是你不可能每次都這么幸運,那么這里我們就來認識一下為什么new和free不能混用
那么這里就要涉及到一點底層了,那么你malloc以及new申請連續的內存空間,那么這些內存空間不是全部都是來存儲有效內容的,也就是它會有一部分空間來存儲這個內存空間的屬性,那么這部分就是申請的內存空間的元數據,那么這個元數據一般是位于malloc以及new的內存空間的頭部,而new以及malloc返回的地址其實不是真正意義上的所謂的申請的連續的空間的首元素地址,而是有效內容的首元素的地址,那么這個元素局存儲的就是這個內存區域的信息,而對于new以及malloc來說,那么該元數據的內容是不同的
對于new來說,那么它開頭的元數據記錄的就是你申請的元素的個數,而對于malloc來說,那么它的元數據記錄的屬性則較多,分別是該申請的內存的總大小包括了元數據以及有效數據,以及一個內存的狀態以及空閑列表等等,所以當你new一個對象,然后調用free函數的時候,那么free函數默認這個空間是調用malloc開辟的,那么它會首先解析該空間的元數據,那么它由于接收的是有效數據的首元素的地址,那么它首先就得向前移動一個偏移量到元數據所在位置,然后獲取該malloc開辟的空間的總大小,但是由于此時是用new運算符開辟出來的對象,那么它的元數據中只有元素的個數,那么這樣就會導致free錯誤的計算出空間的大小,那么會導致非法訪問內存,從而引發段錯誤,同理delete和malloc混用的話,對于delete它只需知道元素個數即可,因為每一個元素的大小,則是由編譯器根據指針指向的對象的類型可以計算得到,那么由于malloc的元數據的構造和new的元數據的構造不同,那么此時delete會錯誤解析得到釋放的元數個數
malloc的元數據內存布局:
±---------------±---------------±---------------+
| 塊大小 | 分配狀態 | 用戶可用內存空間
±---------------±---------------±---------------+
new的元數據內存布局:
±---------------±---------------±----±-----------
| 元素個數 (size_t) |
±---------------±---------------±----±-----------
結語
那么這就是本篇博客關于new和delete介紹的所有內容了,那么下一期博客,我會介紹模版,那么學會了模版之后,我們便可以進入c++的STL的學習了,那么我會持續更新,希望你多多關注與支持,那么如果本文有幫組到你的話,還請三連加關注哦,你的支持,就是我創作的最大的動力!