Item 23: Prefer non-member non-friend functions to member functions
在類的是實現中,常常會面臨成員函數和非成員函數的選擇。比如一個浏覽器類:
class WebBrowser{
public:
void clearCache();
void clearCookies();
void clearHistory();
};
此時你要實現一個clearEverything()
有兩種方式:
class WebBrowser{
public:
void clearEverything(){
clearCache();
clearCookies();
clearHistory();
}
}
// 或者使用非成員函數:
void clearEverything(WebBrowser& wb){
wb.clearCache();
wb.clearCookies();
wb.clearHistory();
}
哪種更好呢?面向對象原則指出,數據和數據上的操作應當綁定在一起,那麼前者更好。 這是對面向對象的誤解,面向對象設計的精髓在於封裝,數據應當被盡可能地封裝。 相比於成員函數,非成員函數提供了更好的封裝,包的靈活性(更少的編譯依賴),以及功能擴展性。
封裝就是對外界隱藏的意思。如果數據被越好地封裝,那麼越少的東西可以看到它,我們便有更大的靈活性去改變它。這是封裝帶來的最大的好處:給我們改變一個東西的靈活性,這樣的改變只會影響到有限的客戶。
作為粗粒度的估計,數據的封裝性反比於可訪問該數據的函數數量。這些函數包括成員函數、友元函數和友元類中的函數。 因此非成員非友元函數會比成員函數提供更好的封裝, 我們應該選擇clearEverything()
的第二種實現。
Item22提到,如果一個數據成員不是私有的,那麼將會有無限數量的函數可訪問它。
這裡有兩點值得注意:
在C++中,可以把這些非成員函數定義在相同的命名空間下。 但問題又來了:這些在命名空間下的函數並不在類中,它們會被傳播到所有的源文件中。 而客戶並不希望為了使用幾個工具函數,就對這樣一個龐大的命名空間產生編譯依賴。 因此我們可以將不同類別的工具函數放在不同的頭文件中,客戶可以選擇它想要的那部分功能:
// file: webbrowser.h
namespace WebBrowserStuff{
class WebBrowser{};
}
// file: webbrowser-bookmarks.h
namespace WebBrowserStuff{
...
}
// file: webbrowser-cookies.h
namespace WebBrowserStuff{
...
}
這也是C++標准庫的組織方式,std
命名空間下的所有東西都被分在了不同的頭文件中:
, ,
等。這樣客戶代碼只對它引入的那部分功能產生編譯依賴。 為了做到這一點,這些工具函數必須是非成員函數,因為類作為整體必須在一個文件中進行定義。
同一命名空間不同頭文件的組織方式,也為客戶擴展工具函數提供了可能。 客戶可以在同一命名空間下定義他自己的工具函數, 這些函數便會和既有工具函數天然地集成在一起。 這也是成員函數無法做到的一個特性,因為類的定義對客戶擴展是關閉的。 即使是子類也不能訪問封裝的(私有)成員數據, 況且有些類不是用來做基類的(見Item 7:將多態基類的析構函數聲明為虛函數)。