条款23:宁以non-member、non-friend替换member函数
为什么要用非成员函数、非友元函数替换成员函数呢?其实这是为了保证数据的封装性。而数据的封装性强弱是怎么体现的呢?一种粗糙的量测,我们认为越多的函数能访问它,数据的封装性就越低,因为如果数据发生改变,因它的改变牵扯到需要改变的太多,所以它的封装性较差。
而非成员函数、非友元函数较成员函数而言,访问不到private中的数据,数据的封装性自然就更好一些。也就是说,越多的函数可以访问,数据的封装性就越低。
需要注意的是,只因在意封装性而让函数“成为class的non-member”,并不意味它“不可以是另一个class的member”。该函数可以成为另一个类的static member函数,只要不影响原类中private成员的封装性。
看一下书中的例子:
class WebBrowser { public: void clearCache(); void clearHistory(); void removeCookies(); }
现在有这样一个类,类中提供三种方法用来清除web浏览器的一些缓存。如果我们想一起清除Cache、History、Cookies,我们可以将这三个函数放在一个成员函数中去做,这样每次只需调用一个函数就行。
class WebBrowser{ public: void clearEverything(); //调用clearCache(),clearHistory(),removeCookies() }
但clearEverything()
是一个成员函数,条款中不是说宁以non-member、non-friend替换member吗?因此,可以这样做:
void clearBrowser(WebBrowser &wb) { wb.clearCache(); wb.clearHistory(); wb.removeCookies(); }
让它成为一个非成员函数。
而在c++中我们通常可以将类和有关该类的非成员函数放在同一命名空间中,就像这样:
namespace WebBrowserStuff { class WebBrowser {...}; void clearBrowser(WebBrowser& wb); ... }
同时,与WebBrowser
类有关的便利函数,可能有多个,以及多个种类,比如,与cookie相关的,还有与书签相关的等等。为了减少没必要的依赖(比如,我现在只想用与cookie相关的函数,也就没必要引入与书签相关的函数),我们可以将他们分别声明在不同的头文件中,但都属于WebBrowserStuff这一命名空间,因为,他们都是与WebBrowser有关的。
//头文件“webbrowser.h”这个头文件针对class WebBrowser自身及WebBrowser核心机能。 namespace WebBrowserStuff { class WebBrowser {...}; ... //核心机能,例如几乎所有客户都需要的non-member函数 } //头文件“webbrowserbookmarks.h” namespace WebBrowserStuff { ... //与书签相关的便利函数 } //头文件“webbrowsercookies.h” namespace WebBrowserStuff { ... //与cookie相关的便利函数 }
将便利函数放在多个头文件中但都属于同一命名空间,意味着我们可以轻松的在此基础上增加其他的便利函数,这也是命名空间较class好的一点,class是一个整体,对于用户来说不能扩展,而命名空间可以。
条款24:若所有参数皆需类型转化,请为此采用non-member函数
现在我们定义一个有理数的类:
class Rational { public: Rational(int numerator = 0, int denominator = 1); int numerator() const; int denominator() const; private: ... }
有理数相乘似乎是很正常的事,为此,在类中重载一下乘法运算:
class Rational { public: ... const Rational operator* (const Rational &rhs) const; }
这样便可支持两个有理数相乘:
Rational oneEighth(1, 8); Rational oneHalf(1, 2); Rational result = oneHalf * oneEighth; result = result * oneEighth;
让有理数和整数相乘似乎也是很正常的事,当我们执行:
result = oneHalf * 2;//正确 result = 2 * oneHalf;//错误
可以发现,其中一个报错,这是因为,oneHalf是一个内含operator*函数的class的对象,所以编译器调用该函数。而整数2没有相应的class,也就没有operator*成员函数。编译器尝试非成员函数operator*(也就是在命名空间内或在global作用域内),然而其他地方也没有相关定义,因此报错。再来看result = oneHalf * 2
,这个为什么正确呢?其实这里涉及到一个隐式的类型转换,即2通过构造函数转换为一个Rational类型,两个Rational类型相乘,自然没有报错。
只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者,正如result = oneHalf * 2
一样。对于出错的情况,我们可以重载一个非成员函数的乘法运算:
class Rational { ... }; const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); }
其实上述两种重载方式称为类内重载和类外重载,当两种重载方式同时存在时,编译器会优先使用类内重载,因为类内重载对于类的使用者来说是优先可见的,因此它的优先级更高(对于类的使用者而言,优先看到的肯定是意图中想用的)。