【Effective C++】讀書筆記(二)---構造/析構/賦值運算

  • 條款6:若不想使用編輯器自動生成的函數,那就應該明確拒絕

我們也知道類這個東西就是爲了解決實際生活當中的實際問題的,當然我們有時候在生活中不希望有哪些事情發生,在類中就會有不希望那些函數被使用,對吧. 舉個例子,給論文加一個類,或者事情營銷方案,各國機密的類你覺得應不應該提供拷貝構造這種類呢?

有些東西世界上只能存在一份. 還有我們以前學習的智能指針當中的一個Scoped_ptr,爲了防止管理權轉移的問題,他直接就禁用了拷貝構造和賦值運算符重載. 接下來的就是教大家如何讓編譯器不要生產你不想使用的函數.首先有的人就想那我就不要聲明就行了唄,但是如果是默認的成員函數這個時候就算你不聲明,結果編輯器還是會爲你生成一個.這樣行不通. 接下來又有人想到了可以將他們設置爲private類型的,這樣也可以. 但是你又忽略了member函數和friend函數的存在,其實正確的做法就是聲明爲private但是你卻不去定義他們.比如這樣:

class HomeForsale
{
public:
	...
private:
	...
	HomeForsale(const HomeForsale& a);
	HomeForsale& operator=(const HomeForsale& b);
};

如果有人通過friend和member函數調用到你的隱藏函數,那麼它還是會獲得一個連接錯誤. 這種方法也是一個極騷的方法.

所以有人想拷貝你的對象編譯器阻攔他,它要是還想用friend和member函數調用,連接器又去阻撓它.

第二種方法:

class Uncopyable{
protected:
	Uncopyable()
	{}
	~Uncopyable()
	{}
private:
	Uncopyable(const Uncopyable& a);
	Uncopyable& operator=(const Uncopyable& b);
};
 
class HomeForSale :private Uncopyable
{
	.......
};

有人說這是在幹啥? 那麼請你再認真仔細的看. 任何人哪怕他是調用了member和friend函數嘗試拷貝HomeForsale對象,編譯器便嘗試生成一個copy構造函數和copy assignment操作符,這些函數的"編譯器生成版會嘗試去調用其base class 對於的兄弟",但那些調用被拒絕了. 因爲其base class拷貝函數爲private.

這兩種方法對於你來說都是可行的,合理運用即可.

  • 條款7:爲多態基類聲明爲virtual析構函數

這裏的問題其實不是很難思考的,舉個例子大家就明白了,看如下代碼:

class Base  
{  
public:  
    virtual void func1()  
    {  
        cout << "Base::func1" << endl;  
    }  
  
    virtual void func2()  
    {  
        cout << "Base::func2" << endl;  
    }  
  
    virtual ~Base()  
    {  
        cout << "~Base" << endl;  
    }  
  
private:  
    int a;  
};  
  
class Derive :public Base  
{  
public:  
    virtual void func1()  
    {  
        cout << "Derive::func1" << endl;  
    }  
    virtual ~Derive()  
    {  
        cout << "~Derive"<< endl;  
    }  
private:  
    int b;  
};  
  
void Test1()  
{  
    Base* q = new Derive;  
    delete q;  
}  
int main()  
{  
    Test1();  
    system("pause");  
    return 0;  
}  

注意這裏我先讓父類的析構函數不爲虛函數(去掉virtual),我們看看輸出結果:

在這裏插入圖片描述

這裏它沒有調用子類的析構函數,因爲他是一個父類類型指針,所以它只能調用父類的析構函數,無權訪問子類的析構函數,這種調用方法會導致內存泄漏,所以這裏就是有缺陷的,但是C++是不會允許自己有缺陷,他就會想辦法解決這個問題,現在我們讓加上爲父類析構函數加上virtual,讓它變回虛函數,我們再運行一次程序的:
在這裏插入圖片描述

誒! 子類的虛函數又被調用了,這裏發生了什麼呢?? 來我們老方法打開監視窗口。

在這裏插入圖片描述

這種情況對於我們來說是一個引起災難的祕訣我跟你講~ 就是內存泄露. 所以當有多態情況出現的時候,你就趕緊把基類析構函數定義爲virtual函數. 是這樣的,我身邊有一個朋友也知道這個規則,他無論寫什麼程序都往析構函數前面加virtual,這樣我就不會了,不管程序內部有沒有虛函數的存在,有沒有多態的存在,他都加virtua. 他完全就是在搞事情.

總結:

帶有多態性質的base classer 應該聲明一個virtual析構函數.

如果class帶有任何virtual函數,他就應該擁有一個virtual析構函數.

  • 條款8:別讓異常存在於析構函數

總結

析構函數絕對不要吐出異常.如果一個被析構函數調用的函數可能拋出異常,析構函數應該捕捉任何異常,然後吞下他們或結束程序.

如果客戶需要對某個操作函數運行期間拋出的異常做出反應,那麼class應該提供一個普通函數執行該操作(函數不在析構函數內)

  • 條款09:絕對不在構造和析構過程中調用virtual函數.

你不該在構造函數和析構函數期間調用virtual函數,因爲這樣的調用不會給你帶來預想的結果.舉個例子:

class Transaction{      //base class
public:
	Transaction();
	virtual void logTransaction() const = 0;  
 
	...
};
 
Transaction::Transaction() //base class
{
{
	...
	logTransaction();
}
 
class BuyTransaction :public Transaction //derived class
{
public:
	virtual void logTransaction() const;
};
 
class SellTransaction : public Transaction //derived class
{
public:
	virtual void logTransaction() const;
	...
};

現在,當以下這行被執行,會發生什麼事:

buyTransaction b;

無疑地會有一個BuyTransaction構造函數被調用,但首先Transaction構造函數一定會更早的調用; derived class對象內的base class成分會在derived class自身成分被構造出來之前先構造妥當.Transaction構造函數的最後一行調用logTransaction函數,這裏正是引起驚奇的起點.這時候被調用的logTransaction是Transaction內的版本,不是BuyTransaction內的版本.即使目前即將建立的對象類型是BuyTransaction.是的,base class構造期間 virtual函數絕不會下降到derived classes階層. 取而代之的是,對象的作爲就像隸屬base類型一樣.非正式的說法或許比較傳神:

在base class構造期間,virtual函數不是virtual函數.

由於base class構造函數的執行更早於derived class構造函數,當base class構造函數執行時derived class的成員變量尚未初始化.如果此期間調用的virtual函數下降至derived class階層,要知道derived class的函數幾乎必然取用local成員變量,而那些成員變量尚未初始化,這將是一張通向不明確行爲和徹夜調試大會串的直達車票. “要求使用對象內部尚未初始化部分” 是一個危險代名詞,所以C++不讓你走這條路.

在derived class對象的base class構造期間,對象類型時base class而不是derived class。不只virtual函數會被編輯器解析至base class。 若使用運行期類型信息,也會把對象視爲base class類型. 相同的道理也適合析構函數. 一旦derived class析構函數開始執行,對象內的derived class成員變量便呈現未定義值,所以C++視他們彷彿不再存在.

進入base class析構函數後對象就成了一個base class對象,而C++的任何部分包括virtual函數,dynami_casts等等也這樣看待它.

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

這個條款大家應該很容易就明白的,關於賦值,有趣的是你可以把他們寫成連鎖反應.

int x = 0;

int y = 0;

int z = 0;

x = y = z = 15;

同樣有趣的是,賦值採用右結合律,所以上述連鎖賦值被解析爲:

x = (y = (z = 15));

這裏15先被賦值給z,然而其結果再被賦值給y,然後其結果再被賦值給x.

爲了實現"連鎖賦值",賦值操作符必須返回一個reference指向操作符的左側實參. 對 就這麼簡單就完了…

總結:

令賦值操作符返回一個reference to *this。

  • 條款11:在operator=中處理"自我賦值"

有的人可能想了,有誰會寫出 a = a;這種表達式這個條款是拿來充數的吧? 你還真的別這麼說,這種情況還真的有情況發生.比如有的自我賦值你根本看不出來:
比如:a[i] = a[j]; //潛在的自我賦值,
如果i = j的時候.*px = *py; // 潛在的自我賦值,如果px和py恰巧指向同一個東西.

這些都不是明顯的自我賦值,是"別名"帶來的結果.一般而言當某段代碼操作Points和reference而他們被用來"指向多個相同類型的對象",就需要考慮這些對象是否爲同一個.實際上兩個對象只要來自同一個繼承體系,他們甚至不許聲明爲相同類型就可能造成"別名"因爲一個base class的reference或者points可以指向derived class對象.

我們一般的operator=實現代碼爲:

widget& operator=(const widget& rhs)
{
	delete pb;  //假設pb爲管理資源的指針
	pb = new Bitma(*rhs.pb);
	return *this;
}

但是如果不慎出現別名,那麼你是先刪除掉自己然後再自己對自己賦值? 讓自己的指針指向一個被銷燬的地方? 所以我們需要加上一層判斷.

widget& operator=(const widget& rhs)
{
	if (this == &rhs)
	{
		return *this;
	}
	delete pb;  //假設pb爲管理資源的指針
	pb = new Bitma(*rhs.pb);
	return *this;
}

這樣做是一點問題都沒有的,但是這個新版本會存在異常方面的麻煩.更明確的說,如果"new Bitma"導致異常,widget最終會持有一個指針指向一塊被刪除的bitmap.這樣的指針是有害的,你無法安全的刪除它們,甚至無法安全的讀取他們.唯一能對他們做的安全事件是付出許多調試能力找出錯誤根源.

如果你爲了讓operator= 具有"異常安全性,所以我們只需要注意在複製pb所指東西之前別刪除pb:

widget& operator=(const widget& rhs)
{
	Bitma* porig = pb; //假設pb爲管理資源的指針
	pb = new Bitma(*rhs.pb);
	delete porig;  
	return *this;
}

現在,如果"new Bitma"拋出異常,pb保持原狀.即使沒有證同測試,這段代碼還是能夠處理自我賦值,因爲我們對原pb做了一件復件,然後重新定義pb,最後pb定義好了之後,刪除掉復件. 它或許不是處理"自我賦值"的最高效方法,但它行得通.

總結:

確保當對象自我賦值時operator有良好的行爲,檢查他是否存在自賦值.

確定任何函數如果操作一個或一個以上的對象,而其中多個對象是同一個對象時,其行爲仍然正確.

  • 條款12:複製對象時勿忘其每一個成分

設計良好之面向對象系統會將對象的內部封裝起來,只留下兩個函數負責對象拷貝,那就是copy構造函數和copy assignment操作符我稱他們爲copying函數.

如果你聲明自己的copying函數,意思就是告訴編輯器你並不喜歡卻省實現中的某些行爲。編譯器就會覺得被冒犯一樣,會用一種奇怪的方式來回敬你,當你的代碼出現一點錯誤的時候卻不告訴你.

我就這樣說吧,當你編寫一個copying函數請確保兩件事情:

1.複製所有的local成員變量.

2.調用所有base classes內的適當的copying函數.

其實兩個copying函數往往擁有相同的實現本體,這可能會引誘你讓某個函數去喬勇另一個函數以避免代碼重複.這樣精益求精的態度值得讚賞,但是令某個copying函數調用一個copying函數卻無法讓你達到你想要的目標.

令copy assignment操作符調用copy構造函數是不合理的,因爲這就像試圖構造一個已經存在的對象.這挺起來就很不通順.單純的接受這個建議:你不該令copy assignment操作符調用copy構造函數.

令copy構造函數調用copy assignment操作符同樣沒有意義.構造函數是用來初始化新對象,而assignent操作符知識性於已初始化對象身上.對應尚未構造好的對象賦值,就像在一個尚未初始化的對象身上做"只對已初始化對象纔有意義"的事情一樣.同樣無聊.如果你發現你的copy構造函數和copy assignment操作符有相近的代碼,消除重複代碼的做法就是建立一個新的成員函數給兩者調用這樣的函數往往是private而且常被命名爲init. 你可以嘗試這樣.

總結:

Copying函數應該確保複製"對象內的所有成員變量"及"所有base class 成分"

不要嘗試以某個copying函數實現另一個copying函數,應該將共同的機能放進第三個函數當中.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章