1. 程式人生 > >構造析構與拷貝賦值那些事

構造析構與拷貝賦值那些事

建構函式

關於建構函式,我們耳熟能詳,似乎都沒有必要成為一個知識點,或者說是重要的知識點拿出來特殊說明,畢竟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;
} 

本函式的寫法頗為模式化:

  1. 將待拷貝的物件拷貝到新記憶體
  2. 釋放sp_原來指向的記憶體
  3. 使用新拷貝的指標值給sp_賦值。
  4. 最後將 * this的引用返回(可以說凡是期望返回ClassDesignTool& ,最後都是返回 * this)

總結起來就是 綜合了析構和建構函式的操作。銷燬了左值運算物件的資源,而從右值運算物件中拷貝資源。

小結:本文初略的說明了建構函式、解構函式和拷貝賦值運算子的過載,可以作為入門者的參考。