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