《Effective C++》筆記:Tips09-Tips12

Tips09:毫不在構造和析構過程當中調用virtual函數

PS:本人以爲,應該改爲不要在基類的構造和析構中調用virtual函數


1、若是在基類的構造函數中調用virtual函數,調用誰的?

class Transaction
{
public:
       Transaction(void){
              logTransaction();
       }
       virtual ~Transaction(void){}
       virtual void logTransaction() const = 0;
};
                                                                           
class BuyTransaction :
       public Transaction{
public:
       BuyTransaction(void){}
       ~BuyTransaction(void){}
       void logTransaction() const{
              std::cout << "BuyTransaction::logTransaction()" << std::endl;
       }
};

客戶端代碼:安全

BuyTransaction b;

程序報錯ide

p_w_picpath013

從錯誤中就能夠看出,Transaction的構造函數並無調用子類的virtual方法logTransaction(),而是調用自己的,可是自身的logTransaction()初純虛函數,並無實現,因此程序保存。固然,咱們能夠實現Transaction的純虛函數logTransaction(),來進一步驗證,雖然這麼作沒意義函數

p_w_picpath014

能夠看出,確實是調用基類Transaction自己的構造函數。this

2、爲何構造函數是調用自己的virtual方法。

根據多態的定義,當子類重載基類的virtual成員方法,應該調用子類的重載方法。可是這裏有個前提,子類對象必須存在!若是連對象都不存在,那對象的成員變量也就不存在,若是成員方法要訪問成員變量,訪問什麼?spa


本例中,是在基類的構造函數中調用virtual方法。構造子類對象的順序是,從基類到子類,依次調用構造函數。實際上,在調用基類的構造函數時,壓根就不知道子類對象的存在。這時的對象實際上就是基類對象,而virtual函數天然被編譯器解析至基類的virtual函數。3d


Tips10:令operator返回一個reference to *this      


1、operator=返回引用和返回值的區別

先來看返回引用指針

class Widget{
public:
       Widget(void);
       Widget(int value);
       ~Widget(void);
       Widget& operator=(const Widget& rhs);
       void log();
private:
       int mVaule;
};
                                                                
Widget::Widget(void) : mVaule(0){}
                                                                
Widget::Widget( int value ) : mVaule(value){}
                                                                
Widget::~Widget(void){}
                                                                
void Widget::log(){
       cout << "Widget = " << mVaule << endl;
}
                                                                
Widget& Widget::operator=( const Widget& rhs ){
       mVaule = rhs.mVaule;
       return *this;
}


客戶端代碼:orm

Widget w1 = 1;
Widget w2 = 2;
Widget w3 = 3;
w1 = w2 = w3 = 123;
w1.log();
w2.log();
w3.log();

p_w_picpath015

和預期的同樣,賦值操做正常執行。xml

那咱們換成返回值,再來看看有何反應對象

Widget Widget::operator=( const Widget& rhs ){
       mVaule = rhs.mVaule;
       return *this;
}

p_w_picpath016

同樣能夠執行,那爲何要讓operator=返回引用呢?

接下來,咱們分別在拷貝構造函數拷貝賦值運算符以及析構函數加上打印語句

operator=返回引用

Widget::~Widget(void){
       cout << "deconstructor" <<endl;
}
                                                       
Widget::Widget( const Widget &rhs ) : mVaule(rhs.mVaule){
       cout << "copy constructor" << endl;
}
                                                       
Widget& Widget::operator=( const Widget& rhs ){
       cout << "Widget copy assignment " << endl;
       mVaule = rhs.mVaule;
       return *this;
}

p_w_picpath017


operator=返回值

Widget Widget::operator=( const Widget& rhs ){
       cout << "Widget copy assignment " << endl;
       mVaule = rhs.mVaule;
       return *this;
}

p_w_picpath018

對比運行結果,能夠清楚的發現,operator=返回值的話,在連續賦值時,會多調用三次拷貝構造函數和三次析構函數

咱們來分析在operator=返回值的狀況下

w1 = w2 = w3 = 123;

發生了什麼?

首先w3 = 123,這部分調用operator=,注意因爲返回的值,在return *this,會調用拷貝構造函數,返回的是w3的拷貝.

而後w2 = w3,首先也是調用operator=,在return *this,返回的是w2的拷貝,再一次調用拷貝構造函數

最後w1 = w2,和上述的狀況同樣,返回的是w1的拷貝。

賦值操做結束後,w3,w2,w1的拷貝是臨時對象,被銷燬。因此,調用了三次析構函數。

因此,operator=返回引用,不只是協議,還能夠又可避免拷貝構造函數和析構函數的調用。


Tips11:在operator=中處理「自我賦值」

1、不安全的「自我賦值」

class SelfAssignment{
public:
       ...
       SelfAssignment& operator=(const SelfAssignment& rhs);
private:
       int *mValuePtr;
};
                                                
SelfAssignment& SelfAssignment::operator=( const SelfAssignment& rhs ){
       delete mValuePtr;
       mValuePtr = new int(*rhs.mValuePtr);
       return *this;
}

一旦客戶端這樣寫

SelfAssignment s;
s = s;

s的成員指針已經成爲空懸指針。


2、自我賦值安全和異常性安全

一個簡單的方法,就能夠避免1、中出現的問題,複製以前先檢查是不是同一個對象

SelfAssignment& SelfAssignment::operator=( const SelfAssignment& rhs ){
       if(this == &rhs)
              return *this;
       delete mValuePtr;
       mValuePtr = new int(*rhs.mValuePtr);
       return *this;
}

可是,若是在建立對象時,即new的時候,拋出異常的話,s的成員指針依然是空懸指針。這裏就須要異常性檢查

SelfAssignment& SelfAssignment::operator=( const SelfAssignment& rhs ){
       int *pOrig = mValuePtr;
       mValuePtr = new int(*rhs.mValuePtr);
       delete pOrig;
       return *this;
}

實際上,就是在確保成員指針正常複製以前,先不要刪除器指向的內存。一旦過程當中出現異常,成員指針還能夠指向原來的內存。此外,就算複製的對象是自己,也能夠正常運行。

這是《C++ Primer》上的方法,我的以爲《Primer》上的方法更容易理解


SelfAssignment& SelfAssignment::operator=( const SelfAssignment& rhs ){
       int *newPtr = new int(*rhs.mValuePtr);
       delete mValuePtr;
       mValuePtr = newPtr;
       return *this;
}

Primer》提供的建議是,現將右側的運算對象拷貝紙一個局部臨時對象。當拷貝完成後(異常沒有發生),銷燬左側運算對象的現有成員就安全了。一旦左側運算對象被銷燬,剩下的就是數據從臨時對象拷貝到左側對象的成員中。


3、拷貝交換技術(copy and swap)實現自賦值

這項技術用到了標準庫的swap()函數

SelfAssignment& SelfAssignment::operator=( const SelfAssignment rhs ){
       std::swap(*this, rhs);
       return *this;
}

要注意的就是參數constSelfAssignment rhs是非引用參數,使用的是值傳遞。swap交換對象自己和參數的成員變量,操做完成以後,rhs的成員變量實際上就是原來對象的成員變量。離開拷貝賦值運算符後,rhs銷燬,也就是對象原先的成員變量銷燬。

這項技術解決了異常安全和自賦值安全。在傳遞參數的時候,就已經獲得右值的副本。至關於

int*newPtr = new int(*rhs.mValuePtr);

只要參數傳遞的過程當中沒有異常,就解決了異常安全的問題。

同時swap使得對象和副本交換,就算rhs是對象自己,也是和本身的副本交換,銷燬的也是副本,這也解決了自我賦值安全的問題。


Tips12:賦值對象時勿忘每個成分

1、編譯器默認的拷貝構造函數(copy)和拷貝賦值運算符(copy assignment)

默認生成的兩個負責拷貝的函數,會賦值全部的non-static成員變量。

l內置類型和對象類型:值拷貝

若是是指針類型,只會拷貝指針,不會拷貝指針所指的內存。

若是是引用類型,sorry,必須自定義拷貝函數。

也就是說,編譯器生成的拷貝函數是淺拷貝


2、自定義的拷貝構造函數(copy)和拷貝賦值運算符(copy assignment),必定要複製每個non-static成員變量

由於,當自定義的拷貝函數忘記複製某一個成員變量,編譯器不會發出任何警告。


3、自定義子類的拷貝構造函數(copy)和拷貝賦值運算符(copy assignment),必定要複製基類(base class)的成員變量

這一點,很容易遺漏。可能在自定義拷貝函數的過程當中,只複製了子類自己的成員變量,而遺漏了父類的成員變量。

然而,父類的成員變量每每是private,子類不能直接訪問,複製父類的成員變量的方法就是調用對應的父類方法。拷貝構造函數就調用父類的拷貝構造函數,拷貝賦值運算符就調用父類的拷貝賦值運算符。

Copy

DerivedClass::DerivedClass(const DerivedClass& rhs) : BasedClass(rhs),初始化成員變量{}

Copy assignment

DerivedClass& DerivedClass::operator=(const DerivedClass& rhs){
       BasedClass::operator=(rhs);
       複製成員變量
       return *this;
}


4、特殊的函數,不要相互調用

這裏指的特殊函數,就是構造函數,析構函數,拷貝構造函數,拷貝賦值運算符

尤爲是拷貝構造函數和拷貝賦值運算符,絕大多數時候,代碼是同樣的。正確的作法是將相同的代碼提取出來,封裝成普通的方法,而後讓拷貝函數區調用。

相關文章
相關標籤/搜索