有些資料會有相關性,相關聯的資料組織在一起,對於資料本身的可用性或者是程式碼的可讀性,都會有所幫助,例如,在程式中你可能發現,在進行帳戶之類的處理時,帳號、名稱、餘額這三個資料總是一併出現的,這時可以將它們組織在一起,定義為類別:
account.h
#include <string>
using namespace std;
class Account {
public:
string id;
string name;
double balance;
};
在檔頭檔中定義類別,表頭檔案的名稱建議與類別名稱同名,class
是定義類別的關鍵字,Account
是類別名稱,public
表示定義的 id
、name
與 balance
值域(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
,程式也可以運作,struct
與 class
的差別在於,前者在第一個權限可見的修飾詞出現前(例如 public
、private
),定義的成員預設會是公開可存取,而後者預設會是私有(也就是 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_string
、deposit
、withdraw
都是為了 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_string
、deposit
、withdraw
被定義為 Account
的方法了,也稱為成員函式(member function),因為實作時,可以透過 this
來存取實例,就不用在方法上定義接受 Account
的參數了,而原本的 id
、name
、balance
被放到了 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;
}
這就是為什麼要定義類別,將相關的資料與方法組織在一起的原因:易於使用。物件導向目的之一就是易於使用,當然,可以重用也是物件導向的其中一個目的,不過易用性的考量,往往會比重用來得重要,過於強調重用,反而會設計出不易使用的類別。