C++類中隱藏的幾個(gè)默認(rèn)函數(shù)你知道嗎
Test類中隱藏的六個(gè)默認(rèn)的函數(shù)
class Test { public: //默認(rèn)的構(gòu)造函數(shù) Test(); //析構(gòu)函數(shù) ~Test(); //拷貝構(gòu)造函數(shù) Test(const Test &t); //賦值函數(shù) Test& operator=(const Test &x); //一般對(duì)象取地址函數(shù) Test* operator&(); //常對(duì)象取地址函數(shù) const Test* operator&()const; private: int data; //int *data; // 注意:如果成員中含有指針類型,需重載拷貝函數(shù)與賦值函數(shù) // 否則會(huì)造成淺拷貝 // 另外,需要注意在析構(gòu)函數(shù)中,釋放類中使用的額外資源(堆區(qū)申請(qǐng)的資源) };
1.構(gòu)造函數(shù)
作用:對(duì)象所在的內(nèi)存空間做初始化 、給對(duì)象賦資源
特點(diǎn):
1.可以重載 :可以根據(jù)實(shí)際需要進(jìn)行缺省的、多參重載
2.不依賴對(duì)象:對(duì)象無法調(diào)用構(gòu)造函數(shù),只能在對(duì)象定義點(diǎn)被調(diào)用
//成員函數(shù)類外實(shí)現(xiàn),需在函數(shù)名前指定作用域,否則編譯器會(huì)認(rèn)為在定義一個(gè)普通的函數(shù) Test::Test() //類中默認(rèn)的構(gòu)造函數(shù) { } //此外,構(gòu)造函數(shù)可以支持重載,我們可以根據(jù)需要自己寫一些構(gòu)造函數(shù) //需要注意的是,如果我們自己寫了構(gòu)造函數(shù),那么編譯器就不會(huì)提供默認(rèn)的構(gòu)造函數(shù)了 Test::Test(int d = 0 ) //缺省的構(gòu)造函數(shù) { data = d; } Test::Test(int d = 0 ):data(d) //缺省的構(gòu)造函數(shù),用初始化列表的方式初始化 { }
兩者初始化的區(qū)別在于,初始化列表是真正意義上的初始化,它告訴編譯器在實(shí)例化對(duì)象的時(shí)候以何種方式對(duì)成員賦值,而在前者的賦值規(guī)則寫在了構(gòu)造函數(shù)內(nèi)部,是在已經(jīng)生成了成員變量之后再進(jìn)行的賦值操作。
初始化列表示例:
Tips: 注意區(qū)分列表參數(shù)初始化和列表初始化的區(qū)別 。列表參數(shù)初始化即在函數(shù)的形參列表后通過 fun(int ia) :mval(ia)
冒號(hào)+括號(hào)的這種方式初始化,而列表初始化一般是指如 std::vector<int> vec{ 1,2,3,4,5 }; vec{1,2,3,4,5};
這種,在定義時(shí)通過 { } 括起來的列表初始化“數(shù)組”的行為。 事實(shí)上,在C++11標(biāo)準(zhǔn)中還有一種就地初始化的概念,這里先不做討論。
對(duì)于 初始化列表 有幾點(diǎn)特性需要注意:
比如以下操作,成員變量有引用類型和const類型,在C++中規(guī)定const類型為一個(gè)常量,定義時(shí)必須初始化,而引用我們認(rèn)為是一個(gè)變量的別名也需要在定義時(shí)就初始化。所以以下操作只能使用初始化列表的方式初始化。
class Test { public: /* error 常量、引用在定義時(shí)就初始化 Test(int a, int b,int c) { ma = a; mb = a; // error mc = a; // error } */ Test(int a, int b, int c):ma(a),mb(b),mc(c) { } private: int ma; int& mb; const int mc; };
此外,如果有多個(gè)成員變量需要使用初始化列表的方式初始化,需要注意一點(diǎn)細(xì)節(jié),初始化的順序只與成員變量的定義順序相關(guān)。
如以下程序,可以寫成Test(int a):ma(mb), mb(a){}
或Test(int a):mb(a),ma(mb){}
因?yàn)槌蓡T變量的定義順序?yàn)?code>int mb; int ma;,也就是說賦值順序與初始化列表無關(guān),只與成員變量被定義的順序有關(guān)。
class Test { public: Test(int a):ma(mb), mb(a) //mb先被定義出來,先給mb賦值,再給ma賦值 { } /* 下面錯(cuò)誤的寫法: 解釋: 1. mb先定義,ma后定義,兩者的使用參數(shù)列表初始化的順序是先 mb, 再 ma 2. 在初始化之前 ma 與 mb 都是隨機(jī)值,或被填充為0xcccccccc (具體看編譯器實(shí)現(xiàn)) 3. 在初始化時(shí), mb(ma) ,則mb被初始化為無效值(隨機(jī)值或0xcccccccc) ma(a) , ma 被初始化為 a 的值。 因此,如果調(diào)用Test(10), 則 mb: -858993460 ma: 10 */ Test(int a) :mb(ma), ma(a) { } public: void Show() { std::cout << "ma: " << ma << std::endl; std::cout << "mb: " << mb << std::endl; } private: int mb; int ma; };
注:以下函數(shù)的Test類成員均為 int *ma
,表示數(shù)據(jù)成員為指針時(shí),各成員函數(shù)的實(shí)現(xiàn)方法。
2.析構(gòu)函數(shù)
作用:釋放對(duì)象所占的其他資源。
特點(diǎn):
不可重載 : 對(duì)象銷毀時(shí)會(huì)調(diào)用析構(gòu)函數(shù),并釋放空間。依賴對(duì)象:可手動(dòng)調(diào)用即this->~Test()
或 Test t; t.~Test()
,但是不建議,因?yàn)閷?duì)象銷毀時(shí)會(huì)自動(dòng)調(diào)用,如果手動(dòng)調(diào)用可能會(huì)引起內(nèi)存空間的重復(fù)析構(gòu)導(dǎo)致程序崩潰
//默認(rèn)的析構(gòu)函數(shù) Test::~Test() { //沒有額外的資源,什么都不寫 }
//如果程序中有額外的空間需要釋放 class Test { public: //構(gòu)造函數(shù) Test(int ia = 0) { data = new int{ ia }; //data指向一塊堆區(qū)內(nèi)存 } //析構(gòu)函數(shù) ~Test(); private: int* data; }; //析構(gòu)函數(shù) Test::~Test() { delete data; //把額外空間的釋放寫進(jìn)析構(gòu)函數(shù) data = nullptr; }
3.拷貝構(gòu)造函數(shù)
作用:拿一個(gè)已存在的對(duì)象來生成相同類型的新對(duì)象
注意:類中提供的拷貝構(gòu)造函數(shù)為一個(gè)淺拷貝類型,即如果成員變量中含有指針類型,它在進(jìn)行拷貝構(gòu)造的時(shí)候不會(huì)進(jìn)行額外空間的開辟,最終會(huì)造成函數(shù)析構(gòu)時(shí)的錯(cuò)誤。
class Test { public: //構(gòu)造函數(shù) Test(int ia = 0) { data = new int{ ia }; //data指向一塊堆區(qū)內(nèi)存 } //拷貝構(gòu)造函數(shù) Test(const Test &t); //一定要傳引用,否則在開辟形參的過程中會(huì)遞歸的調(diào)用拷貝構(gòu)造函數(shù)來構(gòu)造形參,而函數(shù)始終無法執(zhí)行 private: int* data; }; //默認(rèn)的拷貝構(gòu)造函數(shù) Test::Test(const Test &t) { data = t.data; //淺拷貝,只把現(xiàn)有的成員變量進(jìn)行拷貝,沒有對(duì)堆區(qū)內(nèi)存進(jìn)行拷貝,使多個(gè)對(duì)象的data指向了同一片堆區(qū)空間,在對(duì)象銷毀時(shí)會(huì)造成空間的重復(fù)釋放引發(fā)程序崩潰。 } //拷貝構(gòu)造函數(shù) Test::Test(const Test &t) { data = new int; //如果是字符類型data = new char[strlen(t.data) + 1]; // 注意strlen() 函數(shù)不能傳遞nullptr參數(shù) strcpy_s(data,sizeof(int), t.data); } // 或者使用初始化列表的方式 Test::Test(const Test& t) :data(new int{*(t.data)}) { }
4.賦值運(yùn)算符的重載函數(shù)
作用:拿一個(gè)已存在的對(duì)象給相同類型的已存在對(duì)象賦值
實(shí)現(xiàn)步驟
1.賦值判斷
2.釋放舊資源
3.生成新資源
4.賦值
class Test { public: //構(gòu)造函數(shù) Test(int ia = 0) { data = new int{ ia }; //data指向一塊堆區(qū)內(nèi)存 } //賦值函數(shù) Test& operator=(const Test &x); //以自身類類型的引用的方式返回 private: int* data; }; //默認(rèn)的賦值函數(shù)(淺拷貝) Test& Test::operator=(const Test &x) { if(this!=&x) //自賦值判斷 { data=x.data; //淺拷貝 } return *this; //返回自身類類型的引用 } //賦值函數(shù)(深拷貝) Test& Test::operator=(const Test &x) { if(this!=&x) //自賦值判斷 { delete data; //釋放原資源 //delete[] data; 如果申請(qǐng)的空間是多個(gè),即數(shù)組形式,需要delete [] data 釋放 data = new int; //開辟空間 memcpy(data, x.data, sizeof(data)); // 賦值 } return *this; //返回自身類類型的引用 }
5.一般對(duì)象取地址函數(shù)
//一般對(duì)象取地址函數(shù) Test::Test* operator&() { return this; }
6.常對(duì)象取地址函數(shù)
//常對(duì)象取地址函數(shù) const Test::Test* operator&()const { return this; }
C++11以后增加了右值引用的概念,同時(shí)增加了移動(dòng)構(gòu)造函數(shù)、和移動(dòng)賦值函數(shù)。
7.移動(dòng)構(gòu)造函數(shù)
作用:針對(duì)某些情況構(gòu)造對(duì)象的優(yōu)化,避免重復(fù)的開辟內(nèi)存。
使用場(chǎng)景:比如把臨時(shí)對(duì)象的資源作為構(gòu)建新對(duì)象的資源使用,而臨時(shí)對(duì)象銷毀時(shí),資源繼續(xù)被其他對(duì)象使用(這里就節(jié)省了一次舊對(duì)象資源的的銷毀與新對(duì)象資源申請(qǐng)的開銷)。
class Test { public: //構(gòu)造函數(shù) Test(int ia = 0) :data(new int{ ia }) {} // 拷貝構(gòu)造 .. // 賦值.. // 析構(gòu).. // 移動(dòng)構(gòu)造 Test(Test&& rhs) { this->data = rhs.data; // 資源的轉(zhuǎn)移 rhs.data = nullptr; // 資源的釋放 } /* //使用初始化列表的方式 Test(Test&& rhs) :data(rhs.data) { rhs.data = nullptr; } */ public: void print() { std::cout << (void*)data << std::endl; } private: int* data; }; int main() { Test t(10); t.print(); Test t2(std::move(t)); // 把 t 的資源轉(zhuǎn)移給 t2 t.print(); t2.print(); return 0; }
8.移動(dòng)賦值函數(shù)
作用:資源的轉(zhuǎn)移,針對(duì)某些情況下,節(jié)省內(nèi)存的開辟。
class Test { public: //構(gòu)造函數(shù) Test(int ia = 0) :data(new int{ ia }) {} // 拷貝構(gòu)造 .. // 賦值.. // 析構(gòu).. // 移動(dòng)構(gòu)造 // 移動(dòng)賦值 Test& operator=(Test&& rhs) noexcept // 不拋出異常 { if (this != &rhs) // 防止自賦值 { delete data; // 銷毀當(dāng)前資源 this->data = rhs.data; // 轉(zhuǎn)移資源,即接收對(duì)方資源 rhs.data = nullptr; // 對(duì)方放棄資源的擁有 } return *this; } public: void print() { std::cout << (void*)data << std::endl; } private: int* data; }; int main() { Test t(10), t2(20); t.print(); t2.print(); t2 = std::move(t); // 把 t 的資源交給 t2 t.print(); // 輸出 00000000 t2.print(); return 0; }
補(bǔ)充:
另外一個(gè)關(guān)于移動(dòng)構(gòu)造的話題是異常。對(duì)于移動(dòng)構(gòu)造函數(shù)來說,拋出異常有時(shí)是件危險(xiǎn)的事情。因?yàn)榭赡芤苿?dòng)語義還沒完成,一個(gè)異常卻拋出來了,這就會(huì)導(dǎo)致一些指針就成為懸掛指針。因此程序員應(yīng)該盡量編寫不拋出異常的移動(dòng)構(gòu)造函數(shù),通過為其添加一個(gè)noexcept關(guān)鍵字,可以保證移動(dòng)構(gòu)造函數(shù)中拋出來的異常會(huì)直接調(diào)用terminate程序終止運(yùn)行,而不是造成指針懸掛的狀態(tài)。而標(biāo)準(zhǔn)庫(kù)中,我們還可以用一個(gè)std::move_if_noexcept的模板函數(shù)替代move函數(shù)。該函數(shù)在類的移動(dòng)構(gòu)造函數(shù)沒有noexcept關(guān)鍵字修飾時(shí)返回一個(gè)左值引用從而使變量可以使用拷貝語義,而在類的移動(dòng)構(gòu)造函數(shù)有noexcept關(guān)鍵字時(shí),返回一個(gè)右值引用,從而使變量可以使用移動(dòng)語義。
關(guān)于移動(dòng)構(gòu)造函數(shù)的示例程序,引用自《深入理解C++11》一書:
#include <iostream> using namespace std; class HasPtrMem { public: HasPtrMem() :d(new int(3)) { cout<<"Construct:"<<++n_cstr<<endl; } HasPtrMem(const HasPtrMem& h) :d(new int(*h.d)) { cout<<"Copy construct:"<<++n_cptr<<endl; } HasPtrMem(HasPtrMem&& h) :d(h.d) {//移動(dòng)構(gòu)造函數(shù) h.d = nullptr;//將臨時(shí)值的指針成員置空 cout<<"Move construct:"<<++n_mvtr<<endl; } ~HasPtrMem() { delete d; cout<<"Destruct:"<<++n_dstr<<endl; } int* d; static int n_cstr; static int n_dstr; static int n_cptr; static int n_mvtr; }; int HasPtrMem::n_cstr = 0; int HasPtrMem::n_dstr = 0; int HasPtrMem::n_cptr = 0; int HasPtrMem::n_mvtr = 0; HasPtrMem GetTemp() { HasPtrMem h; cout<<"Resource from"<<__func__<<":"<<hex<<h.d<<endl; return h; } int main() { HasPtrMem a = GetTemp(); cout<<"Resource from"<<__func__<<":"<<hex<<a.d<<endl; } //編譯選項(xiàng):g++ -std=c++11 test.cpp -fno-elide-constructors
輸出:左邊是輸出結(jié)果,右邊是注釋
Construct:1 // 在GetTemp() 函數(shù)中,執(zhí)行 HasPtrMem h; 構(gòu)造對(duì)象 Resource fromGetTemp:0x1047f28 // 在GetTemp() 函數(shù)中,執(zhí)行cout << ... Move construct:1 // 在GetTemp() 函數(shù)中,return h; 產(chǎn)生臨時(shí)對(duì)象,此時(shí)第一次調(diào)用移動(dòng)構(gòu)造 Destruct:1 // 進(jìn)入main() 函數(shù)時(shí),GetTemp();調(diào)用結(jié)束、進(jìn)行清棧(棧幀回退),析構(gòu)掉局部對(duì)象( h ) Move construct:2 // 在main() 函數(shù)中,執(zhí)行 GetTemp(); 后產(chǎn)生的返回值是一個(gè)臨時(shí)無名對(duì)象,調(diào)用了移動(dòng)構(gòu)造函數(shù),此時(shí)第二次調(diào)用移動(dòng)構(gòu)造 Destruct:2 // 在main() 函數(shù)中,執(zhí)行了a = GetTemp(); 后,臨時(shí)對(duì)象生存期結(jié)束,析構(gòu)掉臨時(shí)對(duì)象(函數(shù)返回值) Resource frommain:0x1047f28 // 在main() 函數(shù)中,執(zhí)行cout << ... Destruct:3 // main() 函數(shù)結(jié)束,對(duì)象 a 生存周期結(jié)束,銷毀對(duì)象 a
需要注意的是,在編譯器中存在被稱為RVO/NRVO的優(yōu)化(RVO,Return Value Optimization,返回值優(yōu)化,或者NRVO,Named Return Value optimization)。因此在上述編譯時(shí)使用了 -fno-elide-constructors
選項(xiàng)在g++/clang++中關(guān)閉這個(gè)優(yōu)化,這樣可以使我們?cè)诖a運(yùn)行的結(jié)果中較為容易地利用函數(shù)返回的臨時(shí)量右值?!?/p>
如果在編譯的時(shí)候不使用該選項(xiàng)的話,我們寫的很多含有移動(dòng)語義的函數(shù)都被省略了。例如以下的代碼
A ReturnRvalue(){A a();return a;} A b=ReturnRvalue();
b 變量實(shí)際就使用了ReturnRvalue函數(shù)中a的地址,任何的拷貝和移動(dòng)都沒有了。通俗地說,就是b變量直接“霸占”了a變量。這是編譯器中一個(gè)效果非常好的一個(gè)優(yōu)化。不過RVO/NRVO并不是對(duì)任何情況都有效。比如有些情況下,一些構(gòu)造是無法省略的。還有一些情況,即使RVO/NRVO完成了,也不能達(dá)到最好的效果?!?/p>
總體而言,移動(dòng)語義除了可以解決某些情況下編譯器無法解決的優(yōu)化問題,還在一些其他特殊的場(chǎng)合有著重要的用途(比如在unique_ptr中禁止構(gòu)造函數(shù),卻可以通過移動(dòng)的構(gòu)造或移動(dòng)賦值對(duì)unique_ptr擁有的資源進(jìn)行轉(zhuǎn)移)。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
C++實(shí)現(xiàn)LeetCode(41.首個(gè)缺失的正數(shù))
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(41.首個(gè)缺失的正數(shù)),本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07OpenCV實(shí)現(xiàn)人臉識(shí)別簡(jiǎn)單程序
這篇文章主要為大家詳細(xì)介紹了OpenCV實(shí)現(xiàn)人臉識(shí)別簡(jiǎn)單程序,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08VC++中HTControl控件類的CHTSlider控制桿控件類簡(jiǎn)介
這篇文章主要介紹了VC++中HTControl控件類的CHTSlider控制桿控件類,需要的朋友可以參考下2014-08-08C數(shù)據(jù)結(jié)構(gòu)中串簡(jiǎn)單實(shí)例
這篇文章主要介紹了C數(shù)據(jù)結(jié)構(gòu)中串簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-06-06C++ 開發(fā)之實(shí)現(xiàn)操作符重載的實(shí)例
這篇文章主要介紹了C++ 開發(fā)之實(shí)現(xiàn)操作符重載的實(shí)例的相關(guān)資料,這里附有實(shí)例代碼和實(shí)現(xiàn)效果圖幫助大家參考實(shí)踐,需要的朋友可以參考下2017-07-07C++深入探索類真正的形態(tài)之struct與class
前邊我們所定義的類,均是使用struct關(guān)鍵字來定義,但是C++中真正用于定義類的關(guān)鍵字為class,因?yàn)橐狢++兼容C,所以保留struct關(guān)鍵字,struct與class的用法完全相同2022-04-04