C++ 學習筆記之(7)-類
類的基本思想是數據抽象和封裝。封裝實現了類的接口和實現的分離。數據抽象是依賴於接口和實現分離的編程技術。
定義抽象數據類型
定義改進的Sales_data
類
struct Sales_data{
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
double avg_price() const;
// 數據成員
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
}
// Sales_data 的非成員接口函數
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
定義在類內部的函數是隱式的
inline
函數所有成員必須在類內部聲明,但成員函數體定義可在類內或類外
this
爲成員函數的隱式參數,由函數對象地址初始化,爲常量指針,指向對象本身,不可改變。常量成員函數(
const member function
):成員函數參數列表後加const
關鍵字,作用是修改隱式this
指針的類型,使其成爲指向常量對象的常量指針const Sales_data *const
。因爲默認this
類型爲指向類類型非常量版本的常量指針Sales_data *const
,故this
無法綁定到常量對象。常量成員函數不能修改對象內容。std::string isbn() const { return bookNo; }
常量對象以及常量對象的引用或指針都只能調用常量成員函數
編譯器分兩步處理類,首先編譯類成員聲明,然後到成員函數體。故成員函數體可隨意使用類其他成員。
定義類相關的非成員函數
類需要一些輔助函數比如上述add
, read
等,這些函數屬於類的接口組成部分,實際不屬於類本身
如果非成員函數是類接口的組成部分,則這些函數的聲明應該與類在同一個頭文件中
IO類屬於不能被拷貝的類型,所以只能通過引用傳遞他們
istream &read(istream &is, Sales_data &item) { double price = 0; is >> item.bookNo >> item.units_sold >> price; item.revenue = price * item.units_sold; return is; }
構造函數
構造函數是用來控制其對象的初始化過程。
構造函數名字和類名相同,沒有返回類型
構造函數不能被聲明成
const
,當創建類的const
對象時,知道構造函數完成初始化過程,對象才真正取得常量
屬性默認構造函數:類通過一個特殊的構造函數控制默認初始化過程,無需任何實參。當類沒有顯示定義構造函數時,編譯器會隱式定義默認構造函數,又被稱爲
合成的默認構造函數
如果類包含內置類型或複合類型的成員,則應賦予其類內初始值,這樣類才適合使用合成的默認構造函數,否則默認初始化,其值未定義。
C++11新標準定義在參數列表後使用
= default
要求編譯器生成構造函數。struct Sales_data{ Sales_data() = default; }
構造函數初始值列表:負責爲新創建對象的一個或幾個數據成員賦初值
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenus(p*n) {}
訪問控制與封裝
C++使用訪問說明符(access specifiers)加強累的封裝性
- 定義在
public
說明符之後的成員在整個程序內可被訪問 - 定義在
private
說明符後的成員可被類成員函數訪問,但不能被使用該類的代碼訪問
- 定義在
struct
和class
的默認訪問權限不一樣。class
關鍵字的默認爲private
友元:類可以使其他類或函數成爲該類友元,就可以使其他類或函數訪問它的非公有成員
friend Sales_data add(const Sales_data&, const Sales_data&);
友元聲明僅指定了範文權限,而非通常意義的函數聲明,最好在友元聲明外再對函數進行一次聲明
類的其他特性
類成員再探
inline
可用在類內部函數聲明,也可用在類外部函數定義處可變數據成員(mutable):永遠不會是
const
, 即使它是const
對象的成員,即一個const
成員函數可以改變一個可變成員的值class A { public: void add() const { ++b; // 成立,因爲是可變成員 mutable } private: mutable int b; // 即使在一個`const`對象內也能被修改 }
返回 *this
的成員函數
- 一個
const
成員函數如果以引用形式返回*this
, 那麼它的返回類型將是常量引用 - 通過區分成員函數是否爲
const
,可以對其進行重載,原因類似於函數參數中指針是否爲const
類類型
- 前向聲明:函數聲明和定義分離,在聲明之後定義之前是一個不完全類型,即只知類類型,卻不清楚包含那些成員
- 不完全類型的使用非常有限:可以定義指向不完全類型的指針或引用個也可以聲明(但不能定義)以不完全類型作爲參數或返回類型的函數。
友元再探
類之間的友元關係
class Screen{
friend class Window_mgr; // Window_mgr 的成員可以訪問 Screen 類的私有部分
};
- 若類指定了友元類,則友元類的成員函數可以訪問此類包括非公有成員在內的所有成員
- 友元關係不存在傳遞性,即
window_mgr
的友元類不具有訪問Screen
的特權 - 每個類負責控制自己的友元類或友元函數
令成員函數作爲友元
class Screen{
friend void Window_mgr::clear(ScreenIndex); // Window_mgr::clear必須在Screen類之前被聲明
}
友元函數設計規則
- 首先定義
Window_mgr
類,其中聲明clear
函數,但不能定義。在clear
使用Screen
的成員之前必須先聲明Screen
- 接下來定義
Screen
, 包括對於clear
的友元聲明 - 最後定義
clear
,此時它纔可以使用Screen
的成員
友元聲明和作用域
友元聲明的作用是影響訪問權限,並非普通意義的聲明
struct X{ friend void f() { /* 友元函數可以定義在類內部 */} X() { f(); } // 錯誤:f 還沒有被聲明 void g(); void h(); }; void X::g() { return f(); } // 錯誤:f 還沒有被聲明 void f(); // 聲明那個定義在 X 類中的函數 void X::h() { return f(); } // 正確:現在 f 的聲明在作用域中了
類的作用域
一個類就是一個作用域,故外部定義類成員函數需要提供類名和函數名,在類外部,成員的名字會被隱藏
編譯器處理完類中的全部聲明後纔會處理成員函數的定義
外層對象若被隱藏,可使用作用域運算符訪問
void Screen::dummy_fcn(pos height){ cursor = width * ::height; // 全局變量height }
構造函數再探
構造函數初始值列表
- 若在構造函數的初始值列表中未顯示初始化成員,則成員會在構造函數體之前執行默認初始化
- 構造函數初始值是進行初始化,構造函數體中執行的是賦值
- 若成員是
const
、引用,或者某種未提供默認構造函數的類類型,必須通過構造函數初始值列表進行初始化。 - 初始化和賦值的區別事關底層效率問題
- 初始化是直接初始化數據成員
- 賦值是先初始化後賦值
- 成員初始化順序和在類定義中的出現順序一致,並且儘量避免使用某些成員初始化其他成員
- 如果一個構造函數爲所有參數都提供了默認實參,則它實際上也定義了默認構造函數
委託構造函數
C++11定義了 委託構造函數(delegating constuctor):即可使用其所屬類的其他構造函數執行它的初始化過程
class Sales_data{
public:
// 非委託構造函數使用對應的實參初始化成員
Sales_data(std::string s, unsigned cnt, double price): bookNo(s), units_sold(cnt), revenue(cnt * price) {}
// 委託構造函數將初始化過程委託給另一個構造函數,先執行被委託的上述構造函數,在執行本函數體
Sales_data(): Sales_data("", 0, 0) {}
}
- 先執行被委託的構造函數,再執行委託者的函數體
隱式的類類型轉換
轉換構造函數:若構造函數只接受一個實參,則實際上定義了轉換爲此類類型的隱式轉換機制,這種構造函數被稱爲轉換構造函數
string null_book = '9-999=99999-9'; // 構造臨時Sales_data對象,對象的units_sold 和 revenue 爲0, bookNo 等於 null_book item.combine(null_book);
編譯器只會自動執行一步類型轉換
// 錯誤:需要兩步轉換,先將字符串字面值轉爲string,再將臨時的string對象轉換成Sales_data對象 item.combine("9-999-99999-9"); item.combine(string("9-999-99999-9")); // 正確:顯示轉換成 string,隱式轉換成 Sales_data
可以通過將構造函數聲明爲
explicit
阻止隱式轉換,explicit
只對一個實參的構造函數有效,需要多個實參的構造函數不能用於執行隱式轉換。class Salse_data{ public: explicit Sales_data(const std::string &s): bookNo(s) {} }
只能在類內聲明構造函數時使用
explicit
關鍵字,類外部定義時不應重複explicit
構造函數只能用於直接初始化,不能用於拷貝形式初始化
聚合類
聚合類是指用戶可以直接訪問其成員,並且有特殊的初始化語法形式,條件如下
- 所有成員都是
public
的 - 沒有定義任何構造函數
- 沒有類內初始值
- 沒有基類,也沒有
virtual
函數
字面值常量類
字面值常量類的要求
- 數據成員都必須是字面值類型
- 類必須至少含有一個
constexpr
構造函數 - 如果一個數據成員含有類內初始值,則內置類型成員的初始值必須是一條常量表達式;或者如果成員屬於某種類類型,則初始值必須使用成員自己的
constexpr
構造函數 - 類必須使用析構函數的默認定義,該成員負責銷燬類的對象
constexpr
構造函數
構造函數不能爲const
,但字面值常量類的構造函數可以使constexpr
函數
constexpr Debug(bool h, bool i, bool o): hw(h), io(i), other(o) {}
constexpr
構造函數必須初始化所有數據成員,初始值使用constexpr
構造函數或者是常量表達式
類的靜態成員
與類而非對象關聯的成員爲靜態成員,可在成員聲明之前加上關鍵字static
, 靜態成員可以是public
或private
的,類型可以是常量、引用、指針或類類型等
靜態成員函數不能聲明成
const
static
函數體內不能使用this
指針,因爲靜態成員函數不與對象綁定,沒有this
指針可以使用作用域運算符直接訪問靜態成員,可以使用類對象、引用或指針訪問靜態成員
double r; r = Account::rate(); // 使用作用域運算符訪問靜態成員 Account ac1, *arc = &ac1; // 調用靜態成員函數 rate 的等價形式 r = ac1.rate(); // 通過 Account 的對象或引用 r = ac2->rate(); // 通過指向 Account 對象的指針
成員函數不需要通過作用域運算符就能直接使用靜態成員
static
關鍵字只能出現在類內部的聲明語句中因爲靜態數據成員不屬於累的任何一個對象,所以不能再類內部初始化靜態成員,必須在類外部定義和初始化每個靜態成員,靜態數據成員只能定義一次
若靜態成員爲字面值常量類型的
constexpr
,可以爲靜態成員提供const
整數類型的類內初始值,初始值必須是常量表達式靜態數據成員可以使不完全類型,特別的,可以就它所屬的類類型。而非靜態成員則收到限制,只能聲明成它所屬類的指針或引用
靜態成員可以做默認實參,而普通成員不行
結語
類是C++語言中最基本的特性,有兩項基本能力:數據抽象,即定義數據成員和函數成員的能力;二是封裝,即保護類的成員不被隨意訪問的能力。通過將類的實現細節設爲private
,就可以實現封裝。類可以將其他類或者函數設爲友元,這樣它們就能訪問類的非公有成員
類可以定義構造函數,用來初始化對象。構造函數可以重載,切應該使用構造函數初始值列表來初始化所有數據成員
類還能定義可變或靜態成員,一個可變成員永遠不會是const
, 即使在const
成員函數內也能修改它的值;一個靜態成員可以使函數也可以是數據,靜態成員存在於所有對象之外。