在看《Effective C++》這本書的過程中,我無數次的發出感歎,這他媽寫得太好了,句句一針見血,直接說到點上。所以決定把這本書的內容加上自己的理解寫成5篇博客,我覺得不管你是否理解這些條款,都值得你先記下來。下面的索引對應的是書中的章節。
11:如果class內動態配置有內存,請為此class聲明一個copy constructor和一個assignment運算符
12:在constructor中盡量initialization動作取代assignment工作
13:initialization list中的members初始化次序應該和其在class內的聲明次序相同
14:總上base class擁有virtual constructor
15:令operator =傳回*this的reference
16:在operator=中為所有data member賦值
17:在operator =中檢查是否自己賦值給自己
11:如果class內動態配置有內存,請為此class聲明一個copy constructor和一個assignment運算符。
默認的copy constructor和operator=不會對類的每個data member一一賦值,而是一個簡單引用,讓左邊的對象指向右邊對象所指的對象。再也沒有起動作,如果這個類動態分配了內存,比如左邊對象本來指向一塊內存A,現在左邊的對象指向右邊對象所指的內存B了,而再也沒有其他對象指向內存A,由於它是動態分配,不會自己回收,所以就出現內存洩露,還有就是兩個對象指向同一塊內存,如果其中一個對象出了其作用域,那麼其析構函數將自動調用,其動態分配的內存將被回收,現在另一個對象卻指向一塊已經被回收的內存,只要一調用這個對象的數據,就會出現不可知的異常,還有一點值得一提,就算一個對象沒有指向任何地址或是它所指的地址已經被回收了,在調用它的方法的時候,只要它的方法沒有使用它的data member(肯定不存在),就不會出現任何問題,因為方法的內存是和對象類型一起分配,實例化一個對象的時候不會為方法分配內存,只會為data member及其他一些指針分配內存,如指向父類的指針,指向虛擬表的指針等。
如11所述,如果不聲明那兩個方法,在方法調用的時候也會出現問題,當這個對象是以傳值的方式被調用時,會產生一個臨時變量,這個臨時變量會引用這個對象,當方法執行完成,這個臨時變量超出它的作用域,析構函數被調用,這個對象就這樣被銷毀了。所以你必須遵守這一條規則。
12:在constructor中盡量initialization動作取代assignment工作
對象的構造分兩個階段:
1:data member被初始化
2:被調用的構造函數執行起來
如果在構造函數中對data member一一賦值,那麼先要調用data member的構造函數,如果你沒有為data member賦初值,那麼調用的是默認的構造函數,如果你賦了初值調用的是copy constructor,但是我的編譯器不允許data member在定義的時候賦初值,那麼就是調用默認的構造函數,當你在構造函數內為data member賦值的時候調用operator =,相當於你調用了一次constructor和一次operator =,而initialization 只調用一次copy constructor,因為在data member初始化的時候已經為data member賦值了,在構造函數裡面就不用為data member賦值了,經常會遇到這樣的面試題:一個data member在定義的時候給他一個初始值,又在構造函數內賦另一個值,請問這個data member現在的值是多少?還有一些就是base class中的一個data member,多處賦值,然後問題最後它的值是多少?只要記住父類的構造函數在子類的構造函數之前執行,初始化參數在構造函數之前執行。
總之一句話initialization效率比在構造函數中賦值的效率高,如果data member很多且需要初始化成同一個值,而且效率不是那麼重要的話,可以在構造函數中用連等式賦值,這樣會清晰明了一點。效率不是永遠都放在第一位的,代碼的可讀性也很重要。82法則還記得嗎,我曾經因為多寫了一個if,在代碼評審中被批評,理由就是100萬訪問的時候會影響效率,在兩家公司遇到過這種情況,什麼都是這個理由,百萬級訪問時會影響效率。
13:initialization list中的members初始化次序應該和其在class內的聲明次序相同
有時候data member的初始化是依賴別的data member的,那麼data member的初始化順序就必須弄清楚。data member的初始化順序與其在在initialization中出現的順序無關,只與它們定義的順序有關,先定義的data member會在initialization中先初始化,在destructor中後析構,先初始化的data member後析構,所以base data member後析構,跟棧中的變量一樣先定義的變量後析構一樣。如果類繼承多個類,那麼base data member的初始化順序由繼承的先後順序決定,先繼承的先初始化。
14:總上base class擁有virtual constructor
在繼承關系中,是調用父類的方法還是調用子類的方法,這個動態的實現是由虛擬函數來決定的,含有虛擬函數的類都有一個虛擬函數表,如果父類中的方法是virtual的,如果沒有子類沒有覆寫這個方法那麼就是直接繼承過來,不管子類還是父類調用這個方法產生的結果是一樣的,如果子類覆寫了這個方法,那麼子類的虛擬表中存的就是子類方法的地址,如果一個父類的指針指向子類,如果方法被子類覆寫了,那麼調用的方法就是子類的方法,如果沒有覆寫那麼就是調用父類的方法。如果父類的方法不是virtual的,而且子類有一個一樣的方法,那麼父類的方法不會被子類覆寫,也就是說:父類指向子類的指針調用的將不會是子類的方法,而是父類的方法。同樣的父類的析構函數不是virtual的,那麼在delete 這個指針的時候就會出現不可知的情況,反正之類的構造函數是不會調用的,所以當你決定讓一個類成為父類,那麼就讓他的destructor為virtual。
但是也不需要讓每一個類的destructor成為virtual的,因為含有virtual方法的類都有一個指向virtual table的指針,會讓對象變大,如果對象本來就不太的話可能會出現成本翻倍的情況。只有當class中含有至少一個虛擬方法時才讓他的析構函數成為虛擬的。
15:令operator =傳回*this的reference
先看一個等式:(A=B)=C,為了實現這種連等式,operator=肯定是不能返回void的,你可以為*void賦值,但是你不能為void賦值,operator=的返回方式不能是by value,如果是這樣的話,(A=B)返回的是A或是B的副本(正確的方式應該是A的reference),讓後將C的值賦給這
個副本,而A、B的值卻沒有發生任何變化,這當然不是我們想要的,為了不讓這個副本的產生,返回值必須是引用的方式,你可以用指針或是reference的方式返回,指針必須加個*麻煩,所以就是以reference的方式返回,那麼是返回A的引用還是B的引用,毫無疑問是A的,如果是B的,那麼執行的順序是這樣的,先將B賦給A,然後將C的值賦給B,賦值其實就是將右邊的值賦給左邊的返回值嗎!這樣然不是我們想要的,我想要的其實是將B賦給A,然後將C在賦給A,當然寫這樣等式的絕對不是一個合格的程序員。所以operator=必須返回左邊對象的reference。我一直在想this為什麼是一個指針類型而不是一個reference呢?因為我們必須在operator=方法的最後一句加上return *this;而不是 return this;
16:在operator=中為所有data member賦值
這是毫無疑問的,如果有部分data member沒有被賦值,那麼被賦值的對象不就是殘廢了的嗎!有時候我們可能會在為類加data member的時候忘了再operator=中為它賦值了,或是在子類的operator=中忘了為父類的data member賦值。當然這些都不是重點,不記得不是問題,出錯了自然就知道了,為父類的data member賦值只需要在子類的operator=中加上Base::operator=(this);如果編譯器不支持調用base的operator=的話,可以做類型轉換啊,static_case<&Base>(*this)=Derived;我發現在類型轉換中,轉換成的類型一般都是指針或是引用,特別是轉換類型在左邊的時候就一定不能是by value的方式,會產生臨時變量,然後給臨時變量賦值,當然不是你想要的。在為指針類型的data member 賦值時要記住一點,是為指針所指的對象賦值,而不是為指針賦值,如果你讓data member一會兒指向這一會指向那的,可能會出現某些地址不可到達而出現內存洩露。
17:在operator =中檢查是否自己賦值給自己
在為一個對象賦值之前,先要確認這個對象是否動態配置內存,如果動態配置內存,先要回收掉這塊內存,不然當這個對象被賦值,指向別的地址後就會出現內存洩露,當然也不能一發現它動態配置內存了就把它先回收掉,因為可能出現把自己賦給自己的情況,你總不能因為把自己賦給自己之後,自己就莫名其妙的被回收了吧,所以在operator=中藥檢查是否自己賦值給自己。