建構式


在〈定義類別〉中談過,如果沒有定義任何建構式,編譯器會自動產生沒有參數的預設建構式,那麼預設建構式做了什麼呢?如果就以下的類別來說:

class Account { 
public: 
    string id;  
    string name; 
    double balance;
};

預設建構式對每個值域進行預設初始化,對基本型態來說,會初始為各型態的零值,就類別型態來說,使用其無參數建構式來初始化,例如 string 來說,會初始為空字串。

如果定義了類別內初始式(in-class initializer),那麼預設建構式會使用初始式,例如:

class Account { 
public: 
    string id = "000-000-000";  
    string name = "Anonymous"; 
    double balance;
};

id 以定義的初始式初始為 string("000-000-000")name 以定義的初始式初始為 string("Anonymous"),而 double 預設初始為 0.0。

預設建構式只在沒有自定義任何建構式時,編譯器才會產生,若自定義了建構式,就算定義了參數建構式,也不稱為預設建構式。例如:

class Account { 
    string id;  
    string name; 
    double balance;

public: 
    Account() {
        this->id = "000-000-000";
        this->name = "Anonymous";
        this->balance = 0.0;
    };
};

在 C++ 中,絕大多數的情況下,可能不用區分初始化與指定,然而實際上是有分別的,就以上的類別來說,若實例化 Accountidnamebalance 會進行預設初始化,之後執行建構式,將 "000-000-000""Anonymous"、0.0 指定給對應的值域。

預設建構式只在沒有自定義任何建構式時,編譯器才會產生,建構式可以重載,如果自定義了建構式,也想提供無參建構式,並希望其行為與預設建構式相同,可以加上 default。例如:

class Account { 
    string id;  
    string name; 
    double balance;

public: 
    Account() = default;
    Account(string id, string name, double balance);
};

在〈定義類別〉中看過 Account 的建構式是這麼定義的:

Account::Account(string id, string name, double balance) {
    this->id = id;
    this->name = name;
    this->balance = balance;
}

如果建構式中想要指定某個值域的值,可以定義初始式清單(constructor initializer list),就上例來說,可以直接在定義類別時撰寫:

class Account { 
    string id;  
    string name; 
    double balance;

public: 
    Account(string id, string name, double balance) : 
        id(id), name(name), balance(balance) {};
};

這麼一來,id 值域就會用參數 id 的值初始化,name 值域就會用參數 name 的值初始化,balance 值域就會用參數 balance 的值初始化,括號中指定不一定要是參數,也可以是運算式,如果初始式清單省略了某個值域,那就會使用預設初始化;在這邊,初始式清單的順序並不代表值域初始化的順序,值域初始化的順序是依類別中值域定義的順序而定。

絕大多數的情況下,不區分初始化與指定不會有什麼問題,然而底下不區分的話,就會有問題:

class Foo {
    const int wat;
    Foo(int wat) {
        this->wat = wat;
    }
};

若以 Foo(1) 實例化,wat 會預設初始為 0,之後執行建構式流程,然而 watconst 修飾過,不可以在建構式中被指定值了,因此會編譯失敗,然而以下可以通過編譯:

class Foo {
    const int wat;
    Foo(int wat) : wat(wat) {}
}

如果建構過程,想要委由另一個版本的建構式,可以在 : 後指定。例如:

class Account { 
    string id;  
    string name; 
    double balance;

public: 
    Account(string id, string name, double balance) : 
        id(id), name(name), balance(balance) {};
    Account(string id, string name) : Account(id, name, 0.0) {}
};

若以 Account acct("123-456-789", "Justin Lin") 建構實例,那麼會先執行 Account(string id, string name, double balance) 的流程,接著才是 Account(string id, string name) 的流程。

在〈定義類別〉中看過,可以使用以下的方式來建構 Account 實例:

Account acct = {"123-456-789", "Justin Lin", 1000};

在〈不定長度引數〉看過,{"123-456-789", "Justin Lin", 1000} 實際上會建立 initializer_list,可是〈定義類別〉中並沒有定義可接受 initializer_list 的建構式啊?這其實是隱含地型態轉換,預設會尋找符合初始式清單的建構式來進行實例建構。

實際上 string 也是如此,string name = "Justin Lin" 時,"Justin Lin"const *char 型態,隱含地會使用對應的建構式來建構 string 實例。

如果不希望有這種行為,可以在對應的建構式上加上 explicit,例如〈定義類別〉中的類別若定義為:

class Account { 
    string id;  
    string name; 
    double balance;

public: 
    explicit Account(string id, string name, double balance);
    void deposit(double amount);
    void withdraw(double amount);
    string to_string();
};

編譯的時候就會看到以下的錯誤訊息:

error: converting to 'Account' from initializer list would use explicit constructor