幾乎你寫的每一個(gè)class都會(huì)有一個(gè)或多個(gè)構(gòu)造函數(shù)、一個(gè)析構(gòu)函數(shù),一個(gè)copy assignment操作符。如果這些函數(shù)犯錯(cuò),會(huì)導(dǎo)致深遠(yuǎn)且令人不愉快的后果,遍及你的整個(gè)classes。所以確保它們行為正確時(shí)生死攸關(guān)的大事。本章提供的引導(dǎo)可讓你把這些函數(shù)良好地集結(jié)在一起,形成classes的脊柱。
條款05:了解C++默默編寫并調(diào)用哪些函數(shù)
如果你自己沒有聲明,編譯器就會(huì)聲明
- 默認(rèn)構(gòu)造函數(shù)
- copy構(gòu)造函數(shù)? ? ? ? ? ? ? ? ? ??//單純地將來源對(duì)象的每一個(gè)non-static成員變量拷貝到目標(biāo)對(duì)象
- copy?assignment操作符? ? //同上
- 析構(gòu)函數(shù)? ? ? ? ? ? ? ? ? ? ? ? ? ? //是個(gè)non-virtual
唯有這些函數(shù)被需要(被調(diào)用),它們才會(huì)被編譯器創(chuàng)建出來,下面代碼造成上述每一個(gè)函數(shù)被編譯器產(chǎn)出:
Empty e1; //默認(rèn)構(gòu)造函數(shù) //析構(gòu)函數(shù) Empty e2(e1); //拷貝構(gòu)造函數(shù) e2=e1; //copy assignment操作符
注意:
如果生成的代碼不合法或者沒有意義時(shí),編譯器會(huì)拒絕為class生成operate=:
(1)不合法
#include <string> #include <iostream> using namespace std; class Dog { public: Dog(string& namevalue, int agevalue):name(namevalue),age(agevalue){ //name = namevalue; 當(dāng)需要初始化const修飾的類成員/引用成員數(shù)據(jù)應(yīng)使用成員初始化列表 //age = agevalue; } void show() { cout << name << " is " << age << " years old." << endl; } private: string& name; const int age; }; int main() { string s1("Persephone"); string s2("Satch"); Dog d1(s1, 2); Dog d2(s2, 36); //Dog d1("Persephone", 2); //Dog d2("Satch", 36); //d1 = d2;//無法引用 函數(shù) "Dog::operator=(const Dog &)" (已隱式聲明) -- 它是已刪除的函數(shù)
d1.show();
d2.show();
getchar();
}
由于C++不允許“讓引用改指向不同的對(duì)象”,編譯器沒有自動(dòng)生成operate=,d1=d2就會(huì)報(bào)錯(cuò)。
(2)沒有意義
如果某個(gè)base classes將copy assignment操作符聲明為private,編譯器將拒絕為其derived classes生成一個(gè)copy assignment操作符。畢竟編譯器為derived classes所生的copy assignment操作想象中可以處理base class成分(條款12)。
條款06:若不想使用編譯器自動(dòng)生成的函數(shù),就應(yīng)該明確拒絕
有些類,你不想它的對(duì)象被拷貝,但如果你不聲明拷貝構(gòu)造函數(shù),編譯器會(huì)自動(dòng)給你生成。這時(shí)候我們可以,
1)將拷貝構(gòu)造函數(shù)和copy assignment操作符聲明為private(此時(shí)member函數(shù)和friend函數(shù)還是可以調(diào)用你的private函數(shù)),并且不去定義它。
2)寫一個(gè)base class,它有私有的拷貝構(gòu)造函數(shù)和copy assignment操作符,然后去繼承它,根據(jù)上一條,編譯器將拒絕為其生成一個(gè)copy assignment操作符。(可能會(huì)導(dǎo)致多重繼承
3)使用Boost提供的版本。(還沒學(xué)到
條款07:為多態(tài)基類聲明virtual析構(gòu)函數(shù)
比如在使用工廠函數(shù)時(shí),工廠函數(shù)會(huì)返回一個(gè)base class指針,指向新生成的derived對(duì)象。被返回的對(duì)象位于heap,因此為了避免泄露內(nèi)存和其他資源,需要delete該對(duì)象。
BasicCamera* pb = CreateCamera(); ... delete pb;
指針指向的時(shí)子類對(duì)象,但卻經(jīng)由一個(gè)base class指針來刪除,而目前的base class有一個(gè)non-virtual析構(gòu)函數(shù)(條款05指出,編譯器自動(dòng)生成的析構(gòu)函數(shù)是non-virtual的)。這會(huì)引來災(zāi)難,因?yàn)閷?shí)際執(zhí)行時(shí),通常發(fā)生的是,對(duì)象的derived成分沒被銷毀,base class成分通常會(huì)被銷毀,造成一個(gè)局部銷毀的對(duì)象。
解決方法:
給base class一個(gè)virtual析構(gòu)函數(shù)
注意:
- 任何class只要帶有virtual函數(shù)都幾乎確定應(yīng)該也有一個(gè)virtual析構(gòu)函數(shù)。
- 如果class不含virtual函數(shù),通常表示它不意圖被用作一個(gè)base class,此時(shí)令其析構(gòu)函數(shù)為virtual,會(huì)導(dǎo)致其對(duì)象的體積增大,不能傳遞至其他語言的函數(shù)。
- 總而言之,只有當(dāng)class內(nèi)含至少一個(gè)virtual函數(shù),才為它聲明virtual析構(gòu)函數(shù)
- 有時(shí)候抽象類不想被實(shí)體化,比如說BasicCamera創(chuàng)建對(duì)象沒有意義,你可以有一個(gè)pure virtual函數(shù),這時(shí)候可以聲明一個(gè)pure virtual的析構(gòu)函數(shù)。
class BasicCamera { public: virtual ~BasicCamera() = 0; };
加上virtual,加上“=0”,就成為pure virtual函數(shù)了,但是書上說“必須為這個(gè)pure virtual析構(gòu)函數(shù)提供一份定義”???由子類繼承后定義可以嗎?
試了一下,好像確實(shí)如此。
#include<iostream> using namespace std; class BasicCamera { public: virtual ~BasicCamera()=0;//如果此處為~BasicCamera(),只會(huì)調(diào)用父類的析構(gòu) }; BasicCamera::~BasicCamera() { cout << "調(diào)用了BasicCamera的析構(gòu)函數(shù)" << endl; }//不加會(huì)報(bào)錯(cuò),必須要定義 class Hik :public BasicCamera { public: ~Hik() { cout << "調(diào)用了hik的析構(gòu)函數(shù)" << endl; } }; class Factory { public: BasicCamera* CreateCamera() { return new Hik(); } }; int main() { Factory fac = Factory(); BasicCamera* camera = fac.CreateCamera(); delete camera; getchar(); return 0; }
運(yùn)行結(jié)果:
?可以看出,析構(gòu)函數(shù)的運(yùn)作方式是,最深層派生的那個(gè)class(此處為子類hik)的那個(gè)析構(gòu)函數(shù)最先被調(diào)用,然后是每一個(gè)base class的析構(gòu)函數(shù)被調(diào)用。編譯器會(huì)在子類的析構(gòu)函數(shù)中調(diào)用父類的析構(gòu)函數(shù),所以必須提供一份定義。
條款08:別讓異常逃離析構(gòu)函數(shù)
如果析構(gòu)函數(shù)吐出異常程序可能過早結(jié)束或出現(xiàn)不明確行為。比如,HikCamera類在析構(gòu)時(shí)拋出異常:
#include<iostream> using namespace std; class HikCamera { public: ~HikCamera() { throw 1; closed = true; } private: bool closed = false; }; int main() { { HikCamera cam; } getchar(); return 0; }
上圖可以看出,調(diào)用了abort函數(shù)終止了程序。但如果析構(gòu)函數(shù)必須執(zhí)行一個(gè)動(dòng)作,而該動(dòng)作可能會(huì)在失敗時(shí)拋出異常,該怎么辦?
1. 如果拋出異常就結(jié)束程序(通常通過abort完成):
~HikCamera() { try { throw 1; closed = true; } catch (int i) { abort(); } }
如果程序遭遇一個(gè)“于析構(gòu)期間發(fā)生的錯(cuò)誤”后無法繼續(xù)執(zhí)行,“強(qiáng)迫結(jié)束程序”是個(gè)合理選項(xiàng)。畢竟它可以阻止異常從析構(gòu)函數(shù)傳播出去(那會(huì)導(dǎo)致不明確的行為)。也就是說調(diào)用abort可以搶先置“不明確行為”于死地。
2.吞下因調(diào)用close而引發(fā)的異常
~HikCamera() { try { throw 1; closed = true;} catch (int i) { cout << "close函數(shù)中有異常" << endl; } }
3. 將異常放在析構(gòu)函數(shù)之外
提供一個(gè)close函數(shù),賦予客戶機(jī)會(huì)對(duì)可能出現(xiàn)的問題作出反應(yīng)。同時(shí)可以在析構(gòu)函數(shù)中追蹤,由析構(gòu)函數(shù)關(guān)閉之。
class HikCamera { public: void close() { throw 1; closed = true; } ~HikCamera() { if(!closed){ try { close(); } catch (int i) { cout << "close函數(shù)中有異常" << endl; }} } private: bool closed = false; };
條款09:絕不在構(gòu)造和析構(gòu)過程中調(diào)用virtual函數(shù)
?假如在構(gòu)造函數(shù)中調(diào)用了virtual函數(shù):
class BasicCamera { public: BasicCamera() { open();//調(diào)用了virtual函數(shù) } virtual void open() { cout << "打開了相機(jī)" << endl; } }; class HikCamera:BasicCamera{ public: HikCamera() { cout << "創(chuàng)建了hik相機(jī)" << endl; } void open() { cout << "打開了??迪鄼C(jī)" << endl; } };
運(yùn)行結(jié)果:
在創(chuàng)建子類對(duì)象時(shí),不會(huì)創(chuàng)建父類對(duì)象,只是初始化子類中屬于父類的成員。父類的構(gòu)造函數(shù)會(huì)被調(diào)用,運(yùn)行父類構(gòu)造函數(shù)時(shí),里面的父類版本的virtual函數(shù)被調(diào)用了,不是子類中的版本。
原因
1. 當(dāng)基類的構(gòu)造函數(shù)執(zhí)行時(shí),子類的成員變量尚未初始化,如果virtual函數(shù)(open函數(shù))調(diào)用了子類成員變量,這會(huì)導(dǎo)致不明確行為。
2. 基類構(gòu)造期間,對(duì)象類型為基類。不只是virtual函數(shù)被編譯器解析至基類,若使用運(yùn)行期類型信息,也會(huì)把對(duì)象視為基類。
同樣的道理也適用于析構(gòu)函數(shù)。一旦子類的析構(gòu)函數(shù)開始執(zhí)行,子類成員變量就會(huì)呈現(xiàn)未定義值,進(jìn)入基類析構(gòu)函數(shù)后對(duì)象成為基類對(duì)象。
想要確保每一次有子類被建立就調(diào)用對(duì)應(yīng)的open(),
解決方法
把父類的virtual函數(shù)改為不virtual的,然后子類的構(gòu)造函數(shù)傳遞必要信息給父類的構(gòu)造函數(shù),父類的構(gòu)造函數(shù)就可以調(diào)用non-virtual的open()了。像這樣:
#include<iostream> using namespace std; class BasicCamera { public: BasicCamera(const string& info) { open(info); } void open(const string& info) { cout << "打開了" << info << "相機(jī)" << endl; }; }; class HikCamera:BasicCamera{ public: HikCamera(string s):BasicCamera(s) { } }; int main() { HikCamera cam("Hik"); getchar(); return 0; }
換句話說,無法使用virtual函數(shù)從基類向下調(diào)用,在構(gòu)造期間,你可以“令子類將必要的構(gòu)造信息向上傳遞至基類構(gòu)造函數(shù)”。
條款10:令operate=返回一個(gè)reference to *this
關(guān)于賦值,可以將它們寫成連鎖的形式:
int x, y, z; x = y = z = 15; //賦值連鎖形式
因?yàn)橘x值所采用的是右結(jié)合律,因此,上面的連鎖賦值可以解析為:
x = (y = (z = 15));
為了實(shí)現(xiàn)這樣的“連鎖賦值”,賦值操作符必須返回一個(gè)reference,指向操作符的左側(cè)的實(shí)參,這也是為classes實(shí)現(xiàn)賦值操作符是應(yīng)該遵守的協(xié)議:
class Widget { public: ... Widget& operator=(const Widget& rhs) //返回類型是一個(gè)reference,指向當(dāng)前的對(duì)象 { ... return* this; //返回左側(cè)的對(duì)象 } ... };
這個(gè)協(xié)議不僅適用于以上的標(biāo)準(zhǔn)賦值形式,也適用于所有賦值相關(guān)運(yùn)算。例如:+=,*=,-=等。需要注意的是,這只是一個(gè)協(xié)議,并不是強(qiáng)制的。但是,最好還是這樣做,因?yàn)檫@份協(xié)議被所有的內(nèi)置類型和標(biāo)準(zhǔn)程序庫提供的類型如string,vector,complex,tr1::shared_ptr或即將提供的類型所共同遵守的。
條款11:在operate=中處理“自我賦值”
自我賦值
“自我賦值”發(fā)生在對(duì)象被賦值給自己時(shí):
class Widget{ ... }; Widget w; ... w = w; //賦值給自己
看著有些愚蠢,但它合法,不要以為大家絕對(duì)不這么做。以下為某些不明顯“自我賦值”產(chǎn)生的情況:
(1)指針/引用指向同一個(gè)對(duì)象
a[i] = a[j]; //當(dāng)i = j時(shí) *px = *py // 當(dāng)兩個(gè)指針指向同一個(gè)東西時(shí)
(2)父類的指針和子類的指針指向同一個(gè)對(duì)象
class Base { ... }; class Derived: public Base { ... }; void doSomething(const Base& rb, Derived* pd); //rb和pd可能是同一個(gè)對(duì)象
自我賦值的后果
在我們打算自行管理資源時(shí),自我賦值的情況可能會(huì)使我們掉進(jìn)“在停止使用資源之前意外釋放了它”的陷阱。假設(shè)建立一個(gè)class,來保存一個(gè)指針指向一塊動(dòng)態(tài)分配的位圖(bitmap):
class Bitmap{ ... }; class Widget{ ... private: Bitmap* pb; //指針,指向一個(gè)從heap分配而得的對(duì)象 };
下面是operate=的實(shí)現(xiàn)代碼,
Widget& operate=(const Widget& rhs){ delete pb; //停止使用當(dāng)前的bitmap pb = new Bitmap(rhs.pb); //使用rhs‘s bitmap的副本(不new的話,就指向一個(gè)bitmap了 return *this }
表面合理,但自我賦值出現(xiàn)時(shí)不安全:
Widget rhs; Widget w = rhs; //這是ok的 rhs = rhs; //第一步,將rhs.pb指向的對(duì)象刪掉
解決方法
想要阻止這種做法,有以下方法:
(1)增加“證同測試”
Widget& operate=(const Widget& rhs){ if (this == &rhs) return *this; //如果是自我賦值,不要做任何事 delete pb; pb = new Bitmap(rhs.pb); return *this }
這樣做行得通,具備了“自我賦值安全性”,但是還不具備“異常安全性”。如果”new Bitmap“導(dǎo)致了異常,Widget最終還是有一個(gè)指針指向一塊被刪除的Bitmap。這樣的指針有害,你無法安全地刪除它們,甚至無法安全地讀取它們。
(2)在復(fù)制pb所指東西之前不要?jiǎng)h除pb:
Widget& operate=(const Widget& rhs){ Bitmap* pOrig = pb; //pb和pOrig指向一個(gè)對(duì)象 pb = new Bitmap(rhs.pb); //pb指向rhs.pb的副本 delete pOrig ; //利用pOrig刪去舊對(duì)象 return *this }
同時(shí)具備了“自我賦值安全性”和“異常安全性”。
(3)copy and swap技術(shù)
swap(Widget& rhs){ ... } //交換*this和rhs的數(shù)據(jù) Widget& operate=(const Widget& rhs){ Widget temp(rhs); //為rhs的數(shù)據(jù)做一個(gè)復(fù)件 swap(temp); //將*this數(shù)據(jù)和上述復(fù)件的數(shù)據(jù)交換 return *this }
如果創(chuàng)建指針的話,需要delete掉,但是創(chuàng)建對(duì)象的話,析構(gòu)函數(shù)會(huì)delete掉指針。temp拷貝rhs的成功說明new Bitmap沒有異常,然后swap。
更激進(jìn)版:
swap(Widget& rhs){ ... } //交換*this和rhs的數(shù)據(jù) Widget& operate=(Widget rhs){ swap(rhs); //將*this數(shù)據(jù)和上述復(fù)件的數(shù)據(jù)交換 return *this }
傳入值時(shí),會(huì)自動(dòng)生成復(fù)件。此時(shí)的rhs就是原rhs的拷貝。
條款12:復(fù)制對(duì)象時(shí)勿忘其每一個(gè)成分
copy構(gòu)造函數(shù)和copy assignment操作符我們稱之為copying函數(shù),編譯器會(huì)自動(dòng)為我們的classes創(chuàng)建copying函數(shù),將拷貝對(duì)象的所有成員變量都做一份拷貝。如果你聲明自己的copying函數(shù),可能會(huì)導(dǎo)致:
1. 如果你漏了一個(gè)成員變量沒有復(fù)制,大多數(shù)編譯器不會(huì)告訴你
2. 繼承時(shí),子類的copying函數(shù)只復(fù)制了子類的成員,而父類的成員變量會(huì)被默認(rèn)的構(gòu)造函數(shù)初始化
?
PriorityCustomer的copying函數(shù)沒有復(fù)制Customer成員變量,PriorityCustomer的copy構(gòu)造函數(shù)并沒有指定實(shí)參傳給其base class構(gòu)造函數(shù),因此PriorityCustomer對(duì)象的Customer成分會(huì)被不帶實(shí)參的父類default構(gòu)造函數(shù)初始化(上面的是偽代碼,父類必須要有默認(rèn)構(gòu)造函數(shù))。父類的default構(gòu)造函數(shù)將對(duì)name和lastTransaction執(zhí)行缺省的初始化動(dòng)作。
base class 的成分往往是private, 所以你無法直接訪問他們,你應(yīng)該讓derived class的copying函數(shù)調(diào)用相應(yīng)的base class函數(shù):
#include<iostream> using namespace std;
//--------父類-------------------------- class BasicCamera { public: BasicCamera() {}//父類必須要又默認(rèn)構(gòu)造函數(shù),如果子類沒有在拷貝構(gòu)造的過程拷貝父類成員,那么子類會(huì)調(diào)用父類的默認(rèn)構(gòu)造函數(shù),對(duì)父類的成員變量執(zhí)行缺省的初始化。 BasicCamera(const BasicCamera& rhs) { name = rhs.name; } BasicCamera& operator=(const BasicCamera& rhs) {} void setname(string s) { name = s; } void show() { cout << "name is " << name << endl; } protected: private: std::string name; };
//---------------子類-------------- class HikCamera : public BasicCamera { public: HikCamera() {}; HikCamera(const HikCamera& rhs) :BasicCamera(rhs), price(rhs.price) { };//調(diào)用基類的copy構(gòu)造函數(shù) HikCamera& operator=(const HikCamera& rhs) { BasicCamera::operator =(rhs);//對(duì)基類成分進(jìn)行賦值 price = rhs.price; return *this; }; void set(int x, string s) { BasicCamera::setname(s); price = x; } void show() { BasicCamera::show(); cout << "price is " << price << endl; } protected: private: int price; }; int main() { HikCamera cam; string s = "hik"; cam.set(100, s); HikCamera cam1(cam); cam1.show(); getchar(); }
運(yùn)行結(jié)果
如果沒有BasicCamera(rhs),運(yùn)行結(jié)果:
?
總結(jié):
當(dāng)你編寫一個(gè)copying函數(shù),請(qǐng)確保
1.復(fù)制所有的local成員變量,
2.調(diào)用所有的base class內(nèi)的適當(dāng)?shù)腸opying函數(shù)。
當(dāng)這兩個(gè)copying函數(shù)有近似相同的實(shí)現(xiàn)本體,令一個(gè)copying函數(shù)調(diào)用另一個(gè)copying函數(shù)無法讓你達(dá)到你想要的目標(biāo)。
總結(jié):
05:編譯器可以暗自為class創(chuàng)建default構(gòu)造函數(shù)、copy構(gòu)造函數(shù)、copy assignment操作符以及析構(gòu)函數(shù)。
08:析構(gòu)函數(shù)絕對(duì)不要吐出異常。如果一個(gè)析構(gòu)函數(shù)調(diào)用的函數(shù)可能拋出異常,析構(gòu)函數(shù)應(yīng)該捕捉任何異常,然后吞下它們(不傳播)或結(jié)束程序。
08:如果客戶需要對(duì)某個(gè)操作函數(shù)運(yùn)行期間拋出的異常作出反應(yīng),那么class應(yīng)該提供一個(gè)普通函數(shù)(而非析構(gòu)函數(shù)中)執(zhí)行該操作。
10:令賦值操作符返回一個(gè)reference to *this。
確保當(dāng)對(duì)象自我賦值時(shí)operate=有良好行為,其中技術(shù)包括比較“來源對(duì)象”和目標(biāo)對(duì)象“的地址、精心周到的語句順序、以及cpoy and swap。
確定任何函數(shù)如果操作一個(gè)以上的對(duì)象,而其中多個(gè)對(duì)象是同一個(gè)對(duì)象時(shí),其行為仍然正確。
12:Copying函數(shù)應(yīng)該確保復(fù)制“對(duì)象內(nèi)的所有成員變量”及”所有base class成分“。
12:不要嘗試以某個(gè)copying函數(shù)實(shí)現(xiàn)另一個(gè)copying函數(shù)。應(yīng)該將共同機(jī)能放進(jìn)第三個(gè)函數(shù)中,并由兩個(gè)copying函數(shù)共同調(diào)用。
?參考:
1. 《Effective C++》P34-60
本文摘自 :https://www.cnblogs.com/