類的基本思想是資料抽象和封裝。
資料抽象是一種依賴於介面和實現分離的編程技術。類的介面包括使用者所能執行的操作;類的實現包括類的資料成員、負責介面實現的函數體以及定義類所需的各種私人函數。
封裝實現了類的介面和實現的分離。封裝後的類隱藏了它的實現細節。
類要實現資料抽象和封裝,需要首先定義一個抽象資料類型。在抽象資料類型中,由類的設計者負責考慮類的實現過程,使用者只需調用,而無需知道類型的工作細節。
1、定義抽象資料類型定義成員函數
成員函數的聲明必須在類的內部,它的定義則既可以在類的內部也可以在類的外部。定義在類的內部的函數是隱式的inline函數。
this
成員函數通過一個名為this的額外的隱式參數來訪問調用它的那個對象。當調用一個成員函數時,用請求該函數的對象地址初始化this。
total.isbn();
則編譯器負責把total的地址傳遞給isbn的隱式形參this.
在成員函數內部,我們可以直接使用調用該函數的對象的成員,而無須通過成員訪問運算子。
string isbn() const{ return this->bookNo;}const成員函數
預設情況下,this的類型是指向類類型非常量版本的常量指標。例如在Sales_data成員函數中,this的類型是Sales_data *const。儘管this是成員函數的隱式的形參,但它仍然需要遵循初始化規則,我們不能把this綁定到一個常量對象上。這也使得不能在一個常量對象上調用普通的成員函數。
因此,為了提高函數的靈活性,我們要將this聲明為指向常量的指標,而this在成員函數的參數列表中是隱式的,所以就在參數列表的後面加上const,用來說明this是一個指向常量的指標。把函數後面用const修飾的成員函數稱為常量成員函數,在常量成員函數中不能改變調用它的對象的內容。
NOTE:常量對象,以及常量對象的引用或指標都只能調用常量成員函數。
類範圍和成員函數
編譯器分兩步處理類:首先編譯成員的聲明,然後才輪到成員函數。因此,成員函數體可以隨意使用類中的其他成員而無須在意出現的次序。
在類的外部定義成員函數
在類外部定義成員函數時必須保證傳回型別、參數列表和函數名都和類內聲明保持一致,對於類內聲明為常量成員函數的,類外定義時也必須在參數列表後明確指定const屬性。同時,類外部定義的成員的名字必須包含它所屬的類名。
1.3 定義類相關的非成員函數
類通常需要定義一些輔助函數,比如read,print等,儘管這些函數定義的操作從概念上來說屬於類的介面的組成部分,但它們實際上並不屬於類本身。
對輔助函數的定義,通常也是將函數的聲明和定義分離開來。如果函數在概念上屬於類但是不定義在類中,則它一般應與類聲明(而非定義)在同一個標頭檔內。
1.4 建構函式
建構函式的任務是初始化類對象的資料成員,無論何時只要類的對象被建立,就會執行建構函式。
建構函式的名字和類名相同,沒有傳回型別,其他和其他函數一樣。類可以包含多個建構函式,和其他重載函數差不多。
建構函式因為其角色既是初始化類對象,因此不能聲明為const。
合成的預設建構函式
類通過一個特殊的建構函式來控制預設初始化過程,這個函數叫做預設建構函式。預設建構函式無須任何實參。
如果我們沒有顯式地定義建構函式,那麼編譯器就會為我們隱式地定義一個預設建構函式。如果我們顯式地定義了建構函式,編譯器將不會為類產生預設建構函式,所以此時必須自己定義一個預設建構函式。
NOTE:只有當類沒有聲明任何建構函式,編譯器才會自動地產生預設建構函式。
某些類不能依賴於合成的預設建構函式
合成的預設建構函式只適合非常簡單的類,對於一個普通的類來說,必須定義它自己的預設建構函式。
- 已經定義了一些其他的建構函式,那麼必須定義預設的建構函式
- 含有內建類型或複合類型成員的類應該在類的內部初始化這些成員,或者定義一個自己的預設建構函式,否則,在建立類的對象時就可能得到未定義的值。因為定義在塊中的內建類型或複合類型的對象被預設初始化,則它們的值是未定義的
- 有的時候編譯器不能為某些類合成預設的建構函式
struct Sales_data{Sales_data() = default;Sales_data(const string &s) :bookNo(s){}Sales_data(const string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p*n){}Sales_data(istream &);string isbn() const{return bookNo;}Sales_data& combine(const Sales_data&);double avg_price() const;string bookNo;unsigned units_sold = 0;double revenue = 0.0;};=default
可以通過在參數列表後面寫上=default來要求編譯器產生建構函式。其中=default既可以聲明一起出現在類的內部,也可以作為定義出現在類的外部。如果=default在類的內部,則預設建構函式是內聯的,如果在類的外部,則該成員預設情況下不是內聯的。
建構函式初始化列表
Sales_data(const string &s) :bookNo(s){}Sales_data(const string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p*n){}
冒號與花括弧之間的部分稱為建構函式初始值列表。它負責為新建立的對象的一個或幾個資料成員賦初值。建構函式初始值是成員名字的一個列表,每個名字後面緊跟括弧括起來(或者在花括弧內的)成員初始值。不同成員的初始化通過逗號分隔開來。
當某個資料成員被建構函式初始值列表忽略時,它將以與合成預設建構函式相同的方式隱式初始化。
NOTE:建構函式不應該輕易覆蓋掉類內的初始值,除非新賦的值與原值不同。如果你不能使用類內初始值,則所有建構函式都應該顯式地初始化每個內建類型的成員。
在類的外部定義建構函式1.5 拷貝、賦值和析構
除了定義類的對象如何初始化之外,類還需要控制拷貝、賦值和銷毀對象時發生的行為。
如果我們不主動定義這些操作,則編譯器將替我們合成它們。一般來說,編譯器產生的版本將對對象的每個成員執行拷貝、賦值和銷毀操作。
2、存取控制與封裝
在C++中,使用訪問說明符加強類的封裝性:
- 定義在public說明符之後的成員在整個程式內可被訪問,public成員定義類的介面。
- 定義在private說明符之後的成員可以被類的成員函數訪問,但是不能被使用該類的代碼訪問,private部分封裝了類的實現細節
class和struct關鍵字
二者唯一的區別是:預設存取權限不一樣,struct的預設許可權是public,而class的預設許可權是private。
2.1 友元
類可以允許其他類或者函數訪問它的非公有成員,方法是令其他類或函數稱為它的友元。
如果類想把一個函數作為它的友元,只需要增加一條以friend關鍵字開始的函式宣告語句即可。
類還可以把其他的類定義成友元,也可以把其他類的成員函數定義成友元。
友元函數能定義在類的內部,這樣的函數是隱式內聯的。
class Sales_data{
friend Sales_data add(); //友元函數
public:Sales_data() = default;Sales_data(const string &s) :bookNo(s){}Sales_data(const string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p*n){}Sales_data(istream &);string isbn() const{return bookNo;}Sales_data& combine(const Sales_data&);private:double avg_price() const;string bookNo;unsigned units_sold = 0;double revenue = 0.0;};
NOTE:友元聲明只能出現在類定義的內部,但是在類內出現的具體位置不限。友元不是類的成員也不受它所在地區存取控制層級的約束。
一般在類定義開始或結束前的位置集中聲明友元。
友元聲明
友元的聲明僅僅指定了訪問的許可權,而非一個通常意義上的函式宣告。如果希望類的使用者能夠調用某個友元函數,那麼就必須在友元聲明之外再專門對函數進行一次聲明。
為了使友元對類的使用者可見,通常把友元的聲明與類本身放置在同一個標頭檔中(類的外部)。我們的Sales_data標頭檔中應該為read、print和add提供獨立的聲明(除了類內部的友元聲明之外)。
類之間的友元關係
視窗管理的類Window_mgr的某些成員可能需要訪問它管理的Screen類的內部資料,例如,Window_mgr的成員函數clear需要清理其所管理的其中某個螢幕的內容,這時候clear需要能夠訪問Screen的私人成員,所以需要在Screen類中將Window_mgr類指定成其友元。
class Screen{ friend class Window_mgr;};
如果一個類指定了友元類,則友元類的成員函數可以訪問該類包括非公有成員在內的所有成員。因此,Window_mgr的clear的函數可以定義成
void Window_mgr::clear(Screen &s){ s.content=string(s.height*s.width,' ');}
上述,在Window_mgr的成員函數clear可以訪問Screen的私人資料成員。
NOTE:友元關係不存在傳遞性,如果Window_mgr類有其他友元,則這些友元並不能理所當然地具有訪問Screen的特權。
每個類負責控制自己的友元類或友元函數。
令成員函數作為友元
除了將整個Window_mgr類作為友元之外,Screen還可以只為clear提供存取權限。
當把一個成員函式宣告成友元時,必須明確指出該成員函數屬於哪個類;
class Screen{ friend void Window_mgr::clear(Screen &s);};函數重載和友元
如果想把幾個重載函數中的某個聲明成友元,直接將該函數在類中進行友元聲明即可,只有聲明了的函數才能訪問類的所有資料成員。
友元聲明和範圍
友元的聲明不是真正意義上的聲明,如果要使用它還必須加上它真正意義上的函式宣告才能使用。
3、類的其他特性3.1 類成員定義一個類型成員
除了定義資料和函數成員之外,類還可以自訂某種類型在類中的別名。由類定義的類型名字和其他成員一樣存在訪問限制,可以是public或者private中的一種。
class Screen{public:using pos = std::string::size_type;private:pos curser = 0;pos height = 0, width = 0;std::string content;};
NOTE:類型成員通常出現在類開始的地方。
Screen類的成員函數
class Screen{public:using pos = std::string::size_type;Screen() = default;//因為Screen有另一個建構函式,//所以本函數是必須的Screen(pos ht, pos wd, char c) :height(ht), width(wd), content(ht*wd, c){}char get() const{return content[cursor];}inline char get(pos ht, pos wd) const;Screen &move(pos r, pos c);private:pos curser = 0;pos height = 0, width = 0;std::string content;};令成員作為內嵌函式
定義在類內部的成員函數都是自動inline的,上述Screen的建構函式和get函數預設是inline函數。
可以在類的內部把inline作為聲明的一部分顯式地聲明成員函數,同樣的,也能在類的外部用inline關鍵字修飾函數的定義。
#include"2.h"inlinechar Screen::get(pos r, pos c) const{pos row = r*width;return content[row + c];}
雖然無須在聲明和定義的地方同時說明inline,但這麼做是合法的。不過,最好只在類外部定義的地方說明inline,這樣可以使類更容易理解。
重載成員函數
成員函數也可以被重載。
可變資料成員
有時希望能修改類的某個資料成員,即使是在一個const成員函數內,那麼可以通過在變數的聲明中加入mutable關鍵字。
一個可變資料成員永遠不會是const,即使它是const對象的成員。因此,一個const成員函數可以改變一個可變成員的值。例如可以給Screen添加一個可變成員,記錄成員函數被調用的次數。
private:pos curser = 0;pos height = 0, width = 0;std::string content;mutable size_t access_cnt;============================#include"2.h"inlinechar Screen::get(pos r, pos c) const{++access_cnt;pos row = r*width;return content[row + c];}類資料成員的初始值
類內初始值必須使用=的初始化形式或者花括弧括起來的直接初始化形式。
3.2 返回*this的成員函數
返回*this可以把函數嵌入到一組動作序列中。
myScreen.move(4,0).set('#');從const成員函數返回*this
一個const成員函數如果以引用的形式返回*this,那麼它的傳回型別將是常量引用,不能將其嵌入到一組動作序列中。
基於const的重載
對於非常量成員函數和常量成員函數的重載,根據對象是否是const來決定調用哪個成員函數。
3.3 類類型
每個類定義了唯一的類型。對於兩個類來說,即使它們的成員完全一樣,這兩個類也是兩個不同的類型。
類的聲明
可以僅聲明而暫時不定義它。這種聲明被稱作前向聲明。但對於一個類來說,在建立它們的對象之前該類必須被定義過,而不能僅僅聲明它,因為編譯器需要知道類的對象需要多大的儲存空間。
4、類的範圍範圍和定義在類外部的成員
一旦遇到類名,定義的剩餘部分就在類的範圍之內了,這裡的剩餘部分包括參數列表和函數體。救過就是,可以直接使用類的其他成員而無須再次授權。
void Window_mgr::clear(ScreenIndex i){ Screen &s=screens[i]; s.contents=string(s.height*s.width,' ');}
上述Window_mgr::已經明確說明了後續部分處於Window_mgr範圍中了,因此使用Window_mgr的成員ScreenIndex以及screens就不需要說明Window_mgr的範圍了,但是在Window_mgr::範圍之前的函數傳回型別部分就不在Window_mgr範圍內,因此如果函數傳回型別部分需要用到Window_mgr中的成員,必須對該成員加以命名空間說明。
//假設傳回型別是ScreenIndex則,類外部的定義應該是Window_mgr::Screen_mgrWindow_mgr::clear(ScreenIndex i){}4.1 名字尋找與類的範圍
一般情況下,名字尋找的步驟如下:
- 在名字所在的塊中尋找其聲明語句,只考慮在名字的使用之前出現的聲明
- 如果沒找到,繼續尋找外層範圍
- 如果最終沒有找到匹配的聲明,則程式報錯
但對於類內部的成員函數來說,起名字尋找的步驟不一樣,類的定義分兩步處理:
- 首先,編譯成員的聲明
- 直到類全部可見後才編譯函數體
NOTE:編譯器處理完類中的全部聲明後才會處理成員函數的定義。
用於類成員聲明的名字尋找
上述兩階段的處理方式只適用於成員函數中使用的名字。聲明中使用的名字,包括傳回型別或者參數列表中使用的名字,都必須在使用前確保可見。
類型名要特殊處理
類型名的定義通常出現在類的開始處,這樣能確保所有使用該類型的成員都出現在類名的定義之後。
typedef double Money;class Screen{};成員定義中的普通塊範圍的名字尋找
成員函數中使用的名字按照如下方式解析:
- 首先,在成員函數內尋找該名字的聲明
- 如果在成員函數內沒有找到,則在類內繼續尋找
- 如果類內也沒找到該名字的聲明,則在外圍範圍繼續尋找
當成員函式宣告了與類的成員同名的成員時,成員函數中的聲明將覆蓋類的同名成員,如果想使用類的同名成員,此時可以加上類的名字或顯式地使用this指標來強制訪問類的成員。
5、再談建構函式5.1 建構函式初始值列表
Sales_data::Sales_data(const string &s, unsigned cnt, double price){ bookNo=s; units_sold=cnt; revenue=cnt*price;}
注意上述這種建構函式的寫法,其和使用冒號的形式不同,區別是使用冒號是初始化了它的資料成員,而這個版本是對資料成員執行了賦值操作。這以區別到底會有什麼深層次的影響完全依賴於資料成員的類型。
建構函式的初始值有時必不可少
有時可以忽略資料成員初始化和賦值之間的差異,但並非總能這樣。如果成員時const或者是引用的話,必須將其初始化。類似的,當成員屬於某種類類型且該類沒有定義預設建構函式時,也必須將這個成員初始化。
class ConstRef{public: ConstRef(int ii);private: int i; const int ci; int &ri;};ConstRef::ConstRef(int ii){ i=ii; //正確 ci=ii; //錯誤,不能給const賦值 ri=i; //錯誤,ri沒有初始化}
正確的建構函式應該是
CosntRef::ConstRef(int ii):i(ii),ci(ii),ri(i){}
NOTE:如果成員是const、引用、或者屬於某種未提供預設建構函式的類類型,我們必須通過建構函式初始值為這些成員提供初值。
建議使用建構函式初始值。
成員初始化的順序
成員的初始化順序與它們在類定義中的出現順序一致,建構函式初始值列表中初始值的前後位置關係不會影響實際的初始化順序。
NOTE:最好令建構函式初始值的順序與成員聲明的順序保持一致,如果可能的話,盡量避免使用某些成員初始化其他成員。
預設實參和建構函式
如果一個建構函式為所有參數都提供了預設實參,則它實際上也定義了預設建構函式。
5.2 委託建構函式
一個委託建構函式使用它所屬類的其他建構函式執行它自己的初始化過程,或者說它把它自己的一些職責委託給了其他建構函式。
class Sales_data{public: Sales_data(string s,unsigned cnt, double price):bookNo(s), units_sold(cnt), revenue(cnt*price){} //其餘建構函式全都委託給上面的建構函式 Sales_data():Sales(" ",0,0){} Sales_data(string s): Sales_data(s,0,0){} Sales_data(istream &is):Sales_data(){read(is,*this);}};
當一個建構函式委託給另一個建構函式時,受委託的建構函式的初始值列表和函數體被依次執行。在Sales_data類中,受委託的建構函式體恰好是空的。加入函數體包含有代碼的話,將先執行這些代碼,然後控制權才會交還給委託者的函數體。
5.3 預設建構函式的作用
在實際中,如果定義了其他建構函式,那麼最好也提供一個預設建構函式
5.4 隱式的類類型轉換
可以為類定義隱式轉換規則。
如果建構函式只接受一個實參,則它實際上定義了轉換為此類類型的隱式轉換機制,有時把這種建構函式稱為轉換建構函式。
只允許一步類類型轉換
編譯器只會自動地執行一步類型轉換
item.combine("hello!");
這個就是錯誤的,需要兩種轉換,首先將"hello!"轉換成string,然後再將臨時的string轉換成Sales_data。
對於上述這種錯誤,可以顯式地將“hello!”轉換成string
item.combine(string("hello!"))類類型轉換不是總有效抑制建構函式定義的隱式轉換
可以通過將建構函式聲明為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&);};
此時,沒有任何建構函式能用於隱式地建立Sales_data對象,
item.combine(null_book);item.combine(cin);
這兩種試圖通過string和istream轉換成Sales_data對象的行為都不能通過編譯。
NOTE:explicit只對一個實參的建構函式有效,且只能在類內聲明建構函式時才使用explicit關鍵字,在類外部定義時不應重複。
explicit建構函式只能用於直接初始化
Sales_data item1(null_book); //正確,直接初始化Sales_data item2=null_book;//錯誤,null_book想隱式轉換成Sales_data,但建構函式聲明了explicit,因此不能隱式轉換
為轉換顯式地使用建構函式
儘管編譯器不會將explicit的建構函式用於隱式轉換,但是可以使用這樣的建構函式進行顯式強制轉換。
itme.combine(Sales_data(null_book)); //直接調用Sales_data建構函式item.combine(static_cast<Sales_data>(cin)); //強制轉換
5.5 彙總類
彙總類使得使用者可以直接存取其成員,並且具有特殊的初始化文法形式。當一個類滿足下麵條件時,是彙總類
- 所有成員都是public
- 沒有定義任何建構函式
- 沒有類內初始值
- 沒有基類,也沒有virtual函數
可以提供一個花括弧括起來的成員初始值列表,並用它來初始化彙總類的資料成員。初始值的順序必須和聲明的順序一致,如果初始值的數目少於類的成員的數量,後面的則預設初始化。
6、類的靜態成員
類的靜態成員只與類本身有關,而與類的對象無關。
聲明靜態成員
在聲明前面加上“static”關鍵字,使其與類關聯在一起。和其他成員一樣,靜態成員可以是public或private。
靜態成員函數也不與任何對象綁定在一起,它們不包含this指標。靜態成員函數不能聲明成const的,而且也不能在static函數體內使用this指標。
使用類的靜態成員
使用範圍運算子直接存取靜態成員。
double r;r=Account::rate();
還可以使用類的對象、引用或指標來訪問靜態成員。
Account ac1;Account *ac2=&ac1;r=ac1.rate();r=ac2->rate();
成員函數不用通過範圍運算子就能直接使用靜態成員。
定義靜態成員
既可以在類內也可以在類外定義靜態成員函數。當在類的外部定義靜態成員時,不能重複static關鍵字,static關鍵字只能出現在類內部的聲明語句。
一般來說,不能在類的內部初始化靜態成員。相反地,必須在類的外部定義和初始化每個靜態成員,且只能被定義一次。
1.hclass Account{private: static double interestRate; static double initRate();};1.cppdouble Account::interestRate=initRate(); //定義並初始化靜態成員
從類名Account開始,定義語句的剩餘部分就都位於類的範圍之內了,因此,可以直接使用initRate函數,即使它是private的。
靜態成員的類內初始化
即使一個常量待用資料成員在類內部被初始化了,通常情況下也應該在類的外部定義一下該成員。
靜態成員能用於某些情境,而普通成員不能
待用資料成員的類型可以就是它所屬的類類型,而非待用資料成員則受到限制,只能聲明成它所屬類的指標或引用。
class Bar{private: static Bar mem1; //正確,靜態成員可以是不完全類型 Bar *mem2; //正確,指標成員可以是不完全類型 Bar mem3; //錯誤,資料成員必須是完全類型};
靜態成員和普通成員的另一個區別就是可以使用靜態成員作為預設實參
class Screen{public: Screen& clear(char=bkground);private: static const char bkground;};