類的兩項基本能力:
(1) 數據抽象,即定義數據成員和函數成員的能力。
(2) 封裝,即保護類的成員不被隨意訪問的能力。通過將類的實現細節設爲private,我們就能完成類的封裝。類可以將其他類或者函數設爲友元,這樣它們就能訪問類的非公有成員了。
1. 如何設計一個類,如何定義類及類的成員
(1)類可以在它的第一個訪問說明符之前定義成員,對這種成員的訪問權限依賴於類定義的方式。如果使用struct關鍵字,則定義在第一個訪問說明符之前的成員是public的;相反,如果使用class關鍵字,則這些成員是private的。
// 使用struct關鍵字定義類
struct 類名 {
// 訪問說明符之前的行爲或屬性默認是public的
public:
//公有的行爲或屬性
private:
//私有的行爲或屬性
};
// 使用class關鍵字定義類
class 類名 {
// 訪問說明符之前的行爲或屬性默認是private的
public:
//公有的行爲或屬性
private:
//私有的行爲或屬性
};
Note: 類定義結束後的那個分號不能省略。
(2)儘管所有成員函數都必須在類的內部聲明,但是成員函數體可以定義在類內也可以定義在類外。當我們在類的外部定義成員函數時,成員函數的定義必須與它的聲明匹配。也就是說,返回類型、參數列表和函數名都得與類內部的聲明保持一致。如果成員被聲明成常量成員函數,那麼它的定義也必須在參數列表後明確指定 const 屬性。同時,類外部定義的成員名字必須包含它所屬的類名。
class Sales_data {
// 友元聲明,後面會講
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
public:
// 構造函數,採用參數初始化表
Sales_data(): units_sold(0), revenue(0.0) {
}
Sales_data(const std::string &s):
bookNo(s), units_sold(0), revenue(0.0) {
}
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {
}
// 成員函數的聲明
Sales_data(const std::string &book, const unsigned num,
const double sellp, const double salep);
Sales_data(std::istream &is);
// 類的內部定義成員函數
std::string isbn() const {
return bookNo;
}
Sales_data &combine(const Sales_data &rhs) {
units_sold += rhs.units_sold; // 把rhs的成員加到this對象的成員上
revenue += rhs.revenue;
return *this; //返回調用該函數的對象
}
double avg_price() const {
if (units_sold) {
return revenue/units_sold;
} else {
return 0;
}
}
private:
std::string bookNo; // 書籍編號,隱式初始化爲空串
unsigned units_sold = 0; // 銷售量,顯式初始化爲0
double sellingprice = 0.0; // 原始價格
double saleprice = 0.0; // 實售價格
double discount = 0.0; // 折扣
double revenue; // 收入
};
// 類的外部定義成員函數,在類的內部已經做過聲明
Sales_data::Sales_data(std::istream &is) {
read(is, *this); // read函數的作用是從is中讀取一條交易信息然後
// 存入this對象中
}
Sales_data::Sales_data(const std::string &book, const unsigned num, const double sellp, const double salep) {
bookNo = book;
units_sold = num;
sellingprice = sellp;
saleprice = salep;
if (sellingprice == 0) {
discount = saleprice / sellingprice; // 計算實際折扣
}
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
std::istream &read(std::istream &is, Sales_data &item) {
is >> item.bookNo >> item.units_sold >> item.sellingprice >> item.saleprice;
return is;
}
std::ostream &print(std::ostream &os, const Sales_data &item) {
os << item.isbn() << " " << item.units_sold << " " << item.sellingprice
<< " " << item.saleprice << " " << item.discount;
return os;
}
(3)令成員作爲內聯函數: 在類中,常有一些規模較小的函數適用於被聲明成內聯函數,定義在類內部的成員函數是自動 inline 的。在類外定義內聯函數時,可以在類的內部把 inline 作爲聲明的一部分顯式地聲明成員函數,在類的外部用 inline 關鍵字修飾函數的定義:
inline
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) {
return lhs.isbn() < rhs.isbn();
}
Note: 雖然在聲明和定義的地方同時說明 inline 是合法的,但最好只在類外部定義的地方說明 inline,這樣可以使類更容易理解。
內聯函數作用: 內聯函數在編譯的時候將不進行函數調用,編譯器將內聯函數的代碼粘貼在調用(形式上調用)處,可以提高效率。內聯函數只能是代碼很少很簡單的函數,如果一個很大很複雜的函數即使被設爲內聯,編譯器也將自動設置該函數爲非內聯。
(4)重載成員函數: 如果同一作用域的幾個函數名字相同但形參列表不同,我們稱之爲重載(overloaded)函數。和非成員函數一樣,成員函數也可以被重載,只要函數之間在參數的數量和/或類型上有所區別就行。
// 構造函數的重載
Sales_data(){}
Sales_data(std::istream &is){};
Sales_data(const std::string &s){}
Sales_data(const std::string &s, unsigned n, double p){}
Note: 不能通過函數的返回值類型不同來定義重載函數,只能通過形參列表的不同。
2. 類成員的訪問權限
C++通過 public、protected、private 三個關鍵字來控制成員變量和成員函數的訪問權限,它們分別表示公有的、受保護的、私有的,被稱爲成員訪問限定符。所謂訪問權限,就是你能不能操作該類中的成員。
在類的內部(定義類的代碼內部),無論成員被聲明爲 public、protected 還是 private,都是可以互相訪問的,沒有訪問權限的限制。
在類的外部(定義類的代碼之外),只能通過對象訪問成員,並且通過對象只能訪問 public 屬性的成員,不能訪問 private、protected 屬性的成員。
Note: C++ 中的 public、private、protected 只能修飾類的成員,不能修飾類,C++中的類沒有公有私有之分。
3. 構造函數和析構函數
(1)構造函數: 在C++中,有一種特殊的成員函數,它的名字和類名相同,沒有返回值,不需要用戶顯式調用(用戶也不能調用),而是在創建對象時自動執行。這種特殊的成員函數就是構造函數(Constructor)。
(2)構造函數的重載: 和普通成員函數一樣,構造函數是允許重載的。一個類可以有多個重載的構造函數,創建對象時根據傳遞的實參來判斷調用哪一個構造函數。構造函數的調用是強制性的,一旦在類中定義了構造函數,那麼創建對象時就一定要調用,不調用是錯誤的。如果有多個重載的構造函數,那麼創建對象時提供的實參必須和其中的一個構造函數匹配;反過來說,創建對象時只有一個構造函數會被調用。
(3)析構函數: 析構函數(Destructor)是一種特殊的成員函數,沒有返回值,不需要程序員顯式調用(程序員也沒法顯式調用),而是在銷燬對象時自動執行。構造函數的名字和類名相同,而析構函數的名字是在類名前面加一個~符號。
注意:析構函數沒有參數,不能被重載,因此一個類只能有一個析構函數。如果用戶沒有定義,編譯器會自動生成一個默認的析構函數。
// constructor.cpp
#include <iostream>
#include <string>
using namespace std;
class Sales_data {
public:
// 構造函數
Sales_data(): units_sold(0), revenue(0.0) {
cout << "constructor1 executed" << endl;
}
// 構造函數的重載
Sales_data(const std::string &s):
bookNo(s), units_sold(0), revenue(0.0) {
cout << "constructor2 executed" << endl;
}
// 析構函數
~Sales_data() {
cout << "destructor executed" << endl;
}
private:
std::string bookNo;
unsigned units_sold;
double revenue;
};
int main(int argc, char *argv[])
{
Sales_data sale1;
Sales_data sale2("978-7-121-15535-2");
return 0;
}
由運行結果知,創建對象時系統會自動調用構造函數進行初始化工作,程序即將結束時系統會自動調用析構函數進行清理工作。
4. 如何聲明並使用友元
(1)類可以允許其他類或者函數訪問它的非公有成員,方法是令其他類或者函數成爲它的友元(friend)。如果類想把一個函數作爲它的友元,只需要增加一條 friend 關鍵字開始的函數聲明語句即可。
friend Sales_data add(const Sales_data&, const Sales_data&);
友元聲明只能出現在類定義的內部,但是在類內出現的具體位置不限。友元不是類的成員也不受它所在區域訪問控制級別的約束。
(2)類還可以把其他的類定義成友元,也可以把其他類(之前定義過的)的成員函數定義成友元。此外,友元函數能定義在類的內部,這樣的函數是隱式內聯的。
class Screen {
// Window_mgr的成員可以訪問Screen類的私有部分
friend class Window_mg;
private:
unsigned height = 0, width = 0;
unsigned cursor = 0;
string contents;
public:
Screen() = default; // 默認構造函數
Screen(unsigned ht, unsigned wd, char c) : height(ht), width(wd),
contents(ht * wd, c) { }
};
class Window_mgr {
public:
// 窗口中每個屏幕的編號
using ScreenIndex = std::vector<Screen>::size_type;
// 按照編號將指定的Screen重置爲空白
void clear(ScreenIndex);
private:
std::vector<Screen> screens{Screen(24, 80, ' ')};
};
void Window_mgr::clear(ScreenIndex i) {
// s是一個Screen的引用,指向我們想清空的那個屏幕
Screen &s = screens[i];
// 將那個選定的Screen重置爲空白
s.contents = string(s.height * s.width, ' ');
}
如果一個類指定了友元類,則友元類的成員函數可以訪問此類包括非公有成員在內的所有成員。首先把s定義成screens vector中第i個位置上的Screen的引用,隨後利用Screen的height和width成員計算出一個新的string對象,並令其含有若干個空白字符,最後我們把這個含有很多空白的字符串賦給contents成員。如果clear不是Screen的友元,上面的代碼將無法通過編譯,因爲此時clear將不能訪問Screen的height、width和contents成員。而當Screen將Window_mgr指定爲其友元之後,Screen的所有成員對於Window_mgr就都變成可見的了。
Note: 友元關係不具有傳遞性,也就是說,如果Window_mgr有它自己的友元,則這些友元並不能理所當然地具有訪問Screen的特權,每個類負責控制自己的友元類或友元函數。
使用友元的利弊:當非成員函數確實需要訪問類的私有成員時,我們可以把它聲明成該類的友元。此時,友元可以“工作在類的內部”,像類的成員一樣訪問類的所有數據和函數。但是一旦使用不慎(比如隨意設定友元),就有可能破壞類的封裝性。
5. 類的靜態成員和靜態函數
有時候類需要它的一些成員與類本身直接相關,而不是與類的各個對象保持關聯。例如,一個銀行賬戶類可能需要一個數據成員來表示當前的基準利率。在此例中,我們希望利率與類關聯,而非與類的每個對象關聯。從實現效率的角度來看,沒必要每個對象都存儲利率信息。而且更加重要的是,一旦利率浮動,我們希望所有的對象都能使用新值。
聲明靜態成員
我們通過在成員的聲明之前加上關鍵字 static 使得其與類關聯在一起。和其他成員一樣靜態成員可以是 public 的或 private 的。靜態數據成員的類型可以是常量、引用、指針、類類型等。
class Account {
public:
void calculate() {
amount += amount * interestRate;
}
static double rate() {
return interestRate;
}
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
static 成員變量屬於類,不屬於某個具體的對象,即使創建多個對象,也只爲靜態成員變量分配一份內存,所有對象使用的都是這份內存中的數據。當某個對象修改了靜態成員變量,也會影響到其他對象。
static 成員變量既可以通過對象來訪問,也可以通過類來訪問。
//通過類類訪問 static 成員變量
Account::interestRate = 2.735;
//通過對象來訪問 static 成員變量
Account account("hp", 10000);
account.interestRate = 2.735;
//通過對象指針來訪問 static 成員變量
Account *pa = new Account("hp", 10000);
pa -> interestRate = 2.735;
因爲靜態數據成員不屬於類的任何一個對象,所以它們並不是在創建類的對象時被定義的。這意味着它們並不是由類的構造函數初始化的。而且一般來說,我們不能在類的內部初始化靜態成員。相反地,必須在類的外部定義和初始化每個靜態成員。一個靜態數據成員只能定義一次,並且它將一直存在於程序的整個生命週期中。
我們定義靜態數據成員的方式和在類的外部定義成員函數差不多。我們需要指定對象的類型名,然後是類名、作用域運算符以及成員自己的名字:
// 定義並初始化一個靜態成員
double Account::interestRate = initRate();
這條語句定義了名爲 interestRate 的對象,該對象是類 Account 的靜態成員,其類型是 double。從類名開始,這條定義語句的剩餘部分就都位於類的作用域之內了。因此,我們可以直接使用 initRate 函數。注意,雖然 initRate 是私有的,我們也能用它初始化 interestRate。和其他成員的定義一樣,interestRate 的定義也可以訪問類的私有成員。
Note: static 成員變量的內存既不是在聲明類時分配,也不是在創建對象時分配,而是在(類外)初始化時分配。反過來說,沒有在類外初始化的 static 成員變量不能使用。static 成員變量不佔用對象的內存,而是在所有對象之外開闢內存,即使不創建對象也可以訪問。具體來說,static 成員變量和普通的 static 變量類似,都在內存分區中的全局數據區分配內存。
使用 static 成員變量而不是全局變量的優點:
(1) static 成員的名字是在類的作用域中,因此可以避免與其他類的成員或全局對象名字衝突。
(2) 可以實施封裝。static 成員可以是私有成員,而全局對象不可以。
(3) 通過閱讀程序容易看出 static 成員是與特定類關聯的,這種可見性可清晰地顯示程序員的意圖。
定義靜態成員函數
在類的內部聲明函數時需要添加 static 關鍵字,但是在類外部定義函數時就不需要了。
static 成員函數是類的組成部分但不是任何對象的組成部分,它有以下幾個特點:
(1) static 函數沒有 this 指針。
(2) static 成員函數不能被聲明爲 const (將成員函數聲明爲 const 就是承諾不會修改該函數所屬的對象)。
(3) static 成員函數也不能被聲明爲虛函數。
和其他的成員函數一樣,我們既可以在類的內部也可以在類的外部定義靜態成員函數。當在類的外部定義靜態成員時,不能重複 static 關鍵字,該關鍵字只出現在類內部的聲明語句:
void Account::rate(double newRate) {
interestRate = newRate;
}
靜態成員函數與普通成員函數的根本區別:
(1)普通成員函數有 this 指針,可以訪問類中的任意成員。
(2)而靜態成員函數沒有 this 指針,只能訪問靜態成員(包括靜態成員變量和靜態成員函數)。
Note: 在C++中,靜態成員函數的主要目的是訪問靜態成員,靜態成員函數可以通過類來調用(一般都是這樣做),也可以通過對象來調用。