========================
Effective C++ 構造、析構、賦值運算
書作者:Scott Meyers
原筆記作者:Justin
========================
Item 5、6 :C++默認編寫的函數
-----------------------------------------
tag: 默認拷貝函數 copy assignment
如果沒有定義類的構造函數/拷貝函數/拷貝構造函數/析構函數,編譯器會自作主張幫你定義這些函數,而且還是public外加inline的。(估計這是最冷的冷飯了)
幫寫拷貝函數時僅限于幫你處理一下諸如int的內嵌類型(build-in type)以及自身就有拷貝函數的類對象,再復雜一點的就搞不定了。
有時候需要禁止某個類對象的拷貝操作,就要避免以上函數的自動生成,需要自己定義這些函數,并且有以下特性
private
只是聲明,不給予實現(give no implementation)
當然要是還是覺得麻煩,干脆直接繼承Boost的noncopyable(自己寫一個類似的也可以),就可以不費吹灰之力的獲得不能拷貝的特性。
Item 7 :為多態基類聲明virtual析構函數
--------------------------------------------
tag: 多態(polymorphism) 虛函數 virtual function 析構函數
·polymorphic base classes 應將析構函數定義為虛函數(virtual function)
·當class內至少含一個virtual函數時,才聲明virtual析構函數。
否則在通過基類指針析構一個子類對象的時候,因為沒有虛表(V Table)對析構函數的指引,對象的基類部分是被毀了,對象的子類部分卻沒辦法得到析構(沒有了虛表中的說明它根本不知道有子類的析構函數來析構子類部分!),從而導致內存泄漏。
virtual函數實現:
要實現virtual,vptr(virtual table pointer)指針指向一個向函數指針構成的數組,稱為vtbl(virtual table);
帶有virtual函數的class都有一個對應的vtbl,對象調用virtual函數時,實際被調用的函數取決于該對象的vptr所指的那個vtbl(在其中尋找適當的函數指針)
假設基類指針在析構時要做的事情是奉命救人:指針啊指針,XX地發大水了,你趕緊去把張三一家人(子類對象)救出來。
指針拿著指令(析構函數)就找到了張三(張三家家長,子類對象的基類部分):張三是吧,你一家人都在這里了吧,跟我走吧。(析構吧,回空余內存里去吧@#¥%)
如果張三已經沒有其他親戚了,他就是張三家的全部人(這個對象其實就是基類對象)。很簡單,直接跟著指針走就完事了。
如果張三還有個小弟叫張三瘋(張三家除張三外的一部分,即子類對象的子類部分),張三就必須拿著族譜(V Table)跟指針說哎呀這是我弟啊,把他也析構了吧,張三家對您感恩不盡……也很簡單,一并帶走就完成任務了(完整析構/釋放了整個對象)
如果,張三沒了族譜,記不得有個張三瘋弟弟,傻乎乎的跟了指針走,大家也以為張三一家都被救出來了。災區里就只剩下張三瘋一個人在瘋跑了(memory leak)
另一方面,如果不準備把某個類用作多態中的基類,就沒必要定義析構函數為虛函數。這樣除了增加代碼大小開銷外(因為需要空間存放虛表),沒有任何好處。(這個倒是很多書上沒說過的)
如果張三家本來就只有張三一個人,他就沒必要還要帶著個空白的族譜了。逃命的時候能扔就扔嘛。
最后提到的一點和抽象類有關。抽象類是有純虛函數的類,只能作為接口,不能有自己的對象。在設計軟件框架的時候會有需要用到抽象類的時候??墒侨绻羞@么一個類,需要設計為抽象類,但是卻找不到一個成員函數可以拉去做純虛函數,那么這個時候就把析構函數定義為純虛函數好了。只是同時還要給它一個空的實現。
class::~class() {}
Item 8 :別讓異常逃離析構函數
-------------------------------------------
tag:異常 try catch 析構函數
·在析構函數中不能拋出異常。 若被析構函數調用的函數可能會拋出異常,析構函數應該捕捉任何異常,并吞下(不傳播)或結束程序。
·若一個操作可能出現異常,客戶需要對這個異常作出反應。class應提供一個普通函數來執行該函數,而不是在析構函數內調用。
異常不像函數調用,一旦拋出(throw),就回不來了。
如果在析構函數中拋出了異常,有一部分釋放/摧毀對象成員的操作可能就無法進行。因為某個異常,導致了所在的析構函數無法完成全部析構任務。
可是要是真的需要對析構動作中出現的錯誤/異常進行處理咋辦?書中自有解決方案:從差的到好的。
在析構函數內布置catch,一旦發生異常就會被捕獲,然后簡單調用std::abort自殺
點評:干脆是干脆了,但是這樣猝死會不會有點太突然?
也是在函數內布置catch,但是遇到異常就把它直接吃到肚子里(大師語:Swallow the exception)。
點評:一般不該這樣處理,畢竟發生了不好的事。但如果真的想要程序繼續帶傷上陣,也確定這樣不會有問題,那也不會有人有意見。
除了在函數里布置catch,并采用以上任一方法,另外實現一個可供用戶調用的函數接口,用來處理這些有可能出錯的析構工作。
點評:大師給予這個方案高度的評價,因為這樣不但有以上兩種方法的效果,還給用戶一個參與處理異常的機會(調用接口函數)。如果用戶沒有(或者忘記)用該函數處理析構的動作,那么析構函數也會自覺挑起這個任務。而這個時候如果還出問題,用戶也沒有什么理由來責怪了:是你自己不想要管的!
Item 9 :不在構造和析構過程中調用 virtual 函數
-----------------------------------------------------
tag:
擁有虛函數的類就有虛表,虛表可能會引發子類相應虛函數的調用,在這些調用中有可能對某些子類對象成員的訪問。
在構造一個子類對象時: 開始構造父類部分 -> 完成父類部分并開始構造子類部分 -> 完成子類部分 (完成整個構造工作)
析構一個子類對象的時: 開始析構子類部分 -> 子類析構完畢并開始析構父類部分 -> 完成析構父類部分(完成整個析構工作)
在構造函數的第一步,子類對象成員還不存在,調用虛函數有可能會訪問不存在的子類成員;
哪怕到了第二步,因為子類對象還沒有完全構造完畢,此時調用虛函數也是危險的。事實上在整個構造過程中,該對象都被視作一個父類對象。
反過來也是同樣道理,在析構函數的第一步,子類成員已經開始銷毀,不能調用虛函數;到了第二步,整個子類部分都沒有了,更不能用虛函數了。
而在整個析構過程中,該對象也是被看作是父類對象的。
確保虛函數不會在對象構造/析構過程中被調用:
方法之一就是用參數傳遞代替虛函數機制。
把可能被構造函數調用的虛函數改成非虛函數,然后讓父類的構造函數將需要的信息/數據通過參數傳遞給子類構造函數。
Class Parent
{
public :
Parent();
Parent( const std:: string & WordsFromChild){
DoStuff(WordsFromChild);
// ..
};
void DoStuff( const std:: string & WordsFromChild);
}
Class Child : public Parent
{
public :
Child( /**/ /* some parameters here */ ) : Parent(TellParent( /**/ /* some parameters here */ )) {
// ..
};
private :
static std:: string & TellParent( /**/ /* some parameters here */ );
}
也許看到這里會想:要是TellParent()中訪問了未被初始化的子類成員呢?那不也是一樣有問題么?
注意,這就是為什么前面有個static限定的原因。因為靜態函數可以在對象的初始化階段就開始工作,更詳細的描述看這里。
子類的虛表在子類的構造函數時生成,所以在父類的構造函數中調用虛函數使用的是父類的版本。
子類和父類對象都會有自己的虛表,里面安置的是自己版本的虛函數實現。
Item 10-12 : 拷貝運算符
--------------------------
tag:assignment operator(賦值運算符) 自我賦值 copying函數
·令賦值(assignment)操作符返回一個reference to *this。 可實現(a=b=c)。
·處理自我賦值
·構造函數用來初始化新對象,而 assignment操作符只施行于已初始化對象身上。
·copying函數應該確保復制“對象內所有成員變量”和“所有 base class成分”
1、
在函數入口檢查是否屬于自拷貝(例如:檢查指針是否指向同一片內存),如果是,啥也不干直接返回。否則屬于正常情況的拷貝。
這樣解決了self-assignment-unsafe的問題,但是沒能避免exception-unsafe。
2、
第二種方法比較簡單,只是整理一下指令的順序。但是卻同時解決了自賦值和拋出異常帶來的問題。繼續無恥的抄寫代碼一段:
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; // remember original pb
pb = new Bitmap(*rhs.pb); // make pb point to a copy of *pb
delete pOrig; // delete the original pb
return *this;
}
這樣的做法在解決以上兩個問題的同時卻也降低了執行的效率:不論什么情況,這個賦值函數都要創建一個新的Bitmap對象。
第一種方法的額外支出:判斷語句必然地引入了程序的分支(branch),于是指令的預取(prefetch)、緩沖(caching)、流水線處理(pipelining)的效率就會被降低。
3、
Copy And Swap。改賦值為交換。
void swap(Widget& rhs); //交換*this 和rhs的數據;
Widget& Widget::operator=(const Widget& ths)
{
Widget temp(ths); //為rhs數據制作一份副本
swap(temp); //將*this數據和上述副本的數據交換。
return *this;
}
Widget& Widget::operator=(Widget rhs) // rhs is a copy of the object
{ // passed in — note pass by val
swap(rhs); // swap *this's data with the copy's
return *this;
}利用參數傳值,隱性的構造了一個Widget對象。然后將新對象和本對象中的數據成員交換,達到為本對象賦值的效果。
新的臨時對象在跳出函數后自動銷毀。剛才說的兩個unsafe,都不會存在。
這樣開銷較大了,無論什么時候都要構造新的對象。用swap來完成賦值的做法有點邏輯混淆。但這樣做很有可能讓編譯器生成更有效率的代碼。
---------------------------
如何保證在賦值/拷貝的時候能夠將所有的成員完整拷貝過去?
對于簡單的數據成員,編譯器自動生成的拷貝函數可以保證一個不漏都幫你拷貝;
如果是比較復雜的成員(比如說指向一片內存空間的指針),編譯器就沒有足夠的智商把這些成員拷貝到另外一個對象中去了。
在增加類成員以后記得更新拷貝函數,以免拷貝不完全。
子類的拷貝函數把自己的成員都拷貝了,但是卻漏了把父類對象的成員拷貝到新的對象中。 在子類的拷貝函數中調用父類的拷貝函數
Widget& Widget:: operator = (Widget src)
{
swap(src); // copy-and-swap
WidgetParent:: operator = (src); // invoking the parent's copy assignment operator
return * this ;
}
最后的最后,通常來說在拷貝函數和拷貝構造函數中的實現大多相同,
不要在拷貝函數中調用拷貝構造函數或者反之。如果真的需要避免代碼的重復,大可定義一個私有的函數來負責前面兩者相同的部分。
posted on 2010-03-15 22:44
Euan 閱讀(574)
評論(0) 編輯 收藏 引用 所屬分類:
C/C++