構造析構與拷貝賦值那些事
建構函式
關於建構函式,我們耳熟能詳,似乎都沒有必要成為一個知識點,或者說是重要的知識點拿出來特殊說明,畢竟C++的編譯器都能幫我們完成這個工作,只是,事情真的如想象的那麼簡單麼;
可能不是。
本文試圖挖掘關於建構函式,可能不是那麼簡單的一面,當然也不會很全面,權當一起學習了。
建構函式的概念:提供類的物件的初始化的方式,類通過一個或幾個特殊的成員函式來控制物件的初始化過程。
有這個概念出發,我們可以知道,所有的建構函式都是在類的物件初始化時由系統呼叫的,具體呼叫哪個是按過載函式的呼叫規則來的。
備註:建構函式不能被宣告為const。可以想想為何?
建構函式也不能是虛擬函式,這個應該好解釋。
預設建構函式
這個最簡單,在面向物件的世界裡,萬物皆是物件,因為萬物皆需要建構函式,如果我們沒有定義一個建構函式,那麼就由C++的編譯器幫我們完成,在《c++ primer》裡叫做合成的預設建構函式。
下面開始我們的編碼求學之旅:
首先,定義一個類設計者工具類:
#include <iostream> using namespace std; class ClassDesignTool { public: void printSp(){ cout << sp_ << "\n"; } private: string *sp_; };
在這樣一個什麼沒有寫建構函式的類裡,預設建構函式依然會在編譯階段生成,測試程式碼如下:
ClassDesignTool tool;
tool.printSp();
在VS2010的編譯環境下的結果是CCCCCCCC
,看到這個你應該很熟悉,這是Windows環境下對所有未顯式賦值變數的預設賦值,這也就能證明,Windows系統在編譯後使用預設合成建構函式,將成員變數sp_賦值為CCCCCCCC
了。
如果你不放心,可以把預設建構函式加上去,
ClassDesignTool(){};
測試的結果是一樣的。
這說明,如果你不準備在類的物件初始化時做點什麼,完全可以把這件事交給編譯器。反之,我們需要做點別的工作了。
覆蓋預設建構函式
可能,你認為預設的合成建構函式什麼事也沒做,對它心有怨恨,所以你決定出馬把它改寫(覆蓋之)。
ClassDesignTool():sp_(new string("lcksfa")){
cout << "use override default constructor " << "\n";
}
//列印函式同時修改
void printSp(){
cout << "sp_ is " << sp_->c_str() << "\n";
}
測試結果:
use override default constructor
sp_ is lcksfa
現在,我們覆蓋(override)了預設建構函式,合成的預設建構函式不會被呼叫,而呼叫我們自己的建構函式。
建構函式過載
函式過載(overload)的概念,我相信大家都不會陌生,對於建構函式,同樣的也能將其過載。和呼叫普通的過載函式一樣,系統會在初始化物件時,根據不同的引數型別去呼叫不同的過載建構函式:
在上面的程式碼裡新增如下程式碼:
//overload constructor
ClassDesignTool(const string& str)
:sp_(new string(str)){
std::cout << "use overload constructor " << "\n";
}
以上,我們過載了一個建構函式,其引數為一個const string&型別。
ClassDesignTool tool4(string("4"));
tool4.printSp();
測試結果如下:
use overload constructor
sp_ is 4
這說明,當我們添加了建構函式的過載函式後,使用string("4")引數構造物件時,呼叫了我們的string引數的建構函式。
拷貝建構函式
上面的東西都很簡單,下面,我們說下稍微複雜的。
從函式過載層面,拷貝建構函式也是建構函式的過載,只是其引數為本類的const引用,如下:
//copy constructor
ClassDesignTool(const ClassDesignTool&);
ClassDesignTool::ClassDesignTool(const ClassDesignTool& rhs)
{
std::cout << "use copy constructor from " << rhs.sp_->c_str() << "\n";
sp_ = new string(*(rhs.sp_));
}
什麼時候呼叫?
ClassDesignTool tool("lcksfa");
ClassDesignTool tool2(tool);
tool2.printSp();
測試輸出:
use overload constructor
use copy constructor from lcksfa
sp_ is lcksfa
以上程式碼說明,tool是使用的建構函式初始化,其引數為"lcksfa",而tool2是使用拷貝建構函式初始化,其引數為tool。
解構函式
說完建構函式,說下解構函式。我們知道物件在建立時呼叫了建構函式,而在銷燬時則會呼叫解構函式。
//destructor
~ClassDesignTool(){
std::cout <<"use destructor "<<sp_->c_str()<<"\n";
delete sp_;
}
以上是解構函式,事實上,我已經把預設的解構函式給覆蓋了,原因在於sp_的記憶體釋放,如果使用合成的預設解構函式,系統將不會釋放sp__的記憶體,從而導致記憶體洩漏。
和建構函式不同,解構函式沒有過載函式。這一點和人生很像啊。
執行方式
每一個建構函式都是 由兩部分組成的,一個是初始化部分,另一個才是函式體,成員的初始化是在函式體執行之前完成的,所以你的程式碼裡也需要做這兩個部分的區分,不要把成員的初始化和函式體混為一體,因為,可能會影響解構函式的執行(只是,沒有你想的那麼嚴重)。因為一個解構函式,其也是由函式體和其析構部分組成的,析構時,先執行函式體,再執行銷燬操作,成員按構造的初始化列表的逆序銷燬。
由解構函式體引起的
如果你需要覆蓋重寫解構函式體,那麼幾乎可以肯定你還需要拷貝建構函式和拷貝賦值運算子。
舉例子,我在上面的程式中重寫了解構函式,因為我需要顯示釋放sp_的記憶體,按上面的程式看,還可能出現什麼問題呢?畢竟我沒有拷貝賦值運算子函式。在測試函式中新增以下程式碼:
ClassDesignTool tool ;
{
ClassDesignTool tool2("not me");
tool2 = tool;
// tool.printSp();
tool2.printSp();
}
測試輸出:
use override default constructor
use overload constructor
sp_ is lcksfa
use destructor lcksfa
use destructor
///奔潰了!!!
使用大括號{}將tool2的賦值部分封起來,確保tool2先析構。
程式輸出後,到tool析構處就奔潰了!
原因何在?
因為這裡的系統預設的賦值運算是直接將sp_ 的值進行賦值,而沒有去拷貝sp_ 指向的記憶體,tool2離開作用域時呼叫析構將sp_ delete掉了,等到tool離開作用域時,嘗試delete的還是同一塊記憶體,於是就出現了double delete的問題!
賦值操作運算子
這種情況的解決方案之一就是我們自己定義一個賦值操作運算子:
ClassDesignTool&
ClassDesignTool::operator=(const ClassDesignTool& rhs)
{
std::cout << "use copy-assignment operaotr"<<"\n";
auto spNew = new string(*(rhs.sp_));
delete sp_;
sp_ = spNew;
return *this;
}
本函式的寫法頗為模式化:
- 將待拷貝的物件拷貝到新記憶體
- 釋放sp_原來指向的記憶體
- 使用新拷貝的指標值給sp_賦值。
- 最後將 * this的引用返回(可以說凡是期望返回ClassDesignTool& ,最後都是返回 * this)
總結起來就是 綜合了析構和建構函式的操作。銷燬了左值運算物件的資源,而從右值運算物件中拷貝資源。
小結:本文初略的說明了建構函式、解構函式和拷貝賦值運算子的過載,可以作為入門者的參考。