C++:Special Member Functions

Special Member Functions

區別於定義類的行爲的普通成員函數,類內有一類特殊的成員函數,它們負責類的構造拷貝移動銷燬ios

構造函數

構造函數控制對象的初始化過程,具體來講,就是初始化對象的數據成員。構造函數的名字與類名相同,且沒有返回值。構造函數也能夠有重載,重載區別於參數數量或參數類型。與其餘成員函數不一樣的是,構造函數不能被聲明爲const,對象的常量屬性是在構造函數完成初始化以後得到的。編程

默認構造函數

默認構造函數的工做是:若是在類內定義了成員的初始值,那麼用初始值初始化成員;不然,默認初始化成員。數組

默認初始化是指定義變量時不賦予初始值時被賦予默認值的動做。定義於函數體外的內置類型若是沒有被顯式初始化,則賦值0;在函數體內定義的變量不會被初始化。函數

class LiF {
public:
    LiF(int _lif = 0) { lif = _lif; } // 指定了lif的初值,這是一個默認構造函數
private:
    int lif;
}

LiF l; // 調用默認構造函數,此時l.lif值爲0

再看下面這種狀況:this

class LiF1 {
public:
    LiF1(int _lif = 0) { lif = _lif; }
    int lif;
};

class LiF2 {
public:
    LiF1 lif1;
};

LiF2 l2;
std::cout << l2.lif1.lif << std::endl; // 輸出結果是0

在上面的例子中,咱們並無爲LiF2定義默認構造函數,但它又執行了默認構造。這是由於,當類沒有定義任何構造函數,而程序又須要用到構造函數時,編譯器會自動生成一個合成的默認構造函數(synthesized default constructor)。須要注意的是,只有在類內全部成員都具備類內初始值的時候,編譯器才能合成默認構造函數。debug

class LiF {
public:
    void print() { cout << lif << endl; }
private:
    int lif;
};

LiF l;
l.print();

在上面的代碼中,看似是咱們須要一個默認構造函數來完成對l的初始化,理所應當地,編譯器應該爲咱們生成一個合成的默認構造函數,但實際運行時,發現l.lif的值是未定義的。再看下面這種狀況:指針

class LiF {
public:
    int lif;
};

LiF l;
if (l.lif) {
    cout << l.lif << endl;
}

此次連編譯都沒有經過,報錯信息指出程序正在試圖訪問一個未初始化變量,即l.lif。經過上面這兩種狀況能夠看出,編譯器並不會由於程序「須要」默認構造函數,就自動生成一個合成的默認構造函數。事實上,只有下面幾種狀況,編譯器會生成合成的默認構造函數:code

  1. 類含有類對象成員,且該對象類型有默認構造函數(對應第一個例子)。
  2. 類繼承自帶有默認構造函數的類。
  3. 類內帶有虛函數,因爲虛函數表指針的存在,每一個對象的構造都須要賦予該指針正確的值,而這個工做由默認構造函數完成。
  4. 類虛繼承自另外一個類,虛繼承的派生類包含一個指向虛基類的指針,該指針一樣須要正確的值,這個工做一樣由默認構造函數完成。

C++11提供了default關鍵字,能夠經過指定 = default來顯式生成默認構造函數。對象

class LiF {
public:
    LiF() = default;
    void print() { cout << lif << endl; }
private:
    int lif;
};

LiF l;
l.print(); // l.lif的值是未定義的

此外,若是類內包含一個沒法默認初始化的const成員,那麼編譯器也會拒絕生成默認構造函數。繼承

class LiF {
public:
    LiF() = default;
    void print() { cout << lif << endl; }
private:
    const int lif;
};

LiF l; // 編譯沒法經過,提示默認構造函數被禁用

《C++ Primer》也建議,不要依賴編譯器提供的合成的特殊成員函數

構造函數初始值列表

構造函數還能夠包括一部分特殊的內容,這部分稱爲構造函數初始值列表(constructor initialize list),C++建議,在列表內完成成員的初始化。相比在構造函數體內初始化,初始值列表能夠初始化常量成員。

class LiF {
public:
    LiF(int _lif) { lif = _lif; } // 編譯報錯,提示常成員lif沒有初始化
    LiF(int _lif): lif(_lif) {} // 經過編譯
private:
    const int lif;
};

使用初始值列表初始化一個對象時,列表參數的順序並不影響成員的初始化順序,決定初始化順序的是成員的定義順序。良好的編程規範是,初始化列表的成員順序儘可能與成員的定義順序保持一致

class LiF {
public:
    LiF(int val): b(val), a(b) {}
private:
    int a;
    int b;
};

委託構造函數

C++11擴展了構造函數初始值列表的功能,容許定義委託構造函數(delegating constructor)。委託構造函數能夠經過初始化列表把初始化任務委託給以前已經定義過的構造函數。

class LiF {
public:
    LiF(int _a, int _b): a(_a), b(_b) {} // 普通構造函數
    LiF(): LiF(0, 0) {} // 經過委託定義了默認構造函數
private:
    int a;
    int b;
};

轉換構造函數

若是一個類存在一個只接受單個參數的構造函數,那麼這個函數就定義了一個從參數類型向類類型隱式轉換的規則,這個函數也被稱爲轉換構造函數(converting constructor)。這種隱式轉換沒法嵌套,即編譯器只會自動作一次隱式轉換。

class LiF {
public:
    LiF(int _lif = 0) : lif(_lif) {}
    void doNothing(const LiF &l) {} // doNothing()函數須要一個LiF對象的引用
private:
    int lif;
};

LiF l1;
l1.doNothing(1); // 這裏執行了隱式轉換,用參數1生成了一個LiF對象

隱式轉換可能帶來一些沒法預知的後果,有時咱們並不但願隱式轉換的發生。C++提供了explicit關鍵字以禁用這種隱式轉換。explicit只容許出如今函數聲明處,且只適用於單參數構造函數,多參數構造函數並不存在隱式轉換規則。

class LiF {
public:
    explicit LiF(int _lif = 0) : lif(_lif) {}
    void doNothing(const LiF &l) {} // doNothing()函數須要一個LiF對象的引用
private:
    int lif;
};

LiF l1;
l1.doNothing(1); // 編譯沒法經過,由於隱式轉換已被禁用

拷貝構造函數

class LiF {
public:
    LiF();
    LiF(const LiF& l): lif(l.lif) {}
    int lif;
};

在翻閱原碼的時候,常常能見到形如上面的類。其中第二個構造函數就是拷貝構造函數(copy constructor)。最多見的拷貝構造函數每每是:形參列表只包含一個自身類類型的引用,因爲拷貝過程當中並不會改變被拷貝的對象,這個引用通常也是按const屬性傳遞。在定義一個對象時,若是採用=的方式初始化,那麼執行的就是拷貝初始化。

string s1("s"); // 直接初始化
string s2(s1); // 直接初始化
string s3 = s1; // 拷貝初始化
string s4 = "s"; // 隱式拷貝初始化
string s5 = string("s"); // 顯式拷貝初始化(等價於s4)

爲何是按引用傳遞呢?若是按值傳遞,在傳遞過程當中會隱式調用拷貝構造函數生成函數實參,引起無限循環調用。

一般狀況下,拷貝構造函數都是被隱式調用的,所以通常不聲明爲explicit。和默認構造函數相似,若是程序沒有定義拷貝構造函數,在須要時,編譯器會自動生成一個合成的拷貝構造函數(synthesized copy constructor),這個函數會拷貝對象的全部成員,但不一樣的是,即便咱們定義了其餘(非拷貝)構造函數,編譯器也會生成。拷貝構造函數也可使用初始化列表。須要注意的是,合成的拷貝構造函數進行的是淺拷貝

調用拷貝構造函數的場景:

  1. =定義對象
  2. 把對象做爲實參傳遞給非引用類型的形參(這也解釋了爲何拷貝構造函數要按引用傳遞)
  3. 返回一個非引用類型的對象
  4. 用花括號列表初始化數組元素或聚合類成員
string a;
void doNothing(string a);
doNothing(a); // 對應2

string doNothing();
doNothing(); // 對應3

string s[2] = {"1", "2"}; // 對應4.1

struct LiF {
    string a;
};
LiF lif = {"a"}; // 對應4.2

移動構造函數

在C++11中,出現了對象移動的特性。在某些狀況下,咱們拷貝的對象會被當即銷燬,如:使用函數調用的返回值給對象賦值。這種拷貝是沒必要要的,在這種狀況下,更好的方法是移動(move)對象。爲了支持移動操做,C++11提供了move語義以及右值引用move被定義在標準庫中,用於把一個左值轉換爲右值引用,所謂右值引用即綁定到臨時對象的引用。有了右值和move,就能夠輕鬆定義移動構造函數:

#include <iostream>

class LiF {
public:
    LiF(int _lif = 0) : lif(_lif) { std::cout << "default" << std::endl; } // 默認構造函數
    LiF(const LiF& l) : lif(l.lif) { std::cout << "copy" << std::endl; } // 拷貝構造函數
    LiF(LiF&& l) : lif(l.lif) { std::cout << "move" << std::endl; } // 移動構造函數
private:
    int lif;
};

int main() {
    LiF l1; // 調用默認構造函數
    LiF l2 = l1; // 調用拷貝構造函數
    LiF l3 = std::move(l1); // 調用移動構造函數
    return 0;
}

當一個類同時存在拷貝和移動構造時,編譯器會經過參數是不是右值判斷應該使用哪一種構造函數。當參數是右值時,編譯器會選擇移動構造,在移動構造函數中,表面上是把右值引用的對象賦值給待構造的對象,實際上,資源的全部權已經發生了改變,待構造的對象「竊取」了資源。

運算符重載

拷貝賦值運算符重載

經過重載賦值運算符=,類也能夠控制對象的賦值。相似地,若是一個類沒有定義拷貝賦值運算符,編譯器會生成一個合成拷貝賦值運算符(synthesized copy-assignment operator)。回顧通常的賦值操做:首先給=左側的對象賦予右側對象的值,而後返回整個表達式的值(左側對象)。爲了與通常的賦值操做對應,在重載賦值運算符時,一般把返回類型置爲對象引用並返回左側對象(*this)。一樣地,合成的拷貝賦值運算符進行的也是淺拷貝

class LiF {
public:
    LiF(int *_lif): lif(_lif) {}
    LiF(const LiF &l): lif(l.lif) {} // 顯式定義拷貝構造函數
    LiF& operator= (const LiF& l) {
        lif = l.lif;
        return *this;
    } // 顯式定義拷貝賦值運算符重載
    int *lif;
};

int a;
LiF l1(&a);
LiF l2 = l1; // 此時l一、l2中的lif成員指向的都是a的地址

拷貝構造函數與拷貝賦值運算符的行爲很類似,但經過他們的名字能夠很好地理解它們的工做:拷貝構造函數負責拷貝一個對象並構造另外一個對象,拷貝賦值運算符負責拷貝一個對象的內容並賦值給一個已存在的對象,即二者的區別爲有無新對象生成。

移動賦值運算符重載

相似地,咱們還能夠重載=進行移動賦值。

#include <iostream>
using std::string;
using std::cin;
using std::cout;
using std::endl;

class LiF {
public:
    LiF(string _lif = "lif") : lif(_lif) { cout << "default" << endl; } // 默認構造函數
    LiF(const LiF& l) : lif(l.lif) { cout << "copy" << endl; } // 拷貝構造函數
    LiF(LiF&& l) noexcept : lif(l.lif) { cout << "move" << endl; } // 移動構造函數
    LiF& operator= (const LiF &l) { // 拷貝賦值
        cout << "copy=" << endl;
        lif = l.lif;
        return *this;
    }
    LiF& operator= (LiF &&l) noexcept { // 移動賦值
        cout << "move=" << endl;
        if (this != &l) {
            std::swap(lif, l.lif);
        }
        return *this;
    }
    ~LiF() { cout << "destruct" << endl; }
    void print() { cout << lif << endl; }
private:
    string lif;
};

int main() {
    LiF l1; // 調用默認構造函數
    LiF l2 = l1; // 調用拷貝構造函數
    LiF l3 = std::move(l1); // 調用移動構造函數
    return 0;
}

一樣,移動賦值運算符接受的參數也是一個右值引用。編譯器也會在須要時合成移動構造函數和移動賦值運算符。但只有當一個類沒有定義任何拷貝控制,且其全部成員都是可移動構造或移動賦值時,編譯器才能合成。

析構函數

最後一類特殊成員函數叫作析構函數(destructor),與構造函數相反,析構函數負責釋放對象佔用的資源,銷燬對象的數據成員。一樣地,當一個類沒有定義本身的析構函數時,編譯器會生成一個合成析構函數(synthesized destructor)

class LiF {
public:
    ~LiF(){}
};

析構函數的名字爲~加上類名,且不接受任何參數。每一個類類型的對象被銷燬都會執行本身的析構函數,內置類型沒有析構函數。與構造函數不一樣的是,析構函數的析構部分是隱式的,並不會出如今函數體內。析構函數的函數體用於進行一些額外的操做,如:銷燬相關對象,打印debug信息等。真正的析構發生在析構函數函數體以後,且成員按初始化順序的逆序銷燬。特殊地,析構函數不能被聲明爲delete

總結

在編寫一個類時,不管是否須要,都應該顯式地定義以上特殊成員函數(移動構造和移動賦值能夠視狀況而定)。

相關文章
相關標籤/搜索