t Rational& rhs) {
return Rational(lhs.n1*rhs.n1, rhs.n2*rhs.n2);
}
5. 将成员变量声明为private
在我们最初学习C++ OOP时就有一天准则,成员变量总是要声明为private。本节我们来讨论为何成员变量要被声明为private。
理由一:语法一致性。
因为成员变量不是public,客户唯一能够访问对象的办法就是通过成员函数。如果public接口内的每一样东西都是函数,客户就不用纠结调用他时是否需要使用小括号。如此便能省下大量的时间。
理由二:使用函数可以让你对成员变量的处理有更准确的控制。
如果成员变量是public,那么每个人都可以对他进行读写,但如果你以函数取得或设定其值,就可以实现“不准访问”,“只读访问”,“读写访问”等访问控制。
如以下代码:
class AccessLevel {
private:
int noAccess;
int ReadOnly;
int WriteOnly;
int readWrite;
public:
// ...
int getReadOnly() {
return ReadOnly;
}
void setWriteOnly(int i) {
WriteOnly = i;
}
void setreadWrite(int i) {
readWrite = i;
}
int readreadWrite() {
return readWrite;
}
};
如此精细地对各个数据成员进行访问限制是有必要的。
理由三:封装!
这是最有说服力的理由了!C++ OOP其中最重要的一条性质就是封装性!将数据成员封装在接口的后面,可以为“所有可能的实现”提供弹性。
封装的重要性比我们最初见到它时更重要。如果我们对客户隐藏成员变量,就可以确保class的约束条件受到维护,因为只有成员函数可以影响他们。public意味着不封装,而几乎可以说不封装意味着不可改变,特别是对被广泛使用的class而言。被广泛使用的class是最需要封装的一个族群,因为他们能够从“改采用一个教佳实现版本”中获益。
我们继续来讨论protected的封装性。
一般人会认为protected比public更具有封装性。其实不然。更准确的判断方法是:某些东西的封装性与“当其内容改变时可能造成的代码破坏量”成反比。所谓改变,也许是从class中移除他。于是乎,我们可以进行以下分析。对于public的成员变量,如果我们移除他,意味着我们要破坏所有使用它的客户代码。(破坏量很大吧)而对于protected的成员变量呢,如果我们移除它,意味着要破坏所有derived class(破坏量也很大吧?)因此protected和public的封装性其实是一样的。这也就意味着,一旦我们决定把某个成员变量声明为public或protected,就很难改变某个成员变量所涉及的一切。
结论就是,其实只有两种访问权限:private(实现封装)和其他(不实现封装)
6. 宁以non-member、non-friend替换member函数
面向对象守则要求,数据以及操作数据的那些函数应该被捆绑在一起,这意味着它建议所有操作数据成员的函数都应该是member函数。然而事实上是如此吗?
问题产生
假设我们希望写一个类来描述网页:
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
// 用户希望有一个函数能够清楚所有信息
// 问题是,该函数是否应该声明为member?
void clearEverything();
};
// 也可以声明为non-member
void clearEverything(WebBrowser &web) {
...
}
那么哪种选择更好呢?
问题解决
根据面向对象守则要求,声明为member函数应该是更好的选择。然而,这是对面向对象真实意义的一个误解。面向对象要求数据应该尽可能被封装,然而与直观相反地,member函数clearEverything带来的封装性比non-member函数的低。此外,提供non-member函数可允许对WebBrowser相关机能有更大的包裹弹性,从而最终导致较低的编译相依度,增加WebBrowser的可衍生性。以下我们给出理由。
封装性。愈多的东西被封装,越少人可以按到它,那么我们就有越大的弹性去改变它,而我们的改变只会影响看到改变的那些人和事物。这就是我们推崇封装性的原因:它使我们能够改变事物而只影响有限客户。
考虑对象内数据。越少代码可以看到数据,越多的数据可被封装,而我们也就越能自动地改变对象数据。越多的函数可以访问数据成员,数据的封装性就越差!
因此,因为non-member non-friend函数不能直接改变数据成员,因此他就可以最大限度的实现封装。
解答优化
在C++中,最自然的做法,是让clearEverything称为一个non-member函数并且位于WebBrowser所在的同一个namespace内:
namespace WebBrowserStuff {
class WebBrowser {...};
void clearEverything(WebBroswer &web);
...
}
namespace和class是不用的!前者可以跨越多个源码文件而后者不能,这很重要!
像clearEverything这样的函数就是便利函数,虽然没有对WebBrowser有特殊的访问权限,但可以极大的便利客户。而实际上,我们会补充大量的类似的便利函数,并且他们可能分属于不同的模块,于是我们便采用把不同模块便利函数写于不同的头文件中,但他们都隶属于同一个命名空间:
#include "webbrowser.h" 提供class声明本身,以及其中核心机能
namespace WebBrowserStuff {
class WebBroser { ... };
... // 核心机能,几乎所有用户都需要的non-member便利函数
}
// 头文件 “webbrowserbookmarks.h"
// 与标签相关
namespace WebBrowserStuff {
... // 与标签相关的便利函数
}
// 头文件 ”webbrowsercookies.h"
namespace WebBrowserStuff{
... // 与cookie相关的便利函数
}
...
注意这是C++标准程序库的组织方式。标准程序库中并不是拥有单一、整体、庞大的
7. 若所有参数皆需类型转换,请为此采用non-member函数
令class支持隐式类型转换通常是个糟糕的注意。当然也有例外,例如你在建立数值类型时。
问题产生
假设我们需要设计一个有理数类:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denomin