《C++Primer》第十二章-類-學習筆記(3)

《C++Primer》第十二章-類-學習筆記(3)

日誌:
1,2020-03-03 筆者提交文章的初版V1.0

作者按:
最近在學習C++ primer,初步打算把所學的記錄下來。

傳送門/推廣
《C++Primer》第二章-變量和基本類型-學習筆記(1)
《C++Primer》第三章-標準庫類型-學習筆記(1)
《C++Primer》第八章-標準 IO 庫-學習筆記(1)
上一篇
《C++Primer》第十二章-類-學習筆記(2)

友元

某些情況下,允許特定的非成員函數訪問一個類的私有成員,同時仍然阻止一般的訪問,這是很方便做到的。例如,被重載的操作符,如輸入或輸出操作符,經常需要訪問類的私有數據成員。這些操作符不可能爲類的成員,具體原因之後再講述。然而,儘管不是類的成員,它們仍是類的“接口的組成部分”。
友元機制允許一個類將對其非公有成員的訪問權授予指定的函數或類。友元的聲明以關鍵字 friend開始。它只能出現在類定義的內部。友元聲明可以出現在類中的任何地方:友元不是授予友元關係的那個類的成員,所以它們不受聲明出現部分的訪問控制影響。

通常,將友元聲明成組地放在類定義的開始或結尾是個好主意。

友元關係的例子

想像一下,除了 Screen 類之外,還有一個窗口管理器,管理給定顯示器上的一組 Screen。窗口管理類在邏輯上可能需要訪問由其管理的 Screen 對象的內部數據。假定 Window_Mgr 是該窗口管理類的名字,Screen 應該允許Window_Mgr 像下面這樣訪問其成員:

class Screen {
// Window_Mgr members can access private parts of class Screen
friend class Window_Mgr;  //友元類
// ...rest of the Screen class
};

Window_Mgr 的成員可以直接引用 Screen 的私有成員。例如,Window_Mgr可以有一個函數來重定位一個 Screen:

Window_Mgr& Window_Mgr::relocate(Screen::index r, Screen::index c,Screen& s)
{
// ok to refer to height and width
	s.height += r;   //height與width是Screen的私有成員,缺少friend class Window_Mgr友元聲明時,這段代碼將會出錯
	s.width += c;
	return *this;
}

缺少友元聲明時,這段代碼將會出錯:將不允許使用形參 s 的 height 和width 成員。因爲 Screen 將友元關係授予 Window_Mgr,所以,Window_Mgr 中的函數都可以訪問 Screen 的所有成員。
友元可以是普通的非成員函數,或前面定義的其他類的成員函數,或整個類。
將一個類設爲友元,友元類的所有成員函數都可以訪問授予友元關係的那個類的非公有成員。

使其他類的成員函數成爲友元

如果不是將整個 Window_Mgr 類設爲友元,Screen 就可以指定只允許relocate 成員訪問:

class Screen {
// Window_Mgrmust be defined before class Screen
	friend Window_Mgr& Window_Mgr::relocate(Window_Mgr::index,Window_Mgr::index,Screen&);
// ...restofthe Screen class
};

當我們將成員函數聲明爲友元時,函數名必須用該函數所屬的類名字加以限定。

友元聲明與作用域

爲了正確地構造類,需要注意友元聲明與友元定義之間的互相依賴。在前面的例子中,類 Window_Mgr 必須先定義。否則,Screen 類就不能將一個Window_Mgr 函數指定爲友元。然而,只有在定義類 Screen 之後,才能定義relocate 函數——畢竟,它被設爲友元是爲了訪問類 Screen 的成員。
更一般地講,必須先定義包含成員函數的類,才能將成員函數設爲友元。另一方面,不必預先聲明類和非成員函數來將它們設爲友元。
友元聲明將已命名的類或非成員函數引入到外圍作用域中。此外,友元函數可以在類的內部定義,該函數的作用域擴展到包圍該類定義的作用域。
用友元引入的類名和函數(定義或聲明),可以像預先聲明的一樣使用:

//不必預先聲明`類和非成員函數`來將它們設爲友元
//友元引入的類名和函數(定義或聲明),可以像預先聲明的一樣使用
class X {
friend class Y; //類
friend void f() { /* ok to define friend function in the classbody */ } //非成員函數
};
class Z {
Y *ymem; // ok: declaration for class Y introduced by friend in X
void g() { return ::f(); } // ok: declaration of f introduced by X
};

重載函數與友元關係

類必須將重載函數集中每一個希望設爲友元的函數都聲明爲友元:

// overloaded storeOn functions
extern std::ostream& storeOn(std::ostream &, Screen &);
extern BitMap& storeOn(BitMap &, Screen &);
class Screen {
// ostream version of storeOn may access private parts of Screen objects
friend std::ostream& storeOn(std::ostream &, Screen &);
// ...
};

類 Screen 將接受一個 ostream& 的 storeOn 版本設爲自己的友元。接受一個 BitMap& 的版本對 Screen 沒有特殊訪問權。

static 類成員

對於特定類類型的全體對象而言,訪問一個全局對象有時是必要的。也許,在程序的任意點需要統計已創建的特定類類型對象的數量;或者,全局對象可能是指向類的錯誤處理例程的一個指針;或者,它是指向類類型對象的內在自由存儲區的一個指針。
然而,全局對象會破壞封裝:對象需要支持特定類抽象的實現。如果對象是全局的,一般的用戶代碼就可以修改這個值。
因此我們採用定義類靜態成員:類可以定義類靜態成員,而不是定義一個可普遍訪問的全局對象。
通常,非 static 數據成員存在於類類型的每個對象中。不像普通的數據成員,static 數據成員獨立於該類的任意對象而存在;每個 static 數據成員是與類關聯的對象,並不與該類的對象相關聯
正如類可以定義共享的 static 數據成員一樣,類也可以定義 static 成員函數static 成員函數沒有 this 形參,它可以直接訪問所屬類的 static 成員,但不能直接使用非 static 成員

使用類的 static 成員的優點

使用 static 成員而不是全局對象有三個優點。

  1. static 成員的名字是在類的作用域中,因此可以避免與其他類的成員或全局對象名字衝突。
  2. 可以實施封裝。static 成員可以是私有成員,而全局對象不可以。
  3. 通過閱讀程序容易看出 static 成員是與特定類關聯的。這種可見性可清晰地顯示程序員的意圖。

定義 static 成員

在成員聲明前加上關鍵字 static 將成員設爲staticstatic 成員遵循正常的公有/私有訪問規則。
例如,考慮一個簡單的表示銀行賬戶的類。每個賬戶具有餘額和擁有者,並且按月獲得利息,但應用於每個賬戶的利率總是相同的。可以按下面的這樣編寫這個類

class Account {
public:
// interface functions here
	void applyint() { amount += amount * interestRate; }
	static double rate() { return interestRate; }
	static void rate(double); // sets a new rate
private:
	std::string owner;
	double amount;
	static double interestRate;
	static double initRate();
};

這個類的每個對象具有兩個數據成員:owner 和 amount。對象沒有與static 數據成員對應的數據成員,但是,存在一個單獨的 interestRate 對象,由 Account 類型的全體對象共享。

使用類的 static 成員

可以通過作用域操作符類直接調用 static 成員,或者通過對象、引用或指向該類類型對象的指針間接調用

Account ac1;
Account *ac2 = &ac1;
// equivalent ways to call the static member rate function
double rate;
rate = ac1.rate(); // through an Account object or reference 通過對象調用
rate = ac2->rate(); // through a pointer to an Account object指向該類類型對象的指針間接調用
rate = Account::rate(); // directly from the class using the scope operator作用域操作符`從`類直接調用 static 成員

像使用其他成員一樣,類成員函數可以不用作用域操作符來引用類的static 成員:

class Account {
public:
// interface functions here
void applyint() { amount += amount * interestRate; }
};

static 成員函數

Account 類有兩個名爲 rate 的 static 成員函數,其中一個定義在類的內部。當我們在類的外部定義 static 成員時,無須重複指定 static 保留字,該保留字只出現在類定義體內部的聲明處

void Account::rate(double newRate)
{
interestRate = newRate;
}

static 函數沒有 this 指針

static 成員是類的組成部分但不是任何對象的組成部分,因此,static 成員函數沒有 this 指針。通過使用非 static 成員顯式或隱式地引用 this 是一個編譯時錯誤。
因爲 static 成員不是任何對象的組成部分,所以 static 成員函數不能被聲明爲 const。畢竟,將成員函數聲明爲 const 就是承諾不會修改該函數所屬的對象。最後,static 成員函數也不能被聲明爲虛函數

static 數據成員

static 數據成員可以聲明爲任意類型,可以是常量、引用、數組、類類型,等等。
static 數據成員必須在類定義體的外部定義(正好一次)。不像普通數據成員,static 成員不是通過類構造函數進行初始化,而是應該在定義時進行初始化
保證對象正好定義一次的最好辦法,就是將 static 數據成員的定義放在包含類非內聯成員函數定義的文件中。
定義 static 數據成員的方式與定義其他類成員和變量的方式相同:先指定類型名,接着是成員的完全限定名。
可以定義如下 interestRate:

// define and initialize static class member
double Account::interestRate = initRate();

這個語句定義名爲 interestRate 的 static 對象,它是類 Account 的成員,爲 double 型。像其他成員定義一樣,一旦成員名出現,static 成員的就是在類作用域中。因此,我們可以沒有限定地直接使用名爲 initRate 的 static成員函數,作爲 interestRate 初始化式。注意,儘管 initRate 是私有的,我們仍然可以使用該函數來初始化 interestRate。像任意的其他成員定義一樣,interestRate 的定義是在類的作用域中,因此可以訪問該類的私有成員。
像使用任意的類成員一樣,在類定義體外部引用類的 static成員時,必須指定成員是在哪個類中定義的。然而,static 關鍵字只能用於類定義體內部的聲明中,定義不能標示爲static。

特殊的整型 const static 成員

一般而言,類的 static 成員,像普通數據成員一樣,不能在類的定義體中初始化。相反,static 數據成員通常在定義時才初始化。
這個規則的一個例外是,只要初始化式是一個常量表達式,整型 const static 數據成員就可以在類的定義體中進行初始化:

class Account {
public:
static double rate() { return interestRate; }
static void rate(double); // sets a new rate
private:
static const int period = 30; // interest posted every 30 days
double daily_tbl[period]; // ok: period is constant expression
};

用常量值初始化的整型 const static 數據成員是一個常量表達式。同樣地,它可以用在任何需要常量表達式的地方,例如指定數組成員 daily_tbl 的維。
const static 數據成員在類的定義體中初始化時,該數據成員仍必須在類的定義體之外進行定義。
在類內部提供初始化式時,成員的定義不必再指定初始值:

// definition of static member with no initializer;
// the initial value is specified inside the class definition
const int Account::period;

static 成員不是類對象的組成部分

普通成員都是給定類的每個對象的組成部分。static 成員獨立於任何對象而存在,不是類類型對象的組成部分。因爲 static 數據成員不是任何對象的組成部分,所以它們的使用方式對於非 static 數據成員而言是不合法的。
例如,static 數據成員的類型可以是該成員所屬的類類型。非 static 成員被限定聲明爲其自身類對象的指針或引用

class Bar {
public:
// ...
private:
static Bar mem1; // ok
Bar *mem2; // ok
Bar mem3; // error
};

類似地,static 數據成員可用作默認實參

class Screen {
public:
// bkground refers to the static member
// declared later in the class definition
Screen& clear(char = bkground);
private:
	static const char bkground = '#';
};

非 static 數據成員不能用作默認實參因爲它的值不能獨立於所屬的對象而使用。使用非 static 數據成員作默認實參,將無法提供對象以獲取該成員的值,因而是錯誤的。

總結

類是 C++ 中最基本的特徵,允許定義新的類型以適應應用程序的需要,同時使程序更短且更易於修改。
數據抽象是指定義數據和函數成員的能力,而封裝是指從常規訪問中保護類成員的能力,它們都是類的基礎。
成員函數定義類的接口。通過將類的實現所用到的數據和函數設置爲 private 來封裝類。
類可以定義構造函數,它們是特殊的成員函數,控制如何初始化類的對象。可以重載構造函數
每個構造函數就初始化每個數據成員。初始化列表包含的是名—值對,其中的名是一個成員,而值則是該成員的初始值。
類可以將對其非 public 成員的訪問權授予其他類或函數,並通過將其他的類或函數設爲友元來授予其訪問權。
類也可以定義 mutablestatic 成員mutable 成員永遠都不能爲const;它的值可以在 const 成員函數中修改。static 成員可以是函數或數據獨立於類類型的對象而存在。

參考資料

【1】C++ Primer 中文版(第四版·特別版)

註解

本文許可證

本文遵循 CC BY-NC-SA 4.0(署名 - 非商業性使用 - 相同方式共享) 協議,轉載請註明出處,不得用於商業目的。
CC BY-NC-SA 4.0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章