程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> Effective C++讀書筆記(14)

Effective C++讀書筆記(14)

編輯:C++入門知識

條款23:寧以non-member、non-friend替換member函數

Prefer non-member non-friend functions tomember functions

想象一個用來表示網頁浏覽器浏覽器的類。這樣一個類可能提供的大量函數中,有一些用來清空下載元素高速緩存區、清空訪問過的URLs歷史,以及從系統移除所有cookies的功能:

class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};

很多用戶希望能一起執行全部這些動作,所以WebBrowser可能也會提供一個函數去這樣做:

class WebBrowser {
public:
...
void clearEverything();
// calls clearCache, clearHistory, and removeCookies
...
};

當然,這個功能也能通過非成員函數調用適當的成員函數來提供:

void clearBrowser(WebBrowser& wb)
{wb.clearCache();wb.clearHistory();wb.removeCookies();}

那麼哪個更好呢,成員函數clearEverything還是非成員函數clearBrowser?

面向對象原則指出:數據和對它們進行操作的函數應該被綁定到一起,而且建議成員函數是更好的選擇。不幸的是,這個建議是不正確的。面向對象原則指出數據應該盡可能被封裝,與直覺不符的是,成員函數clearEverything居然會造成比非成員函數clearBrowser更差的封裝性。此外,提供非成員函數允許WebBrowser相關功能的更大的包裝彈性,可以獲得更少的編譯依賴和增加WebBrowser的擴展性。因而,在很多方面非成員函數比成員函數更好。

我們將從封裝開始。封裝為我們提供一種改變事情的彈性,而僅僅影響有限的客戶。結合對象內的數據考慮,越少有代碼可以看到數據(訪問它),數據的封裝性就越強,我們改變對象數據的的自由也就越大,比如,數據成員的數量、類型,等等。如何度量有多少代碼能看到數據呢?我們可以計算能訪問數據的函數數量:越多函數能訪問它,數據的封裝性就越弱。

我們說過數據成員應該是private,否則它們根本就沒有封裝。對於private數據成員,能訪問他們的函數數量就是類的成員函數加上友元函數,因為只有成員和友元函數能訪問 private成員。假設在一個成員函數(能訪問的不只是一個類的private數據,還有 private 函數,枚舉,typedefs等等)和一個提供同樣功能的非成員非友元函數(不能訪問上述那些東西)之間選擇,能獲得更強封裝性是非成員非友元函數。這就解釋了為什麼clearBrowser(非成員非友元函數)比clearEverything(成員函數)更可取:它能為WebBrowser獲得更強的封裝性。

在這一點,有兩件事值得注意。首先,這個論證只適用於非成員非友元函數。友元能像成員函數一樣訪問一個類的private成員,因此同樣影響封裝。從封裝的觀點看,選擇不是在成員和非成員函數之間,而是在成員函數和非成員非友元函數之間。

第二,只因關注封裝而讓函數成為類的非成員並不意味著它不可以是另一個類的成員。這對於習慣了所有函數必須屬於類的語言(例如,Eiffel,Java,C#等等)的程序員是一個適度的安慰。例如,我們可以使clearBrowser成為某工具類的static成員函數,只要它不是WebBrowser的一部分(或友元),它就不會影響WebBrowser的private成員的封裝。

在C++中,比較自然的做法是使clearBrowser成為與 WebBrowser在同一個namespace中的非成員函數:

namespace WebBrowserStuff {

    classWebBrowser { ... };
void clearBrowser(WebBrowser& wb);
...
}

namespace能跨越多個源文件而類不能。這是很重要的,因為類似clearBrowser的函數是提供便利的函數。如果既不是成員也不是友元,他們就沒有對WebBrowser的特殊訪問權力,所以不能提供任何一種WebBrowser客戶無法以其它方法得到的機能。例如,如果clearBrowser不存在,客戶可以直接調用clearCache,clearHistory和 removeCookies本身。

一個類似WebBrowser的類可以有大量的方便性函數,一些是書簽相關的,另一些打印相關的,還有一些是cookie管理相關的,等等。通常多數客戶僅對其中一些感興趣。沒有理由讓一個只對書簽相關便利函數感興趣的客戶在編譯時依賴其它函數。分隔它們直截了當的方法就是將頭文件分開聲明:

// header "webbrowser.h" – 針對WebBrowser自身及其核心機能
namespace WebBrowserStuff {
class WebBrowser { ... };
... // 核心機能,如人人都會用到的non-member函數
}

// header "webbrowserbookmarks.h"
namespace WebBrowserStuff {
... // 書簽相關的便利函數
}

// header "webbrowsercookies.h"
namespace WebBrowserStuff {
... // cookie相關的便利函數
}

...

這正是C++標准程序庫的組織方式。標准程序庫並不是擁有單一整體而龐大的<C++StandardLibrary>頭文件並內含std namespace中的所有東西,它們在許多頭文件中(例如,<vector>,<algorithm>,<memory>等等),每一個都聲明了std中的一些機能。這就允許客戶在編譯時僅僅依賴他們實際使用的那部分系統。當機能來自一個類的成員函數時,用這種方法分割它是不可能的,因為一個類必須作為一個整體來定義,它不能四分五裂。

將所有方便性函數放入多個頭文件中,但隸屬於一個namespace中,意味著客戶能容易地擴充便利函數的集合,要做的只是在namespace中加入更多的非成員非友元函數。例如,如果一個 WebBrowser的客戶決定寫一個關於下載圖像的便利函數,僅僅需要新建一個頭文件,包含那些函數在WebBrowserStuff namespace中的聲明,這個新函數現在就像其它便利函數一樣可用並被集成。這是類不能提供的另一個特性,因為類定義對於客戶是不能擴展。當然,客戶可以派生新類,但是派生類不能訪問基類中被封裝的(private)成員,所以這樣的“擴充機能”只是次等身份。

·    用非成員非友元函數取代成員函數。這樣做可以提高封裝性,包裝彈性,和機能擴充性。

 

條款24:若所有參數皆需類型轉換,請為此采用non-member函數

Declare non-member functions when typeconversions should apply to all parameters

讓一個類支持隱式類型轉換通常是一個不好的主意。當然,這條規則有一些例外,最普通的一種就是在創建數值類型時。例如,如果你設計一個用來表現有理數的類,允許從整數到有理數的隱式轉換看上去並非不合理:

class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
// 非explicit,允許int-to-Rational隱式轉換int-to-Rational
int numerator() const; // 分子和分母的訪問函數
int denominator() const;

    private:
...
};

應該支持算術運算,比如加法,乘法等等,但不能確定是通過成員函數、非成員函數、還是非成員的友元函數來實現它們。當你搖擺不定的時候,你應該堅持面向對象的原則。於是有理數的乘法與Rational類相關,所以在Rational類的內部實現有理數的operator*似乎更加正常,我們先讓operator*成為Rational的一個成員函數:

class Rational {
public:
...
const Rational operator*(const Rational& rhs) const;
};

Rational oneEighth(1, 8);

Rational oneHalf(1, 2);

Rational result = oneHalf * oneEighth; // fine

result = result * oneEighth; // fine

這個設計讓你在有理數相乘時不費吹灰之力,但你還希望支持混合模式的操作,以便讓 Rational能夠和其它類型(如int)相乘。畢竟兩個數相乘很正常,即使它們碰巧是不同類型的數值。

result = oneHalf * 2; // fine

result = 2 * oneHalf; // error!

只有一半行得通,但乘法必須是可交換的。當以對應的函數形式重寫上述兩個式子,問題所在便一目了然:

result = oneHalf.operator*(2);

result = 2.operator*(oneHalf);

對象oneHalf是一個包含 operator* 的類實例,所以編譯器調用那個函數。然而整數2沒有operator*成員函數。編譯器同樣要尋找可被如下調用的非成員operator*(也就是說,在 namespace 或全局范圍內的operator*):

result = operator*(2, oneHalf);

但在本例中,沒有非成員的接受int和Rational的operator*函數,所以搜索失敗。再看那個成功的調用,它的第二個參數是整數2,而Rational::operator*卻持有一個 Rational對象作為它的參數。這裡發生了隱式類型轉換。編譯器知道你傳遞一個int而那個函數需要一個Rational,通過用你提供的int調用Rational的構造函數,它們能做出一個相配的Rational。換句話說,它們將那個調用或多或少看成如下這樣:

const Rational temp(2); // 根據2建立一個臨時Rational對象

result = oneHalf * temp; // 等同於oneHalf.operator*(temp);

當然,編譯器這樣做僅僅是因為提供了一個非explicit構造函數。如果Rational的構造函數是explicit,那兩句語句都將無法編譯,但至少語句的行為保持一致。

 

這兩個語句一個可以編譯而另一個不行的原因在於,當參數列在參數列表中的時候,才有資格進行隱式類型轉換。現在支持混合運算的方法或許很清楚了:讓operator*作為非成員函數,因此就允許將隱式類型轉換應用於所有參數:

class Rational {... // 不包括operator*};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{return Rational(lhs.numerator() * rhs.numerator(),

    lhs.denominator()* rhs.denominator());}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; // fine
result = 2 * oneFourth; // it works!

另外,僅僅因為函數不應該作為成員並不自動意味著它應該作為友元。

·    如果你需要在一個函數的所有參數(包括被 this 指針所指向的那個)上使用類型轉換,這個函數必須是一個非成員。

 



 摘自 pandawuwyj的專欄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved