C++ 面向物件 | 類
根據看的書(《C++ Primer 5th》)和網上搜集的資料做一個知識沉澱。目前只是對「類」這個部分的鞏固梳理。
類的基本思想
類的基本思想:
- 資料抽象:依賴於介面 和實現 分離的程式設計(及設計)技術
- 封裝:實現了類的介面和實現的分離
類的介面 包括:
- 使用者所能執行的操作
類的實現 包括:
- 類的資料成員
- 負責介面實現的函式體
- 定義類的各種私有函式
這裡我的理解是類的介面應該是 public 的,而類的實現是 private 的。
成員函式和非成員函式
類的成員函式是類的一個成員,它可以操作類的任意物件,可以訪問物件中的所有成員。成員函式的宣告必須在類的內部,定義既可以在類的內部也可以在類的外部。屬於介面部分
非成員函式是屬於類的實現部分,不屬於介面部分。它們的定義和宣告都在類的外部。
在類內部中定義的成員函式是隱式的inline
函式。
行內函數是在編譯時展開,因此沒有執行時開銷,可以用來優化規模小、流程直接、頻繁呼叫的函式。
在類外部中定義的成員函式,但是定義的時候,返回型別、引數列表、函式名需要加上類的作用域
,比如Sale_data::avg_price()
。
this 指標
成員函式通過this
的額外隱式引數來訪問呼叫它的那個物件。this
是一個常量指標,不允許改變this
中儲存的地址。
友元函式沒有this
指標,因為友元不是類的成員。只有成員函式才有this
指標。
const 成員函式
有些成員函式在緊隨引數列表之後有const
關鍵字,在這裡,const
的作用是修改隱式this
指標的型別。
預設情況下,this
的型別是指向類型別非常量
版本的常量指標
。
當我們在函式體內不會修改this
所指向的物件,這時我們把this
設定為指向常量類型別的常量指標。
像這樣使用const
的成員函式稱作常量成員函式。
建構函式
建構函式是類的一種特殊的成員函式,它會在每次建立類的新物件時執行。是定義物件初始化的方式。
建構函式的名稱與類的名稱是完全相同的,並且不會返回任何型別,也不會返回void
。建構函式可用於為某些成員變數設定初始值。
建構函式不能被宣告為 const 函式,因為當我們建立類的一個 const 物件時,直到建構函式完成初始化過程,物件才真正取得“常量”屬性。
建構函式可以在類的外部定義 。
合成的預設建構函式
預設的建構函式控制預設初始化過程,不需要任何實參。
如果類沒有顯式的定義建構函式,那麼編譯器就會為我們隱式的定義一個預設建構函式,編譯器建立的預設建構函式又叫做合成的預設建構函式 。這個合成的預設建構函式將按照以下規則來初始化類的資料成員:
- 如果存在類內的初始值,用它來初始化成員
- 否則,預設初始化該成員
一旦為類定義了一些其他的建構函式,那麼除非我們再定義一個預設的建構函式 ,否則類將沒有預設的初始化函式。
Sale_data() = default;// 通過 = default 要求編譯器生成預設建構函式
建構函式的初始值列表
Line::Line(double len, int p): length(len), price(p) { };
: length(len), price(p)
這部分程式碼就是建構函式初始值列表。這個有兩個引數的建構函式分別用那兩個引數初始化成員。
當某個資料成員被建構函式初始值列表忽略時,它將以與合成建構函式相同的方式隱式初始化。
拷貝、賦值和析構
除了定義類的物件如何初始化之外,類還需要控制拷貝、賦值和銷燬物件發生的行為。
如果我們不主動定義這些操作,則編譯器會替我們合成它們。
但是當類需要分配物件之外的資源時,合成的版本會失敗。比如管理動態記憶體的類。
訪問控制與封裝
說明符
封裝類需要用訪問說明符:
- 定義在 public 說明符之後的成員在整個程式內可被訪問,public 成員定義類的介面
- 定義在 private 說明符之後的成員可被類的成員函式訪問,但是不能被使用該類的程式碼訪問,private 部分封裝了類的實現細節
建構函式和部分成員函式在 public 之後;資料成員和作為實現部分的函式跟在 private 之後。
class 和 struct 關鍵字
class 和 struct 關鍵字定義的類不同在於預設的訪問許可權不同:
- 使用 struct:定義在第一個說明符之前的成員都是 public 的
- 使用 class:定義在第一個說明符之前的成員都是 private 的
友元
類可以允許其他類或者函式訪問它的非公有成員,方法是令其他的類或者函式稱為它的友元 。
如果一個類想把一個函式作為它的友元,只需要增加一條以friend
關鍵字開始的函式宣告語句即可:
class Sales_data { // 友元宣告 friend Sales_data add(const Sales_data&, const Sales_data&); // .... public: // ... private: // ... }
友元宣告只能出現在類定義的內部,但是在類內出現的具體位置不限制。
注意:warning::友元的宣告僅僅是指定了訪問的許可權,而非一個通常意義上的函式宣告。因此如果我們希望呼叫這個友元函式,還需要在友元宣告之外再專門對函式進行一次宣告。通常放在與類在同一個標頭檔案中的類的外部。
類的其他特性
行內函數
定義在類的內部的成員函式是自動 inline 的。同時也可以在類的內部顯示的宣告成員函式為 inline,也可以在外部定義時使用 inline 關鍵字修飾函式的定義。無需再宣告和定義的地方同時說明 inline,但是這樣是合法的。最好只在類外部定義的地方說明 inline,這樣可使類更容易理解。
可變資料成員
有時(並不頻繁)會發生這樣這樣的情況:希望能修改類的某個資料成員,即使在一個 const 成員函式中。可以通過在變數的宣告中加入mutable
關鍵字做到這一點。
一個可變資料成員永遠不會是 const,即使它是 const 物件的成員。一個 const 成員函式可以改變一個可變資料成員的值。
類資料成員的初始值
一個類裡面有一個類資料成員,這個類資料成員需要有一個預設值,通常將這個預設值宣告為一個類內初始值:
class Windows { private: std::vector<Screen> screens{Screen(24, 80, ' ')}; }
注意:使用花括號。
友元再探
類可以將其他的類定義成友元,也可以將其他類的(已經定義過的)成員函式定義為友元。友元函式能定義在類的內部,這樣的函式是隱式 inline 的。
// 類作為友元 class Screen { friend class Windows; // ... } // 類的函式作為友元 class Screen { friend void Windows::clear(...); // ... }
友元關係不具有傳遞性,比如 B 是 C 的友元,而 A 是 B 的友元,但是 A 不具有能訪問 C 的特權。
一個類想要把一組過載函式宣告為它的友元,它需要對這組函式中的每一個分別宣告。
類和非成員函式的宣告不是必須在它們的友元宣告之前。
其他
常量物件無法呼叫非常量成員函式。
非常量物件可以呼叫常量成員函式或者非常量成員函式。
類的作用域
每個類都會定義它自己的作用域。一個類就是一個作用域,當我們在類的外部定義成員函式時,必須同時提供類名和函式名。當提供了類名後,定義的剩餘部分就在類的作用域之內了。
函式返回型別通常出現在函式名之前,因此成員函式定義在外面時,返回型別中使用的名字都位於類的作用域之外,這時當返回型別是類中的成員時必須指明它是那個類的成員。
Windows::ScreenIndex Windows::addScreen(...) { // ... }
名字查詢
成員函式中使用的名字按照以下方式解析:
- 首先,在成員函式中查詢該名字
- 如果在成員函式中沒有找到,則在類中繼續查詢,這時類的所有成員都可以考慮
- 如果類中也沒有找到該名字的宣告,在成員函式定義以前的作用域內 繼續查詢
建構函式再探
如果一個類的資料成員是「const」或者「引用」,必須將其初始化。類似的,當成員是某種類型別且該類沒有定義預設建構函式時,也必須將其初始化。
因此如果成員是「const」、「引用」或者屬於某種未提供預設建構函式的類型別,必須通過建構函式初始化列表 為這些成員提供初值。
預設實參
如果一個建構函式為所有的引數都提供了預設實參,則它實際上也定義了預設建構函式。
委託建構函式
一個委託建構函式使用它所屬類的其他建構函式執行它自己的初始化過程,或者說把它自己的一些(或者全部)職責委託給了其他建構函式。
一個委託建構函式有一個成員初始值的列表和一個函式體。在委託建構函式內,成員初始值列表只有一個唯一的入口,就是類名本身。類名之後緊跟圓括號括起來的引數列表,引數列表與類中的另一個建構函式匹配 。
當一個建構函式委託給另一個建構函式時,受委託的建構函式的初始值列表和函式體被依次執行,然後控制權才會交還給委託者的函式體。
隱式的類型別轉換
轉換建構函式:建構函式只接受一個實參, 則它實際上定義了轉換為此類型別的隱式轉換機制。通過一個實參呼叫的建構函式定義了一條從建構函式的引數型別向類型別隱式轉換機制。
string null_book = "9-99-999"; item.combine(null_book);// 編譯器會將 null_book 自動建立了一個 Sales_data 物件
// 錯誤 item.combine("9-99-999"); // 正確:顯示的轉換為 string,隱式的轉換為 Sales_data item.combine(string("9-99-999")); // 正確:隱式的轉換為 string,顯式的轉換為 Sales_data item.combine(Sales_data("9-99-999"));
想要抑制建構函式定義的隱式轉換,可通過 explicit 宣告建構函式。
關鍵字 explicit 只對一個實參的建構函式有效,只能在類內宣告建構函式時使用 explicit 關鍵字,在類外部定義時不應重複。
class Sales_data { public: Sales_data() = default; Sales_data(const string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { } explicit Sales_data(const string &s): bookNo(s) { } explicit Sales_data(istream &); private: // ... }
聚合類
聚合類使得使用者可以直接訪問其成員,並且有特殊的初始化語法形式。當一個類滿足如下條件時,我們說它是聚合的:
- 所有的成員都是 public
- 沒有定義任何的建構函式
- 沒有類內初始值
- 沒有基類,也沒有 virtual 函式
初始化聚合類物件:
struct Data { int v; string s; }; // 通過花括號括起來的成員初始值列表初始化資料成員 Data val1 = {0, "Anna"};
初始化順序必須與宣告的順序一致!若初始值列表中的元素個數少於類的成員數量,則靠後的成員被值初始化。
字面值常量類
資料成員都是字面值型別的聚合類是字面值常量類。
字面值型別:算數型別、引用、指標。
類是字面值型別的話,該類可能含有 constexpr 函式成員。
如果一個類不是聚合類,但它符合以下要求,則它也是一個字面值常量類:
- 資料成員都是字面值型別
- 類必須含有一個 constexpr 建構函式
- 如果一個數據成員含有類內初始值,則內建型別成員的初始值必須是一條常量表達式;或者如果成員屬於某種類型別,則初始值必須使用成員自己的 constexpr 建構函式
- 類必須使用解構函式的預設定義,該成員負責銷燬類的物件
字面值常量類的建構函式可以是 constexpr 函式。事實上,一個字面值常量類必須至少提供一個 constexpr 建構函式。
constexpr 建構函式可以宣告成=default
的形式。constexpr 建構函式體一般來說應該是空的。
類的靜態成員
有時候類的一些成員直接與類相關,而不是與類的各個物件相關。
在成員的宣告之前加上關鍵字static
使其與類關聯在一起。靜態成員可以是public
或者private
。靜態成員的型別可以是常量、引用、指標、類型別。
類的靜態成員存在於任何物件之外,物件中不包含任何與靜態資料成員有關的資料。
類似的,類的靜態成員函式也不和任何物件繫結在一起,他們不包含this
指標。靜態成員函式不能宣告為const
的,不能在 static 函式體內使用this
指標。
雖然靜態成員不屬於某個物件,但是可以使用類的物件、引用或者指標來訪問靜態成員。
成員函式不能通過作用域運算子就能直接使用靜態成員。
定義靜態成員
可以在類的內部或者外部定義靜態成員函式。static 關鍵字只能出現在類內部的宣告語句中 。
靜態成員不是由類的建構函式初始化的,而且不能再類的內部初始化靜態成員。相反的,必須在類的外部定義和初始化每個靜態成員。和其他物件一樣,一個靜態成員只能定義一次。
靜態成員的類內初始化
通常情況下,類的靜態成員不應該在類的內部初始化。然而我們可以為靜態成員提供 const 整數型別的類內初始化,不過要求靜態成員必須是字面值常量型別的 constexpr。初始值必須是常量表達式。
class Accout { public: // ... private: static constexpr int period = 30 }
即使一個一個常量靜態資料成員在類內部被初始化了,通常情況下也應該在類的外部定義一下該成員。
靜態成員和普通成員的一個區別:靜態成員可以作為預設實參,非靜態成員不能作為預設實參。