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

構造函數

關於構造函數,咱們耳熟能詳,彷佛都沒有必要成爲一個知識點,或者說是重要的知識點拿出來特殊說明,畢竟C++的編譯器都能幫咱們完成這個工做,只是,事情真的如想象的那麼簡單麼;ios

可能不是。c++

本文試圖挖掘關於構造函數,可能不是那麼簡單的一面,固然也不會很全面,權當一塊兒學習了。ide

構造函數的概念:提供類的對象的初始化的方式,類經過一個或幾個特殊的成員函數來控制對象的初始化過程。函數

有這個概念出發,咱們能夠知道,全部的構造函數都是在類的對象初始化時由系統調用的,具體調用哪一個是按重載函數的調用規則來的。工具

備註:構造函數不能被聲明爲const。能夠想一想爲什麼?學習

構造函數也不能是虛函數,這個應該好解釋。測試

默認構造函數

這個最簡單,在面向對象的世界裏,萬物皆是對象,由於萬物皆須要構造函數,若是咱們沒有定義一個構造函數,那麼就由C++的編譯器幫咱們完成,在《c++ primer》裏叫作合成的默認構造函數。this

下面開始咱們的編碼求學之旅:編碼

首先,定義一個類設計者工具類:spa

#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)

總結起來就是 綜合了析構和構造函數的操做。銷燬了左值運算對象的資源,而從右值運算對象中拷貝資源。

小結:本文初略的說明了構造函數、析構函數和拷貝賦值運算符的重載,能夠做爲入門者的參考。

相關文章
相關標籤/搜索