天天看點

第十二章:類(二)

構造函數

構造函數是一類特殊的成員函數,它是在構造對象時調用的成員函數。構造函數的名稱與類名相同,無傳回類型,可以包含多個版本(重載)

class Str{

public:
    Str(){
        std::cout << "Constructor is called " << std::endl;
    }

    Str(int input){
        x = input;
    }

private:
    int x;
};
           

在C++11之後,标準引入了一種新的構造函數-代理構造函數

class Str{

public:
	// 這裡需要注意下代理構造函數的執行順序
    Str() : Str(3)					// 先執行 Str(3)
    {								// 再執行函數體
        std::cout << x << std::endl;
    }

    Str(int input){
        x = input;
        std::cout << x << std::endl;
    }

private:
    int x;
};
           

在構造函數中,我們通常需要對成員變量進行初始化和指派,在不使用初始化清單的情況下,我們隻能使用以下的方式

class Str{

public:
    Str(const std::string & val)
    {
        x = val;				// 并不高效,因為這裡其實是指派語句,而初始化語句被系統預設調用,并進行了預設的指派
    }


private:
    std::string x;
};
           
這種方式的主要缺點是進行了一次無意義的預設值指派操作

在使用初始化清單之後,我們可以将初始化與指派結合在一起,進而減少不必要的操作,提升程式的性能,比如

class Str{

public:
    Str(const std::string & val): x(val)		// 高效的方式
    {
    }


private:
    std::string x;
};
           

對于初始化清單,我們需要注意以下幾點

  • 通常情況下使用初始化清單可以提升系統性能
  • 一些情況下必須使用初始化清單,比如類中包含引用成員
  • 元素的初始化順序與其聲明順序相關,與初始化清單中的順序無關
  • 使用初始化清單可以覆寫類内成員初始化的行為,初始化清單的優先級要高于類内成員初始化

在構造函數中還存在一些特殊的構造函數,首先我們關注預設構造函數。 預設構造函數是不需要提供實際參數就可以調用的構造函數,比如

class Str{

public:
    Str(){

    }
    Str(int input = 3){

    }

private:
    int x;
};
           
注意以上兩種都是預設構造函數,但是在不提供參數進行構造時會報錯,因為系統無法判斷需要調用哪一個

關于預設構造函數,我們需要注意以下幾點

  • 如果類中沒有提供任何構造函數,那麼在條件允許的情況下,編譯器會生成一個預設構造函數
    注意,如果提供了帶參數的構造函數,那麼編譯器不會生成無參數的預設構造函數,這時如果使用預設的方式構造,會報錯
    注意,生成預設構造函數需要在條件允許的情況下。典型的比如類内包含引用成員,在這種情況下是不允許生成預設構造函數的,因為沒有辦法為引用成員初始化
  • 調用預設構造函數時要避免

    most vexing parse

    class Str{
    
    public:
        Str(){
    
        }
    
    private:
        int x;
    };
    
    int main() {
        Str m();			// 注意,這玩意是函數聲明,不是使用預設構造函數來構造對象
    }
               
  • 可以使用

    default

    關鍵字來定義預設構造函數
    class Str{
    
    public:
        Str() = default;			// 強制編譯器在存在帶參數構造函數的情況下生成預設構造函數
       
        Str(int input): x(input)
        {
            
        }
    
    private:
        int x;
    };
               

之後我們關注單一參數構造函數,他在構造時可以有一些特殊的用法,比如

class Str{

public:
    Str(int input): x(input)
    {

    }

private:
    int x;
};

int main() {
    Str m = 3;
}
           

關于單一構造函數,我們需要注意以下幾點

  • 單一參數構造函數可以視為一種類型轉換函數
  • 可以使用

    explicit

    關鍵字避免求值過程中的隐式轉換
    class Str{
    
    public:
        explicit  Str(int input): x(input)
        {
    
        }
    
    private:
        int x;
    };
    
    int main() {
        Str m = 3;					// 此時這裡是非法的
    }
               

接下來我們關注拷貝構造函數。拷貝構造函數是一種接收一個目前類對象的構造函數

class Str{

public:
    Str() = default;
    Str(const Str& x)
        :val(x.val)
    {
    }

private:
    int val = 3;
};

int main() {
    Str m ;
    Str m2 = m;			// 調用拷貝構造函數
}
           

關于拷貝構造函數,我們需要注意以下幾點

  • 拷貝構造函數會在涉及到拷貝初始化的場景被調用,比如:參數傳遞。是以要注意拷貝構造函數的形參類型。
    如果不加引用會導緻無限調用拷貝構造函數,直接報錯。其次,一般我們不希望拷貝構造函數對傳入的對象進行修改,是以一般會加上

    const

    進行修飾
  • 如果沒有顯式提供拷貝構造函數,那麼編譯器會自動生成一個拷貝構造函數,自動生成的版本會依次對每個資料成員調用拷貝構造。
  • 可以使用

    default

    關鍵字來生成預設的拷貝構造函數
    class Str{
    
    public:
        Str() = default;
        Str(const Str&) = default;
    
    private:
        int val = 3;
    };
               

最後,我們關注C++11引入的一種新的構造函數-移動構造函數。移動構造函數是接收一個目前類右值引用對象的構造函數。

class Str{

public:
    Str() = default;
    Str(const Str & val) = default;
    Str(Str&& x)					// 移動構造函數
        :val(std::move(x.val))
    {
        
    }

private:
    std::string val;
};
           

關于移動構造函數,我們需要注意以下幾點

  • 移動構造函數可以從輸入對象中“偷竊”資源,隻要確定在偷竊資源之後輸入對象處于合法即可
  • 當某些特殊成員函數(如拷貝構造)未定義時,編譯器可以生成一個預設移動構造函數。預設移動構造函數的行為是對資料成員中有移動構造函數的成員調用對應的移動構造函數,沒有移動構造函數的成員調用對應的拷貝構造函數
  • 移動構造函數通常聲明未不可抛出異常的函數(

    noexcept

    ),這樣可以避免引入異常處理邏輯,提升性能。同時,一些可以選擇調用移動構造和拷貝構造的邏輯隻有在移動構造是

    noexcept

    時才會選擇使用移動構造進行加速,這樣主要是考慮了異常安全的問題。
  • 注意右值引用對象做表達式時是左值

繼續閱讀