類的基本思想是數(shù)據(jù)抽象data abstraction和封裝 encapsulation。
數(shù)據(jù)抽象是一種依賴于接口 interface和實(shí)現(xiàn) implementation分離的編程和設(shè)計(jì)技術(shù)
封裝實(shí)現(xiàn)了類的接口和實(shí)現(xiàn)的分離
c++ Primer 第五版 第230頁
成員函數(shù)的聲明必須在類的內(nèi)部,他的定義既可以在類的內(nèi)部也可以在外部。作為接口組成部分的非成員函數(shù),定義和聲明都在類的外部
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 add(const Sales_data&,, const Sales_data&);
std::ostream &print(std::ostream&,, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
我們再觀察一次對(duì)isbn成員函數(shù)的調(diào)用
total.isbn()
當(dāng)我們調(diào)用成員函數(shù)時(shí),實(shí)際上是在替某個(gè)對(duì)象調(diào)用它。如果isbn指向Sales_data的成員(例如bookNo),它隱式的指向調(diào)用該函數(shù)的對(duì)象的成員。在上面的調(diào)用中,isbn隱式的返回total.bookNo
成員函數(shù)通過this的額外隱式參數(shù)來訪問調(diào)用他的對(duì)象,也就是說編譯器負(fù)責(zé)將total的地址傳遞給isbn的隱式形參this
//偽代碼 說明調(diào)用成員函數(shù)的實(shí)際執(zhí)行過程
Sales_data::isbn(&total);
在成員函數(shù)內(nèi)部 我們可以直接調(diào)用該函數(shù)的對(duì)象成員
,而無需通過成員訪問運(yùn)算符做到這一點(diǎn),因?yàn)閠his所指的正是這個(gè)對(duì)象。也就是說isbn使用bookNo時(shí),就像我們書寫了this->bookNo一樣
isbn的另一個(gè)關(guān)鍵之處是緊隨參數(shù)列表后面的const,他的作用是隱式修改this指針的類型
默認(rèn)情況下,this的類型是指向類類型非常量版本的常量指針
在成員函數(shù)中,this的類型是Sales_data * const,隱式參數(shù)仍然要遵循初始化規(guī)則,意味著(在默認(rèn)情況下),我們不能把this綁定在 一個(gè)常量對(duì)象上(因?yàn)榈讓邮且粋€(gè)非const),這種情況使得我們不能在一個(gè)常量對(duì)象上調(diào)用普通的成員函數(shù)
如果isbn是一個(gè)普通函數(shù)且this是一個(gè)普通指針參數(shù)u,那么可以聲明成const Sales_dataconst,但由于this是隱式的,所以如何做出類似的聲明就成了一個(gè)問題了。
C++的做法是允許把const放在成員函數(shù)的參數(shù)列表之后,緊隨在參數(shù)列表之后的const代表this是一個(gè)指向常量的指針。這樣使用const的成員函數(shù)稱為常量成員函數(shù)*
可以將其想象成
//偽代碼 說明其是如何使用的
//非法的,不能顯示定義this
std::string Sales_data::isbn(const Sales_data * const this)
{return this->isbn;}
常量對(duì)象以及常量對(duì)象的引用或指針執(zhí)泥調(diào)用常量對(duì)象函數(shù)
c++ Primer第五版 第234頁
類的作者常常定義一些輔助函數(shù),雖然操作從概念上屬于類的接口的組成部分,當(dāng)并不將其定義為類的成員函數(shù)
一般來說,如果非成員函數(shù)是類接口的組成部分,這些類的聲明應(yīng)該與類放在一個(gè)頭文件里面
istream &read(istream &is,Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os<< item.isbn()....;
return os;
}
注意
- 由于IO類不能被拷貝,所以只能通過引用來傳遞他們,
- print不負(fù)責(zé)換行,盡量減少對(duì)格式的控制,可以確保由用戶代碼來決定是否換行
c++ Primer 第五版 第235頁 構(gòu)造函數(shù)
構(gòu)造函數(shù)的名字和類名相同,和其他函數(shù)不同,構(gòu)造函數(shù)沒有返回類型
構(gòu)造函數(shù)不能被聲明為const的
編譯器創(chuàng)造的構(gòu)造函數(shù)被稱為合成的默認(rèn)構(gòu)造函數(shù)
對(duì)大多數(shù)類來說,它的操作是
- 存在類內(nèi)初始值的,用于初始化成員
- 否則,默認(rèn)初始化
只有類沒有聲明任何構(gòu)造函數(shù)時(shí),編譯器才會(huì)自動(dòng)生成默認(rèn)構(gòu)造函數(shù)
如果類包含有內(nèi)置類型或者復(fù)合類型的成員,只有這些成員都被賦予了類內(nèi)初始值時(shí),這個(gè)類才適合合成的默認(rèn)構(gòu)造函數(shù)
struct Sales_data{
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s){}
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s),units_sold(n), revenue(n*p){}
Sales_data(std::istream &);
//其他已有的
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data() = default;在C++11標(biāo)準(zhǔn)中,如果我們需要默認(rèn)行為,可以用=default來要求編譯器生成構(gòu)造函數(shù)
對(duì)于第二個(gè)和第三個(gè)構(gòu)造函數(shù),在冒號(hào)和花括號(hào)之間新出現(xiàn)的部分,,稱為構(gòu)造函數(shù)初始化列表
當(dāng)某個(gè)成員變量被初始化列表忽略時(shí),他將以與合成默認(rèn)構(gòu)造函數(shù)相同的方式隱式初始化
c++Primer 第五版 第241頁
類可以允許其他類或者函數(shù)訪問它的非公有成員,方法是將其聲明為類的友元(friend)
友元聲明只能出現(xiàn)在類的內(nèi)部,位置不限。不過一般來說,最好在類定義開始或者結(jié)束前的位置集中聲明友元。
c++Primer 第五版 第242頁 封裝的益處
封裝有兩個(gè)重要的優(yōu)點(diǎn):
- 確保用戶代碼不會(huì)無意間破壞封裝對(duì)象的狀態(tài)
- 被封裝的類的具體實(shí)現(xiàn)細(xì)節(jié)可以隨時(shí)改變,而無需調(diào)整用戶級(jí)別代碼
C++ Primer第五版 第242頁 友元的聲明
友元的聲明僅僅指定了訪問的權(quán)限,而非一個(gè)通常意義上的函數(shù)聲明,因此在友元聲明之外需要再專門對(duì)函數(shù)進(jìn)行一次聲明
由于我們前面提到友元聲明通常放在類同一個(gè)頭文件中,因此我們的Sales_data頭文件應(yīng)該為友元函數(shù)在類外部提供聲明(除了類內(nèi)部以外)
c++ Primer 第五版 第245頁 類內(nèi)聯(lián)函數(shù)
定義在類內(nèi)部的成員函數(shù)是自動(dòng)inline的
我們可以在類的內(nèi)部將inline作為聲明的一部分顯式支出,也可以在類的外部用inline修飾其定義
雖然我們無需在聲明和定義同時(shí)說明inline,但這樣做是合法的。一般最好在類外部定義的地方說明inline,這使得類更容易理解
當(dāng)然inline成員函數(shù)應(yīng)該與相應(yīng)的類定義在同一個(gè)頭文件中
c++Primer 第五版第245頁 可變數(shù)據(jù)成員
有時(shí)(但并不頻繁)會(huì)有一種情況,我們希望修改類的某個(gè)數(shù)據(jù)成員,即使在const成員函數(shù)中,這時(shí)可以通過mutable關(guān)鍵字來實(shí)現(xiàn)
class Screen{
private:
mutable size_t access_ctr;
public:
void some_member() const;
};
void Screen::some_member() const{
++access_ctr;
}//const成員函數(shù)仍然可以修改mutablemember
c++Primer 第五版 第247頁 從const成員函數(shù)返回*this
一個(gè)const成員函數(shù)如果以引用的形式返回*this,那他的返回類型將是常量引用
如果display是一個(gè)const成員,返回類型為const Screen&,這樣其返回值無法被修改
Screen myScreen;
myScreen.display(cout).set('*');
//如果display返回常量引用,set將引發(fā)錯(cuò)誤
通過區(qū)分成員函數(shù)是否是const的,我們可以對(duì)其進(jìn)行重載。由于非常量版本的函數(shù)對(duì)常量對(duì)象不可用,所以我們只能在常量對(duì)象上調(diào)用const成員對(duì)象
在下面這個(gè)例子中,定義一個(gè)do_display進(jìn)行實(shí)際工作,
class Screen{
public:
Screen &display(std::ostream &os)
{do_display(os);return *this;}
const Screen &display(std::ostream &os) const
{do_display(os); return *this;}
private:
void do_display(std::ostream &os) const{os<<contents;}
};
和我們之前學(xué)的一樣,一個(gè)成員調(diào)用另一個(gè)成員時(shí),this被隱式的傳遞
小建議:
為什么要費(fèi)力定義一個(gè)單獨(dú)的do_display函數(shù)呢?
- 一個(gè)基本的原因是避免多處使用同一代碼
- 隨著類的規(guī)模發(fā)展,display可能更復(fù)雜,相應(yīng)操作只寫一處作用較明顯
- 開發(fā)過程中可能給do_display加調(diào)試信息,最終版本被去掉,因此只定義一處更容易
- 額外的調(diào)用不會(huì)增加任何開銷,因?yàn)樗陬悆?nèi)定義被隱式的聲明為內(nèi)聯(lián)函數(shù)
實(shí)踐中,設(shè)計(jì)良好的c++代碼常常包含大量類似于do_display的小函數(shù),完成一組其他函數(shù)的“實(shí)際”工作。
c++Primer第五版第250頁 類的聲明
類似于函數(shù),類也可以先聲明,而暫時(shí)不定義
class Screen;//Screen類的聲明
對(duì)于一個(gè)類來說,我們創(chuàng)建他的對(duì)象之前該類必須被定義,而不是僅僅被聲明,但也有一種例外情況,當(dāng)一個(gè)類的名字出現(xiàn)后,他就被認(rèn)為被聲明過(不是定義),因此類允許包含指向他自身類型的引用或指針:
class Link_screen{
Screen window;
Link_screen *next;
Link_screen *prev;
};
c++Primer 第五版 第250頁 友元
類可以把其他類定義為友元,也可以把其他類(之前定義過的)的成員函數(shù)定義成友元
如果一個(gè)類指定為友元類,那友元類的成員函數(shù)可以訪問此類包括非公有成員在內(nèi)的所有成員
class Screen{friend class Window_mgr;};//友元類
class Screen{friend void Window_mgr::clear(ScreenIndex);};//友元 類成員函數(shù)
注意友元關(guān)系不存在傳遞性
盡管重載函數(shù)名字相同,但仍然是不同的函數(shù),因此一組重載函數(shù)的友元聲明必須為每一個(gè)分別聲明
c++ Primer 第五版 第253頁 類的作用域
void Window_mgr::clear(ScreenIndex i){}
由于編譯器在處理參數(shù)列表之前已明確了我們處于Window_mgr作用域中,所以不必專門說明ScreenIndex是Window_mgr類定義的
另一方面,函數(shù)返回類型通常在函數(shù)名之前。因此當(dāng)類成員函數(shù)定義在類外部時(shí),返回類型中使用的名字都位于類的作用域之外,這時(shí)需要指定他屬于哪個(gè)類
class Window_mgr{
public:
ScreenIndex addScreen(const Screen&);};
//首先處理返回類型
Window_mgr::ScreenIndex
Window_mgr::addScreen(const Screen &s){}
c++Primer 第五版 第254頁 名字查找和類的作用域
一般來說內(nèi)層作用域可以重新定義外層作用域中的名字,即使該名字在內(nèi)層作用域中使用過
然而,在類中,如果成員使用了外層作用域中的某個(gè)名字,而這個(gè)名字代表一種類型,則類不能在之后重新定義該名字。
c++Primer 第五版第259頁 構(gòu)造函數(shù)初始化
構(gòu)造函數(shù)初始化列表只說明用于初始化成員的值,但并不規(guī)定他們具體執(zhí)行順序
成員的初始化順序和他們在類定義中出現(xiàn)的順序一致
最好令構(gòu)造函數(shù)初始值的順序與成員聲明的順序一致,如果可能的話,盡量避免使用某些成員初始化其他成員
c++Primer第五版 第261頁委托構(gòu)造函數(shù)
c++11允許委托構(gòu)造函數(shù)使用所屬類的其他構(gòu)造函數(shù)協(xié)助其進(jìn)行構(gòu)造
class Sales_data{
public:
Sales_data(std::string s, unsigned cnt):bookNo(s),units_sold(cnt){}
Sales_data():Sales_data("",0){}
Sales_data(std::istream &is): Sales_data()
{read(is,*this);}
};
c++ Primer 第五版第263頁 隱式類類型轉(zhuǎn)換
如果類的構(gòu)造函數(shù)只接受一個(gè)實(shí)參,實(shí)際上定義了轉(zhuǎn)換為該類型的隱式轉(zhuǎn)換機(jī)制,有時(shí)我們稱之為轉(zhuǎn)換構(gòu)造函數(shù)
而由于編譯器只能自動(dòng)地執(zhí)行一步類型轉(zhuǎn)換,因此
string null_book = "9999";
item.combine(null_book);//會(huì)臨時(shí)構(gòu)造Sales_data轉(zhuǎn)換null_book
//下面的代碼隱式使用了兩種轉(zhuǎn)換,因此報(bào)錯(cuò)
item.combine("9999");
//把"9999"轉(zhuǎn)換為string,再將string轉(zhuǎn)為Sales_data
//如果想完成上述代碼 可以
item.combine(string("9999"));//正確 顯式string,隱式Salesdata
item.combine(Sales_data("9999"));//正確 隱式string,顯式Salesdata
而在要求隱式轉(zhuǎn)換的上下文中,我們可以通過explicit關(guān)鍵字進(jìn)行阻止
class Sales_data{
explicit Sales_dadta(const std::string &s):bookNo(s){}
explicit Sales_data(std::istream&);
};
item.combine(null_book);//報(bào)錯(cuò)
item.combine(cin);//報(bào)錯(cuò)
關(guān)鍵字explicit支隊(duì)一個(gè)實(shí)參的構(gòu)造函數(shù)有效
盡管編譯器不會(huì)將explicit構(gòu)造函數(shù)用于隱式轉(zhuǎn)換,但我們可以用于顯式強(qiáng)制轉(zhuǎn)換
item.combine(Sales_data(null_book));//正確
item.combine(static_cast<Sales_data>(cin));//正確
C++ Primer 第五版 第267頁 字面值常量類
某些類可以是字面值常量類,它可能含有constexpr函數(shù)成員,這樣的成員必須符合constexpr函數(shù)的所有要求,它們都是隱式const的
數(shù)據(jù)成員都是字面值類型的聚合類是一個(gè)字面值常量類
符合下列要求的費(fèi)聚合類也是字面值常量類:
- 數(shù)據(jù)成員都是字面值
- 至少含有一個(gè)constexpr構(gòu)造函數(shù)
- 如果一個(gè)數(shù)據(jù)成員有類內(nèi)初始值,則內(nèi)置類型成員的初始值必須是常量表達(dá)式;或者成員屬于某種類類型,初始值必須使用成員自己的constexpr構(gòu)造函數(shù)
- 類必須使用析構(gòu)函數(shù)的默認(rèn)定義,該成員負(fù)責(zé)銷毀類的對(duì)象
盡管構(gòu)造函數(shù)不能是const的,但字面值常量類的構(gòu)造函數(shù)可以是constexpr的
constexpr構(gòu)造函數(shù)可以聲明成=default,或者刪除函數(shù)的形式。構(gòu)造函數(shù)必須既符合構(gòu)造函數(shù)的要求(不能包含返回語句),又必須符合constexpr的要求(它能擁有的唯一可執(zhí)行語句就是返回語句),這兩點(diǎn)可知其函數(shù)體一般來說是空的
class Debug{
public:
constexpr Debug(bool b=true):hw(b),io(b),other(b){}
/**/
private:
bool hw,io,other;
};
constexpr構(gòu)造函數(shù)補(bǔ)習(xí)初始化所有數(shù)據(jù)成員,初始值或使用constexpr構(gòu)造函數(shù)或者一挑常量表達(dá)式
c++Primer 第五版 第269頁 類的靜態(tài)成員
static關(guān)鍵字使其成為類的靜態(tài)成員,靜態(tài)成員可以是public或者private的
類似的靜態(tài)成員函數(shù)不與任何對(duì)象綁定在一起,不包含this指針,不能聲明為const,且函數(shù)體內(nèi)不能使用this
和類的所有成員一樣,當(dāng)我們只想類外部的靜態(tài)成員時(shí),必須指明所屬的類。static關(guān)鍵字只出現(xiàn)在類內(nèi)部的聲明語句中。
靜態(tài)數(shù)據(jù)成員不由類的構(gòu)造函數(shù)初始化。而且一般來說,我們不在類內(nèi)部初始化靜態(tài)成員。相反的,必須在類的外部定義和初始化每個(gè)靜態(tài)成員。和其他對(duì)象一樣,一個(gè)靜態(tài)數(shù)據(jù)成員只能定義一次
類似于全局變量,一單定義,靜態(tài)數(shù)據(jù)成員就一直存在于程序的整個(gè)聲明周期中。
double Account::interestRate = initRate();
//定義并初始化一個(gè)靜態(tài)成員
通常情況下類的靜態(tài)成員不應(yīng)該在類內(nèi)初始化,但是我們可以為靜態(tài)成員提供const整數(shù)類型的類內(nèi)初始值,不過要求靜態(tài)成員必須是字面值常量類型的constexpr
class Account{
public:
static double rate(){return interestRate;}
static void rate(double);
private:
static constexpr int period = 30;//
};
c++Primer 第五版 第271頁
靜態(tài)成員獨(dú)立于任何對(duì)象。因此在某些非靜態(tài)成員非法的場合,靜態(tài)成員卻可以正常使用。例如,靜態(tài)數(shù)據(jù)成員可以是不完全類型(249頁),特別地,靜態(tài)數(shù)據(jù)成員的可以就是他所屬的類類型。而非靜態(tài)成員受到限制,只能聲明它所屬的類的指針或引用:
class Bar{
public:
//
private:
static Bar mem1;//正確,靜態(tài)成員可以 是不完全類型
Bar *mem2;//正確 指針成員可以是不完全類型
Bar mem3;// 錯(cuò)誤,數(shù)據(jù)成員必須是完全類型
};
//另外一個(gè)區(qū)別是我們可以使用靜態(tài)成員作為**默認(rèn)實(shí)參**
class Screen{
public:
Screen& clear(char=bkground);
private:
static const char bkground;
};
非靜態(tài)數(shù)據(jù)成員不能作為默認(rèn)實(shí)參,因?yàn)樗闹当旧韺儆趯?duì)象的一部分,這樣做無法真正提供一個(gè)對(duì)象以便從中獲取成員的值,從而引發(fā)錯(cuò)誤