定義類別


有些資料會有相關性,相關聯的資料組織在一起,對於資料本身的可用性或者是程式碼的可讀性,都會有所幫助,例如,在程式中你可能發現,在進行帳戶之類的處理時,帳號、名稱、餘額這三個資料總是一併出現的,這時可以將它們組織在一起,定義為類別:

account.h

#include <string>
using namespace std; 

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

在檔頭檔中定義類別,表頭檔案的名稱建議與類別名稱同名,class 是定義類別的關鍵字,Account 是類別名稱,public 表示定義的 idnamebalance 值域(field),都是可以公開存取的。例如:

main.cpp

#include <iostream> 
#include "account.h"

void printAcct(Account *acct) {
    cout << "Account(" 
         << acct->id << ", "
         << acct->name << ", "
         << acct->balance << ")"
         << endl;
}

void printAcct(Account &acct) {
    printAcct(&acct);
}

int main() { 
    Account acct1;
    acct1.id = "123-456-789";
    acct1.name = "Justin Lin";
    acct1.balance = 1000;
    printAcct(acct1);

    Account *acct2 = new Account();
    acct2->id = "789-654-321";
    acct2->name = "Monica Huang";
    acct2->balance = 1000;   
    printAcct(acct2);
    delete acct2;

    return 0; 
}

Account acct1 建立了 Account 的實例,這時 acct1 在函式執行完畢後就會自動清除,存取實例的值域時可以使用 dot 運算子「.」。

若是 Account acct = acct1 這類指定,會將 acct1 的值域複製給 acct,若 Account 的值域佔用了許多資源,複製會造成負擔的話,可以透過參考或指標來避免複製的動作,例如 printAcct(acct1) 運用的就是參考。

可以使用 new 來動態建構 Account 的實例,動態建立的實例不需要時要使用 delete 清除,透過指標存取實例成員時,要使用箭號運算子「->」。

從 C 背景來的開發者可能會想,這種風格像是 C 的結構(struct),在 C++ 中,struct 也被視為定義類別,將以上的 class 關鍵字換為 struct,程式也可以運作,structclass 的差別在於,前者在第一個權限可見的修飾詞出現前(例如 publicprivate),定義的成員預設會是公開可存取,而後者預設會是私有(也就是 private)。

執行結果如下:

Account(123-456-789, Justin Lin, 1000)
Account(789-654-321, Monica Huang, 1000)

在方才的範例中,初始 Account 值域的流程,其實是重複了,若要消彌這類重複,可以定義建構式(constructor),例如:

account.h

#include <string>
using namespace std; 

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

在標頭檔的建構式定義中,定義了建構實例時,需要帳號、名稱、餘額這三個資料,接下來將方才的初始流程重構至建構式的實作:

account.cpp

#include <string>
#include "account.h"
using namespace std;

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

:: 是類別範圍解析(class scope resolution)運算子,在實作類別建構式或方法(method)時,在 ::前指明實作哪類別之定義。

如果沒有定義任何建構式,編譯器會自動產生沒有參數的預設建構式,如果自定義了建構式,就會使用你定義的建構式,在建構式或方法的實作中,若要存取實例本身,可以透過 this,這是個指標,因此要透過箭號運算子來存取值域。

現在可以如下寫個程式來使用 Account 類別:

main.cpp

#include <iostream> 
#include <string>
#include "account.h"

string to_string(Account &acct) {
    return string("Account(") + 
           acct.id + ", " +
           acct.name + ", " +
           std::to_string(acct.balance) + ")";
}

void deposit(Account &acct, double amount) {
    if(amount <= 0) {
        cout << "必須存入正數" << endl;
        return;
    }
    acct.balance += amount;
}

void withdraw(Account &acct, double amount) {
    if(amount > acct.balance) {
        cout << "餘額不足" << endl;
        return;
    }
    acct.balance -= amount;
}

int main() { 
    Account acct("123-456-789", "Justin Lin", 1000);
    cout << to_string(acct) << endl;

    deposit(acct, 500);
    cout << to_string(acct) << endl;

    withdraw(acct, 700);
    cout << to_string(acct) << endl;

    return 0; 
}

std::to_string 是 C++ 11 定義在 string 中的函式,執行結果如下:

Account(123-456-789, Justin Lin, 1000.000000)
Account(123-456-789, Justin Lin, 1500.000000)
Account(123-456-789, Justin Lin, 800.000000)

範例中的 to_stringdepositwithdraw 都是為了 Account 而設計的,既然這樣,為什麼不將它們放到 Account 的定義中呢?

account.h

#include <string>
using namespace std; 

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

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

以上只定義了方法的簽署,也可以選擇在類別中同時實作方法,這類方法預設是 inline 的,選擇在類別之外實作方法時,則可以明確地指定 inline

現在 to_stringdepositwithdraw 被定義為 Account 的方法了,也稱為成員函式(member function),因為實作時,可以透過 this 來存取實例,就不用在方法上定義接受 Account 的參數了,而原本的 idnamebalance 被放到了 private 區段,這是因為不想被公開存取,也就只能被建構式或方法存取,這麼一來,就可以定義更動這些值域的流程。

實際上,private 在這邊是不需要的,如前頭談過的,以 class 定義類別時,在第一個權限可見的修飾詞出現前,定義的成員預設會是私有。

account.cpp

#include <iostream> 
#include <string>
#include "account.h"
using namespace std;

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

string Account::to_string() {
    return string("Account(") + 
           this->id + ", " +
           this->name + ", " +
           std::to_string(this->balance) + ")";
}

void Account::deposit(double amount) {
    if(amount <= 0) {
        cout << "必須存入正數" << endl;
        return;
    }
    this->balance += amount;
}

void Account::withdraw(double amount) {
    if(amount > this->balance) {
        cout << "餘額不足" << endl;
        return;
    }
    this->balance -= amount;
}

接下來要使用 Account 就簡單多了:

#include <iostream> 
#include <string>
#include "account.h"

int main() { 
    Account acct = {"123-456-789", "Justin Lin", 1000};
    cout << acct.to_string() << endl;

    acct.deposit(500);
    cout << acct.to_string() << endl;

    acct.withdraw(700);
    cout << acct.to_string() << endl;

    return 0; 
}

這就是為什麼要定義類別,將相關的資料與方法組織在一起的原因:易於使用。物件導向目的之一就是易於使用,當然,可以重用也是物件導向的其中一個目的,不過易用性的考量,往往會比重用來得重要,過於強調重用,反而會設計出不易使用的類別。