C++ Primer筆記(七)
第七章 類
類的基本思想是封裝和抽象,抽象是一種依賴於介面和實現分離的技術
定義ADT
- 成員函式必須在類的內部宣告,定義可以在類的內部或者外部,且定義應當與宣告完全一致,而作為介面組成部分的非成員函式,定義在宣告都在類外
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 &print(std::istream&,const Sales_data&);
定義成員函式
isbn()
方法是如何確定bookNo
成員依賴的物件呢?這個成員函式通過一個this
的額外的隱式引數呼叫訪問函式的物件,用請求該函式的物件的地址來初始化this
Sales_data::isbn(&total)
- 而在函式體內部,可以直接訪問類的成員而不必
this->bookNo
因為對成員的直接訪問都被看做this
的隱式呼叫。this是一個常量指標 isbn()
引數列表之後緊跟const,這裡是修改了this
的型別,因為它是預設指向類型別非常量的常量指標,也就是一個頂層const,無法將其繫結到一個常量物件上,所以要緊跟const以賦與this
底層const保證能將其繫結到一個常量物件上。這種在形參列表後宣告const的是常量成員函式(可以讀取類的成員變數,但不能修改),加了const之後,const類物件才能呼叫這個成員函式Sales_data& Sales_data::combine(const Sales_data &rhs)
定義了一個返回this物件的函式,因為返回的是引用型別,所以返回this指標的解引用。
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold+=rhs.units_sold;
revenue+=rhs.revenue;
return *this;
}
- 在定義非類成員的介面函式時,如果想改變類物件的值,就不要常量引用
建構函式
建構函式的任務是初始化類物件的資料成員,只要類的物件被建立就會執行建構函式,建構函式沒有返回型別,名字與類名相同,可以過載,但是不能被宣告為const的 。
- 預設初始化:類通過一個預設建構函式來控制預設初始化程序, 無需任何實參,如果沒有顯式地定義建構函式,那麼編譯器就為我們定義一個合成的預設建構函式,該函式的行為是:如果存在類內初始值,用它來初始化成員,否則預設初始化(函式外的內建型別變數為0,函式內未定義) 我們之所以需要一個類的建構函式,原因有三 一是編譯器只有發現類不包含任何建構函式的情況下才會替我們生成一個預設建構函式,如果在某種情況下控制了初始化,很有可能在所有情況下都需要控制,需要考慮多個Case 二是預設的建構函式在操作內建型別變數有可能置為未定義,容易出錯 三是如果類裡包含了其他類型別的成員且包含的這個類沒有預設建構函式,那麼就沒有可用的預設建構函式。
- 建構函式的形式
Sales_data()=default;
預設建構函式,不接受任何實參,C++11允許我們在後面加上=default來顯式宣告一個預設建構函式;(前提是你在類中對成員預設初始化了)- 如果沒有對成員預設初始化,應當使用建構函式初始值列表如:
Sales_data(const std::string &s, unsigned n):bookNo(s),units_sold(n){}
如果一個數據成員被建構函式初始值列表忽略,它將以合成預設建構函式的形式初始化! - 在類的外部定義建構函式,要指明該建構函式是哪個類的成員,但是沒有建構函式初始值列表,所以未出現的成員將被預設初始化/類內初始值初始化
Sales_data::Sales_data(istream& is)
{
if(if)
read(if,*this);
}
- 拷貝賦值與析構,當初始化變數或以值傳遞方式返回變數,會發生賦值操作;而一個區域性物件在建立它的塊結束時被銷燬,執行析構操作,對於某些類來說,合成的拷貝和解構函式無法工作,特別是類需要分配類物件之外的資源時,如管理動態記憶體。 如果類包含了vector,其拷貝賦值銷燬的合成版本可以正常工作(管理動態記憶體的類應當使用vector或 string管理複雜的儲存空間)
訪問控制與封裝
- class和struct類的定義區別是在第一個訪問說明符之前,class預設為private,struct預設為private
- 定義在
public
說明符之後的成員在整個程式內可被訪問,public成員定義類的介面, - 定義在
private
說明符之後的成員可以被類的成員函式訪問,但不能被使用該類的程式碼訪問 一般將建構函式和部分成員函式緊跟在public之後,而資料成員和實現部分的函式在private說明符之後。private部分封裝了類的實現細節 封裝可以:
- 讓使用者只能通過程式規定的方法來訪問資料,確保使用者程式碼不會無意間破壞封裝物件的狀態
- 便於故障定位,如果物件狀態改變,則只有實現部分的程式碼能產生這樣的錯誤
- 類自身的安全性提升,只能被訪問不能被修改;
- 類的細節可以隨時改變,不需要修改使用者級別的程式碼;
- 宣告在類外的非成員函式,不能訪問
private
,但是可以通過友元的方法訪問,友元宣告只能出現在類定義的內部,但是位置不受約束,友元不是類的成員也不受它所在區域訪問控制級別的約束。友元的宣告僅僅指定了訪問的許可權而並非一個傳統意義上的函式宣告,如果類的物件想呼叫一個友元函式,就必須在友元函式之外再專門對函式進行一次宣告。
friend Sales_data add(const Sales_data&, const Sales_data);
類的其他特性
- 可以在public部分定義類型別名,以將變數的型別隱藏,但是該別名必須先定義後使用
- 一些規模較小的函式,定義在類內部的函式自動內聯;也可以在類的內部顯式宣告行內函數,也可以在類的外部用
inline
修飾函式的定義。
class Screen
{
public:
inline char get(pos r, pos c) const { pos row=r*width;return contents[row+c]};
}
inline Screen& Screen::move(pos r, pos c)
{
pos row=r*width;
cursor = row+c;
return *this;//返回Screen成員的引用
}
- 成員函式也可以被過載,匹配過程與非成員函式的過程很相似
- 可變資料成員,有時希望在一個const成員函式中修改類的資料成員,可以在變數宣告中加mutable,它永遠不會是const
class Screen
{
public:
void some_member() const;
private:
mutable size_t access_ctr;//在一個const成員函式中也可以被修改
};
void Screen::some_member() const
{
++access_ctr;
}
- 類成員的初始值:對vector執行了列表初始化
class WindowsMgr
{
private:
vector<Screen> screen{Screen(24,80,'')};
}
返回*this的成員函式
- 返回this的成員函式
Screen& set(char c){contents[cursor]=c;return *this}
返回值是呼叫set的物件的引用,返回的是左值,也就是物件本身,也就是可以連續使用myScreen.move(4,0).set('#')
,而如果返回的不是物件的引用,那麼函式的返回值是*this的副本,連續操作時,後續方法操作的是一個臨時變數! - 從const成員返回*this,如果一個函式被設定為了const成員函式,
Screen& display() const {return *this}
那麼它返回的是一個常量物件的引用,不能連續操作了 - 基於const的過載,定義兩個版本分別操作常量和非常量物件
class Screen
{
public:
Screen& display(ostream &os)
{do_display(os);return *this;};
const Screen& display(ostream &os)const
{do_display(os);return *this};
private:
void do_display(ostream &os) const {os<<contents;}
}
實現部分放在do_display()
中,而display
的兩個版本分別操作常量和非常量,而且返回型別也不同,const物件呼叫display
返回一個const物件的引用,非常量物件則返回一個非常量物件的引用,意義在於:試想如果只有常量成員函式,則只能返回常量的引用,無法連續操作,如果只有非常量成員函式,那麼常量物件就無法呼叫該函式。
類型別
- 每個類定義了唯一的型別,即使成員列表完全一樣,也是不同的型別
- 可以僅僅宣告類而暫時不定義它,
class Screen;
這種宣告叫做前向宣告,向程式中引入了類的名字,但這個類是一個不完全型別 ,只可以定義指向這種型別的指標或引用,或者宣告以不完全型別作為引數或者返回型別的函式 - 類被定義之後,編譯器才知道儲存需要多少空間,才能宣告資料成員,因此類不能以自身作為資料成員,但可以將其他已定義的類作為資料成員。此外,因為類在定義時一定已經宣告過了,所以允許包含指向它自身型別的引用或者指標。
友元再探
- 之前介紹了非成員函式作為友元的例子,其實也可以將其他類或者其他類的成員函式定義成友元,從而使得友元可以訪問自己的private成員;需要注意的是友元關係不具有傳遞性 需要注意的是,如果成員函式被宣告為友元,那麼成員函式所屬的類必須在之前被宣告。
- 如果一個類想把一組過載函式宣告為它的友元,則必須對每個函式都進行宣告
- 友元宣告只控制訪問許可權,並不是傳統的函式宣告,所以如果要用到友元函式,需要在類的外部提供相應的宣告。
類的作用域
類的作用域之外,普通的資料和函式成員只能由物件,引用或指標訪問,類型別成員使用作用域運算子訪問。 一旦遇到了類名,定義的剩餘部分就在類的作用域之內了。另外,成員函式定義在類的外部時,返回型別需要指定它到底是哪個類的成員
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s)
名字查詢
- 名字查詢的過程為:首先在名字所在的塊中尋找宣告語句,然後在外部作用域尋找,最終未找到匹配的宣告才報錯
- 而定義在類內部的成員, 首先編譯成員的宣告,類成員全部可見後編譯函式體
- 如果在類中使用了外部作用域當中的某個名字,而該名字代表一種型別,則類不能在之後定義該名字。
typedef double Money;
class Account
{
public:
Money balance() {return bal;};
private:
typedef double Money;//不能重複定義,是錯誤的
}
成員函式中名字查詢規則如下:首先在成員函式中尋找宣告,再在類內繼續查詢,再在成員函式定義之前的作用域內繼續查詢(包括類定義之前的全域性作用域)
建構函式
- 如果沒有在建構函式的初始值列表中顯式初始化成員,則成員將在建構函式體之前執行預設初始化,但有時成員的初始化必不可少,如成員是const或者引用,或者是某種類型別而且沒有預設的建構函式
- 成員初始化的順序與建構函式初始值列表順序無關,只與在類定義中出現的順序一致,因此下面的是錯誤的,它嘗試用未定義的j來初始化i。
class X
{
int i;
int j;
public:
X(int val):j(val),i(j){}
}
3.預設實參與建構函式,如果一個建構函式為所有引數都提供了預設實參,實際上也定義了預設建構函式.
委託建構函式
一個委託建構函式使用它所屬類的其他建構函式執行初始化過程,
class Sales_data
{
public:
Sales_data(string s,unsigned cnt,double price):
bookNo(s), unit_sold(cnt), revenue(cnt*price) {}
//以下的三個建構函式都委託給三引數版本的建構函式。
Sales_data():Sales_data("",0,0){}
Sales_data(string s):Sales_data(s,0,0){}
Sales_data(istream &is):Sales_data(){read(is, *this);}
}
一個建構函式委託給另一個建構函式,受委託的建構函式的初始值列表和函式體被依次執行,也就是先執行被委託函式的函式體,然後才是委託者的函式體。
預設建構函式作用
預設建構函式在以下情況執行
- 塊作用域內不使用任何一個初始值定義非靜態變數時
- 含有類型別的成員且使用預設的建構函式
- 類型別的成員沒有在建構函式初始值列表中顯式初始化
- 陣列初始化提供的值小於陣列長度
- 顯式的請求初始化時
vector<int> T(n,val)
如果提供了其他的建構函式,那麼應當同時定義一個預設建構函式
隱式的類型別轉換
在類中接受某種型別作為引數的建構函式定義了這兩種型別向類型別轉換的規則,因此可以用這種型別來代替需要使用類型別的地方
- 只允許一步型別轉換
item.combine(string("123"));
而不允許item.combine("123");
- 可以在建構函式前加explicit來禁止隱式型別轉換,只對一個實參的建構函式有效,因為多個實參的建構函式不能被用來執行隱式轉換;而且只能在類內宣告時加,類外定義時不能重複。
- 聲明瞭explicit之後,可以通過強制型別轉換來實現相似的功能
聚合類
聚合類使得使用者直接訪問其成員,並具有特殊的初始化形式,滿足如下條件:
- 所有成員public屬性
- 沒有定義建構函式
- 沒有類內初始值
- 沒有基類、沒有
virtual
函式 然後提供一個花括號的成員初始值列表來初始化聚合類的資料成員,初始值順序與宣告順序一致 這樣初始化,將正確初始化每個成員的任務交給使用者,容易出錯,並且新增或者刪除一個成員之後,所有的初始化語句都要更新(很麻煩)
字面值常量類
- 資料成員都是字面值型別的聚合類是字面值常量類
- 或者:資料成員都是字面值型別,類內至少有一個constexpr函式,內建型別成員類內初始值必須是常量表達式,用解構函式的預設定義
類的靜態成員
類的靜態成員可以是public
或者private
的
class Account
{
public:
void calculate(){amount+=amount*interestRate;}
static double rate(){return interestRate;}
static void rate(double);
private:
string owner;
double amount;
static double interestRate;
static double initRate();
}
-
類的靜態成員存在於物件之外,只存在一個靜態變數並被所有物件所共享,所以不能通過類的建構函式初始化,只能在類的外部定義和初始化(類內宣告,類外定義+初始化)
-
靜態成員函式不包含this指標,所以不能隱式訪問物件的非靜態成員,
-
成員函式可以直接訪問靜態成員
-
在類的外部定義靜態成員函式時,不能重複出現
static
關鍵字 -
靜態成員的類內初始化通常情況下不應當在類的內部初始化靜態成員,但可以為類的靜態成員提供const整數型別的類內初始值,初始值必須是常量表達式。此外,即使一個常量靜態資料成員在類內初始化了,也應當在類的外部定義一下該成員。
-
靜態成員可以是不完全型別,靜態成員也可以作為預設實參
class Bar
{
public:
Screen& clear(char =bkground );
private:
static const char bkground;
static Bar mem1;//合法,靜態成員可以是不完全型別
Bar *mem2;
Bar mem3;//錯誤,資料成員必須是完全型別
}