1.成员函数 VS non-member函数
在类中可能有很多个成员函数,考虑下面的网页浏览器WebBrowser类,该类中有多个成员函数。如:清除下载元素高速缓存区成员函数、清除访问过的URL历史记录的成员函数、移除系统中所有的cookies成员函数。
class WebBrowser { public: // ... void clearCache(); void clearHistory(); void removeCookies(); // ... };
许多用户会想将上面的一系列操作都封装到一个函数中,因此WebBrowser类中也提供了这样的成员函数clearEverything(),如下所示:
class WebBrowser { public: // ... void clearEverything(); // 调用clearCache()、clearHistory()、removeCookies // ... };
当然,也可以将上面的一系列操作都封装到一个non-member函数中,在该函数中调用成员函数也可以。如下所示:
void clearBrowser(WebBrowser& wb){ // 非成员函数 wb.clearCache(); wb.clearHistory(); wb.removeCookies(); }
上面两种方法,究竟哪一个比较好呢?是成员函数clearEverything()还是non-member函数clearBrowser()?根据面向对象原则要求,数据以及操作数据的那些函数应该被绑定在一块,这意味着建议使用成员函数更好。不幸的是,前面的观点是错误的。它是对面向对象规则的错误理解。面向对象原则的正确理解是:数据应该尽可能被封装,成员函数clearEverything()带来的封装性比non-member函数clearBrowser()低。此外,non-member函数可允许对WebBrowser相关机能有较大的包裹弹性,而那最终导致较低的编译依赖度,增加了WebBrowser的可延伸性。
2.为啥non-member函数会胜出?
从面向对象语言的封装性角度来说,如果某些东西被封装,它就不再可见。越多东西被封装,越少的人可以看到它。而越少的人看到它,我们就有越大的弹性可以去改变它,这是因为我们的改变仅仅直接影响能看到改变的那些人和事物。因此,它能使我们能够改变事物而只影响有限客户。
现在考虑对象内部的数据。越少的代码可以“看到”数据,越多的数据就可被封装,而我们也就越能自由地改变对象的内部数据。如何测量“有多少代码可以看到某一块数据”呢?我们计算能够访问该数据的函数数量,以此作为一种粗略的测量。越多的函数可访问它,数据的封装性就越低。
在前面的文章C++类中数据成员要私有中说明了成员变量应该是private的必要性,如果它们不是的话,就会有无限量的函数可以访问它们,它们也就毫无封装性可言。能够访问私有成员变量的函数只有类的成员函数和友元函数而已。如果要你在一个成员函数和一个non-member non-friend函数之间抉择,而且两者提供相同的功能,那么导致较大封装性的是non-member non-friend函数,因为它并不增加“能访问类的内部的private成分”的函数数量。因此,这也说明了为什么non-member函数clearBrowser()能胜出的原因:它导致了WebBrowser类有较大的封装性。
3.值得注意的两点
第一,上面的论述只适用于non-member non-friend函数。友元函数对类中的私有成员的访问权限和成员函数相同,因此两者对封装的冲击力相同。从封装的角度来看,这里的选择关键不在于成员函数与non-member函数之间,而在于成员函数与non-member non-friend函数之间。
第二,只考虑封装性而让函数成为类的non-member函数,并不意味着此函数不可以是另一个类的成员函数。例如,我们可以令clearBrowser()函数成为某个工具类的一个静态成员函数。只要它不是WebBrowser类的一部分,就不会影响WebBrowser私有成员的封装性。
4.C++中的做法
在C++中,比较自然的做法是让clearBrowser()函数成为一个non-member函数并且位于WebBrowser类所在的同一个命名空间(namespace)中。如下所示:
namespace WebBrowserStuff{ class WebBrowser{ // ... }; void clearBrowser(WebBrowser& wb); // non-member函数 }
上面的做法并不是为了看起来自然而已,本质是命名空间与类不同,命名空间可以跨越多个源码文件而类则不可以。因为像clearBrowser()这样的函数是一个“提供便利的函数”,如果它既不是成员函数也不是友元函数,就没有对WebBrowser的特殊访问权限,也不能提供“WebBrowser客户无法以其他方式取得”的机能。例如,如果clearBrowser()函数不存在,客户端就只好自行调用clearCache()、clearHistory()、removeCookies()。
一个像WebBrowser这样的类中可能有大量的便利函数,如书签便利函数、打印便利函数、cookies管理有关的便利函数。通常,大多数客户只对其中某些感兴趣。为了防止多个便利函数之间发生编译相互依赖性,分离它们的最直接方法是将书签便利函数声明在一个头文件中,将cookies管理有关的便利函数声明在另一个头文件中,再将打印便利函数声明于第三个头文件中。如下所示:
// 头文件webbrowser.h,这个头文件针对WebBrowser类 namespace WebBrowserStuff{ class WebBrowser{ // ... }; // ... non-member函数 } // 头文件webbrowserbookmarks.h namespace WebBrowserStuff{ // ... 与书签相关的便利函数 } // 头文件webbrowsercookies.h namespace WebBrowserStuff{ // ... 与cookies管理相关的便利函数 }
注意:上面的形式正式C++中STL的组织方式。STL并不是拥有单一、整体、庞大的<C++ StandardLibrary>头文件并在其中内含std命名空间内的每一样东西,而是有数十个头文件(<vector>、<list>、<algorithm>、<memory>等),每个头文件声明std的某些机能。如果客户只想使用vector相关机能,他不需要#include <memory>。这允许客户只对他们所用的那一小部分系统形成编译依赖。以此种方式切割机能并不适用于类的成员函数,因为一个类必须整体定义,不能被分割成很多片段。
将所有便利函数放在多个头文件内,但隶属于同一个命名空间,意味着客户可以轻松扩展这一组便利函数。他们需要做的是添加更多的non-member non-friend函数到此命名空间中。这样新的函数就像其他旧的便利函数那样可用且整合为一体,这是类所无法提供的另一个性质,因为类定义式对客户而言是不可扩展的。当然,你可能会说客户使用继承就可以派生出新的类啦,但请记住派生类是无法访问基类中被封装的私有成员。于是,如此的扩展机能拥有的只是次级身份。
5.总结
(1) 宁可拿non-member non-friend函数替换成员函数。这样做可以增加封装性、包裹弹性和机能扩展性。