《Effective C++》第2章 構造/析構/賦值運算(2)-讀書筆記

QingLiXueShi發表於2015-04-21

章節回顧:

《Effective C++》第1章 讓自己習慣C++-讀書筆記

《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記

《Effective C++》第2章 構造/析構/賦值運算(2)-讀書筆記

《Effective C++》第3章 資源管理(1)-讀書筆記

《Effective C++》第3章 資源管理(2)-讀書筆記

《Effective C++》第4章 設計與宣告(1)-讀書筆記

《Effective C++》第4章 設計與宣告(2)-讀書筆記

《Effective C++》第5章 實現-讀書筆記

《Effective C++》第8章 定製new和delete-讀書筆記


 

條款09:絕不在構造和析構過程中呼叫virtual函式

你不該在構造和解構函式期間呼叫virtual函式,因為這樣的呼叫不會帶來你預期的結果。

(1)在derived class物件的base class構造期間,物件的型別是base class而不是derived class。不只virtual 函式會被編譯器解析至base class,若使用執行期型別資訊(runtime type information,如dynamic_cast和typeid),也會把物件視為base class型別。

(2)一旦derived class解構函式開始執行,物件內的derived class成員變數便呈現未定義值,所以C++視它們不存在。進入base class解構函式後物件就成為一個base class物件。

一個解決方法是:令derived classes將必要的構造資訊向上傳遞至base classes建構函式。


 

條款10:令operator=返回一個reference to *this

為了實現“連鎖賦值”,賦值操作符必須返回一個reference指向操作符的左側實參。這是你為class實現賦值操作符時應該遵守的協議。

class Widget
{
public:
    Widget& operator=(const Widget& ths)
    {
        ...
        return *this;
    }
};

這個協議不僅適用於以上標準賦值形式,也適用於所有賦值相關運算。

Widget& operator+=(const Widget& rhs)                 // the convention applies to +=, -=, *=, etc. ...
{  
    ...
    return *this; 
}

Widget& operator=(int rhs)                    //此函式也適用,即使此操作符的引數型別不符協定
{
    ...
    return *this;
}

注意:這只是個協議,並無強制性。如果不遵守它,程式碼一樣可通過編譯,這份協議被所有內建型別和標準程式庫提供的型別遵守。


 

條款11:在operator=中處理“自我賦值”

假設你建立一個class用來儲存一個指標指向一塊動態分配的點陣圖(bitmap)。

class Bitmap{...};
class Widget
{
    ...
private:
    Bitmap *pb;
};

Widget& Widget::operator=(const Widget &rhs)
{
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

這是一份不安全的operator=實現,operator=函式內的*this和rhs可能是同一物件,這樣delete就不只銷燬當前的bitmap,也銷燬了rhs的bitmap,導致指標指向一個已刪除的物件。下面這種修改方法是行的通的:

Widget& Widget::operator=(const Widget &rhs)
{
    if (rhs == *this) return *this;            //如果是自我賦值,不做任何事

    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

這個修改版本雖然具有自我賦值安全性,但不具備異常安全性。如果new Bitmap導致異常,無論是因為分配時記憶體不足或因為Bitmap的copy建構函式丟擲異常,Widget都會持有一個指標指向一塊被刪除的Bitmap。

實際上,讓operator=具備異常安全性往往自動獲得賦值安全性。許多時候一群精心安排的語句就可以導致異常安全性。例如以下修改:只需注意在複製pb所指東西之前別刪除pb。

Widget& Widget::operator=(const Widget &rhs)
{
    Bitmap *pOrig = pb;
    pb = new Bitmap(*rhs.pb);
    delete pOrig;
    return *this;
}

如果new Bitmap丟擲異常,pb保持原狀,這段程式碼還可以處理自我賦值或許它的效率不高,但行的通。

如果你關心效率,當然可以把if (rhs == *this) return *this;放在函式起始處。但這時你要考慮一下:自我賦值的頻率高嗎?因為這項測試需要成本:程式碼變大了(包括原始碼和目標碼),並匯入了一個新的控制流分支,兩者都會降低執行速度。

 

以上最佳版本還有一個替換版本,就是所謂的copy and swap技術,這個技術和異常安全性有密切關係。

class Widget
{
    ...
    void swap(Widget &rhs);
    ...
};

Widget& Widget::operator=(const Widget& rhs)
{
    Widget temp(ths);            //為rhs資料製作一份副本
    swap(temp);                    //交換*this與這份副本
    return *this;
}

請記住:

(1)確保當物件自我賦值時operator=有良好行為。技術包括比較“來源物件”和“目標物件”的地址、精心周到的語句順序、以及copy and swap。

(2)確定任何函式如果操作一個以上的物件,而其中多個物件是同一物件時,其行為仍然正確。


 

條款12:複製物件時勿忘每一個成分

如果你為class新新增一個成員變數,你必須同時修改copying函式,同時也需要修改所有建構函式以及任何非標準形式的operator=。

任何時候只要你承擔起“為derived class撰寫copying函式”的重大責任,必須很小心地也複製其base class成分。那些成分往往是private,你無法直接訪問它們,而應該讓derived class的copying函式呼叫相應的base class函式。

class Customer {};

class PriorityCustomer : public Customer
{
    PriorityCustomer(const PriorityCustomer & rhs);
    PriorityCustomer& operator=(const PriorityCustomer &rhs);
private:
    int priority;
};

PriorityCustomer::PriorityCustomer(const PriorityCustomer & rhs) 
    : Customer(rhs), priority(rhs.priority)                //呼叫base class的copy建構函式
{

}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer &rhs)
{
    Customer::operator=(rhs);            //呼叫base class的operator=
    priority = rhs.priority;
    return *this;
}

儘管copying函式有相似地方,但你也不該令copy assignment操作符呼叫copy建構函式,copy建構函式呼叫copy assignment操作符同樣無意義。

如果你發現你的copy建構函式和copy assignment操作符有相似的程式碼,消除重複的最好做法是:建立一個新的private成員函式,供二者呼叫。

請記住:

(1)copying函式應該確保複製“物件內的所有成員變數”及“所有base class成分”。

(2)不要嘗試以某個copying函式實現另一個copying函式。應該將共同機能放進第三個函式中,並由兩個copying函式共同呼叫。

相關文章