Effective C++第二章總結
2.構造/析構/賦值運算
幾乎你寫的每個類都會有一或多個建構函式、一個解構函式、一個拷貝賦值操作符。如果這些函式犯錯,會導致深遠且令人不愉快的後果,遍及整個類。所以確保它們行為正確時生死攸關的大事。
條款05:瞭解C++默默編寫並呼叫哪些函式
如果你自己沒宣告,編譯器就會為類宣告(編譯器版本的)一個拷貝建構函式,一個拷貝賦值操作符和一個解構函式。此外如果你沒有宣告任何建構函式,編譯器也會成為你宣告一個預設建構函式。所有這些函式都是public且inline。
惟有當這些函式被需要(被呼叫),它們才會被編譯器創建出來。即有需求,編譯器才會建立它們。
預設建構函式和解構函式主要是給編譯器一個地方用來放置“藏身幕後”的程式碼,像是呼叫基類和非靜態成員變數的建構函式和解構函式(要不然它們該在哪裡被呼叫呢??)。
注意:編譯器產生的解構函式是個non-virtual,除非這個類的基類自身宣告有virtual解構函式。
至於拷貝建構函式和拷貝賦值操作符,編譯器建立的版本只是單純地將來源物件的每一個非靜態成員變數拷貝到目標物件。
如一個類聲明瞭一個建構函式(無論有沒引數),編譯器就不再為它建立預設建構函式。
編譯器生成的拷貝賦值操作符:對於成員變數中有指標,引用,常量型別,我們都應考慮建立自己“合適”的拷貝賦值操作符。因為指向同塊記憶體的指標是個潛在危險,引用不可改變,常量不可改變。(即深拷貝和淺拷貝問題)
請記住:
- 編譯器可以暗自為類建立預設建構函式、拷貝建構函式、拷貝賦值操作符,以及解構函式。
條款06:若不想使用編譯器自動生成的函式,就該明確拒絕
通常如果你不希望類支援某一特定技能,只要不說明對應函式就是了。但這個策略對拷貝建構函式和拷貝賦值操作符卻不起作用。因為編譯器會“自作多情”的宣告它們,並在需要的時候呼叫它們。
由於編譯器產生的函式都是public型別,因此可以將拷貝建構函式或拷貝賦值操作符宣告為private。通過這個小“伎倆”可以阻止在外部呼叫它。程式碼如下:
class HomeForsale{ public: ........ ........ private: HomeForsale(const HomeForsale&); HomeForsale& operator=(const HomeForsale&); }
當通過外部拷貝HomeForsale物件時,編輯器會出錯,因為宣告為了私有。但是類中的成員函式和友元函式還是可以呼叫private函式。解決方法可能是在一個專門為了阻止拷貝動作而設計的基類。(Boost提供的那個類名為noncopyable)。程式碼如下:
class Uncopyable{ //允許繼承的物件構造和析構
protected:
Uncopyable() {};
~Uncopyable() {};
private:
Uncopyable(const Uncopyable&); //但是阻止copying
Uncopyable& operator=(const Uncopyable&);
}
//只要繼承該類
class HomeForSale:private Uncopytable //不再聲名拷貝建構函式和拷貝運算子
{
........
}
任何嘗試拷貝HomeForSale物件,編輯器都會報錯,因為會嘗試呼叫基類的拷貝建構函式或賦值運算子,而這些被宣告為私有。
請記住:
- 為駁回編譯器自動(暗自)提供的機能,可將相應的成員函式宣告為private並且不予實現。使用像noncopyable這樣的基類也是一種做法。
條款07:為多型基類宣告virtual解構函式
當基類的指標指向派生類的物件的時候,當我們使用完,對其呼叫delete的時候,其結果將是未有定義——基類成分通常會被銷燬,而派生類的充分可能還留在堆裡。這可是形成資源洩漏、敗壞之資料結構、在偵錯程式上消費許多時間。
消除以上問題的做法很簡單:給基類一個virtual解構函式。此後刪除派生類物件就會如你想要的那般。
如果一個類不含virtual函式,通常表示它並不意圖被用做一個基類,當類不企圖被當做基類的時候,令其解構函式為virtual往往是個餿主意。因為實現virtual函式,需要額外的開銷(指向虛擬函式表的指標vptr)。。
許多人的心得是:只有當class內含有至少一個virtual函式才為它宣告virtual解構函式。
STL容器都不帶virtual解構函式,所以最好別派生它們。
請記住:
- 帶有多型性質的基類應該宣告一個virtual解構函式。如果一個類帶有任何virtual函式,它就應該擁有一個virtual解構函式。
- 一個類的設計目的不是作為基類使用,或不是為了具備多型性,就不該宣告virtual解構函式。
條款08:別讓異常逃離解構函式
C++並不禁止解構函式吐出異常,但它不鼓勵你這樣做。C++不喜歡解構函式吐出異常。
如果可能導致異常:
- 如果丟擲異常,就結束程式。(強迫結束程式是個合理選項,畢竟它可以阻止異常從解構函式傳播出去。)
- 捕獲異常,但什麼也不做。
如果某個操作可能在失敗時丟擲異常,而又存在某種需要必須處理該異常,那麼這個異常必須來自解構函式以外的某個函式。
請記住:
- 解構函式絕對不要吐出異常。如果一個被解構函式呼叫的函式可能丟擲異常,解構函式應該捕捉任何異常,然後吞下它們(不傳播)或結束程式。
- 如果客戶需要對某個操作函式執行期間丟擲的異常做出反應,那麼類應該提供一個普通函式(而非在解構函式中)執行該操作。
條款09:決不讓構造和析構過程中呼叫virtual函式
你不該在建構函式和解構函式中呼叫virtual函式,因為這樣的呼叫不會帶來你預想的結果。
如下程式碼:
class Transaction{
public:
Transaction();
virtual void logTransaction() const = 0;
........
};
Transaction::Transaction()
{
......
logTransaction();
}
class BuyTransaction:public Transaction
{
public:
virtual void logTransaction() const;
.....
};
class SellTransaction:public Transaction
{
public:
virtual void logTransaction() const;
......
};
當以下這行現在執行,會發生什麼:
BuyTransaction b;
Transaction建構函式的最後一行呼叫virtual函式logTransaction,這時被呼叫的logTransaction是Transaction內的版本,不是BuyTransaction內的版本!!!
在derived class物件的base class構造期間,物件的型別是base class 而不是derived class。不止virtual函式會被編輯器解析成base class,若使用執行期型別資訊(RIIT,例如:dynamic_cast和typeid),也會把物件視為base class型別。上面的例子:這個物件內的“BuyTransaction 專屬成分”尚未被初始化,視為不存在。物件在derived class建構函式開始執行前不會成為一個derived class物件。
派生類的成員變數沒初始化,即為指向虛擬函式表的指標vptr沒被初始化又怎麼去呼叫派生類的virtual函式呢?
解構函式也相同,派生類先於基類被析構,又如何去找派生類相應的虛擬函式?
請記住:
- 在構造和解構函式期間不要呼叫虛擬函式,因為這類呼叫從不下降至派生類。
條款10:令operator= 返回一個reference to *this
對於賦值操作符,我們常常要達到這種類似效果,即連續賦值:
int x, y, z;
x = y = z = 15;
為了實現“連鎖賦值”,賦值操作符必須返回一個“引用”指向操作符的左側實參。
即:
Widget & operator = (const Widget &rhs)
{
...
return *this;
}
所有內建型別和標準程式庫提供的型別如string,vector,complex或即將提供的型別共同遵守。
請記住:
- 令賦值操作符返回一個reference to *this。
條款11:在operator =中處理“自我賦值”
條款11在我看c++primer plus中就已經重點提及到,裡面的operator =都處理了"自我賦值"
先舉幾個自我賦值的例子:
例:Widget w;
w = w;
a[i] = a[j]; //i == j or i != j
*px = *py;// px,py指向同個地址;
以上情況都是對“值”的賦值,但我們涉及對“指標”和“引用”進行賦值操作的時候,才是我們真正要考慮的問題了。
看下面的例子:
Widget& Widget::operator=(const Widget& rhs)
{
delete pb; //這裡對pb指向記憶體物件進行delete,試想 *this == rhs?情況會如何
pb = new Bitmap(*rhs.pb); //如果*this == rhs,那麼這裡還能new嗎?“大事不妙”。
return *this;
}
也許以下程式碼能解決以上問題:(C++primer plus中就已經重點提及到的版本)
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs)
return *this; //解決了自我賦值的問題。
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
“許多時候一群精心安排的語句就可以匯出異常安全(以及自我賦值安全)的程式碼。”,以上程式碼同樣存在異常安全問題。(即如果new Bitmap產生異常)
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; //記住原先的pb
pb = new Bitmap(*rhs.pb); //令pb指向*pb的一個複本
delete pOrig; //刪除原先的pb
return *this; //這樣既解決了自我賦值,又解決了異常安全問題。自我賦值,將pb所指物件換了個儲存地址。
}
請記住:
- 確保當物件自我賦值時operator =有良好行為。其中技術包括比較“來源物件”和“目標物件”的地址、精心周到的語句順序、以及copy-and-swap。
- 確定任何函式如果操作一個以上的物件,而其中多個物件是同一個物件時,其行為仍然正確。
條款12:複製物件時勿忘其每一個成員
還記得條款5中提到編譯器在必要時會為我們提供拷貝建構函式和拷貝賦值函式,它們也許工作的不錯,但有時候我們需要自己編寫自己的拷貝建構函式和拷貝賦值函式。如果這樣,我們應確保對“每一個”成員進行拷貝(複製)。
如果你在類中新增一個成員變數,你必須同時修改相應的copying函式(所有的建構函式,拷貝建構函式以及拷貝賦值操作符)。
在派生類的建構函式,拷貝建構函式和拷貝賦值操作符中應當顯示呼叫基類相對應的函式,否則編譯器可能又“自作聰明瞭”。
當你編寫一個copying函式,請確保:
(1)複製所有local成員變數;
(2)呼叫所有基類內的適當copying函式。
程式碼如下:
class Customer
{
public:
...
Customer(const Customer& rhs):m_name(rhs.m_name){}
Customer& operator=(const Customer& rhs)
{
name=rhs.name;
return *this;
}
...
private:
std::string m_name;
}
錯誤的程式碼:
class PriorityCustomer:pulic Customer
{
public:
...
PriorityCustomer(const PriorityCustomer& rhs):m_priority(rhs.m_priority)
{
}
PriorityCustomer& operator=(const PriorityCustomer& rhs)
{
m_priority=rhs.m_priority;
return *this;
}
...
private:
int m_priority;
}
程式碼看起來沒有問題,但是,卻忽略了baseclass的成分 !
正確如下:
class PriorityCustomer:pulic Customer
{
public:
...
PriorityCustomer(const PriorityCustomer& rhs):Customer(rhs),m_priority(rhs.m_priority)
{
}
PriorityCustomer& operator=(const PriorityCustomer& rhs)
{
Customer::operator=(rhs); //呼叫父類的
m_priority=rhs.m_priority;
return *this;
}
...
private:
int m_priority;
}
呼叫所有基類內的適當copying函式,Customer::operator=(rhs);
請記住:
- Copying函式應該確保複製“物件內的所有成員變數”及“所有基類成員”;
- 不要嘗試以某個copying函式實現另一個copying函式。應該將共同機能放進第三個函式中,並由兩個copying函式共同呼叫。
參照部落格:https://blog.csdn.net/zyq522376829/article/details/48179163