四、设计和声明--条款21-23
条款21:必须返回对象时,别妄想返回其reference
看下面这个类,是一个表现分数相乘的class:
class Rational
{
public:
Rational(int numerator = 0, int denominator = 1);
const Rational operator*(const Rational& lhs, const Rational& rhs);
...
private:
int numerator;
int denominator;
};
这段代码里面,重载乘号返回的是以by-value形式的计算结果。那就要承担额外的构造和析构成本。
Q:这样做是不建议的吗?
既然要承担额外的构造和析构成本,那么我们暂且使用引用代替之,看看是否会更好。
仿佛听起来是正确的,其实不然。任何时候我们使用引用,都要问问,引用绑定的另一个名称是什么?
方案一
我们要先定义个变量来被引用绑定:
const Rational& operator*(const Rational& lhs,const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
方案一的错误之处
- 在这个函数中,我们还是没有避免拷贝构造函数的调用。即使放回了引用,我们还是需要定义个变量来和引用绑定。=
- 这个函数有个致命的错误:返回了一个local对象作为引用,出了作用域,local对象就被销毁了。 任何对象和这个local对象返回值绑定,只要对这个返回值做出一点运用,就会立刻坠入“无定义的行为”之中。
方案二
避免返回一个local对象。
const Rational& operator*(const Rational& lhs,const Rational& rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
现在我们返回的是一个动态分配的对象,就不会出了这个作用域就被销毁,那么问题来了:谁应该负责对这个对象进行销毁呢?
可能你真的比较细心,有意识的去释放自己使用完的引用对象,但是如果是这样呢?
Rational w, x, y, z;
w = x * y * z;
当执行y*z时就new出一个对象,使用它返回的引用结果再和x相乘,这时已经有两个对象被new出来,但是并没有一个合理的方法去释放它们!
综合以上的情况,返回一个引用在这种情况下是不妥的,我们宁愿选择承担额外的构造和析构成本的by-value方法。
作者总结
绝不要返回pointer或reference指向一个local stack对象,或者返回一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
条款22:将成员变量声明为private.
作者在书中总结了一些优点:
- 使用函数访问成员变量,函数都有个小括号,方便记忆。
- 可以精准控制每个成员变量,不至于每个客户都可以用各种方法访问(读、写、读写)。
- 封装。为所有可能的实现提供弹性。通过调用函数而隐藏其内部实现。
- 代码破坏量。
- 假如使用public:没有封装性。假如我们取消了一个public成员变量,那么所有使用它的客户端都会被破坏。
- 家兔使用protected: 没有封装性。如果取消了一个protected成员变量,那么所有使用它的derive对象都会受到破坏。
综上,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。
总结
切记将成员变量声明为private。这可赋予客户访问一切数据一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者充分的实现弹性。
protected并不比public更具封装性。
条款23:宁以non-member,non-friend替换member函数
Q:这样做有什么好处?
封装。首先从封装开始考虑。愈多东西被封装,愈少人可以看到它。俞少人看到,我们就有俞大的弹性去改变它。俞少的代码可以看到数据,封装性就俞强。
所以为了更大的封装性,我们有时候宁以non-member,non-friend函数替换类的member函数。下面给出一个浏览器清除数据的例子:
我们有一个WebBrowser类,里面有清理高速缓存区,清理历史记录,清理系统中所有的cokies:
class WebBrowser
{
public:
void clearCache();
void clearHistory();
void removeCookies();
...
}
现在我们想要有一个能够一次删除全部的函数clearBrowser。这个情况比较自然的做法是让clearBrowser成为一个non-member函数,并且处于和WebBrowser同一个命名空间下:
namespace WebBrowserStuff
{
class WebBrowser{...}; // 包括三个清除函数
void clearBrowser(WebBrowser &wb);
}
这样就和我们所讨论的增加封装性的方法一致了。我们将这样non-member函数称为便利函数(为类提供便利执行方法,却不需要访问类中的数据)。
存在大量的便利函数带来的影响和解决方法
倘若我们存在许多的便利函数,但特定的便利函数只与特定的操作相关:比如某些与书签有关,某些与打印有关,某些与cookie相关,但是如果声明在同一个头文件中,这些便利函数都会有编译相依的关系。事实上,有关书签的便利函数和cookie的便利函数是没有任何关系的。
解决之道:分别建立多个头文件,需要的时候在引用相应的头文件,这样就能使我们所需要调用的那一部分系统形成编译相依,降低了文件编译依存性。
千万别将class成员函数用此方法进行切割,成员函数需要保持整体定义,不可被分割成多个部分。
在我们这一个条款的例子里可以分割成以下样子:
// webbrowser.h头文件,不可分割。
namespace WebBrowser
{
class WebBrowser
{
...// 将member函数写进去
}
}
与书签相关的便利函数独立抽出来放到webbrowserbookmarks.h中:
namespace WebBrowser
{
... // 与书签相关的便利函数,非member函数。
}
与cookie相关的便利函数独立抽出来放到webbrowsercookies.h中:
namespace WebBrowser
{
... // 与cookies相关的便利函数,非member函数。
}
其他便利函数也均采用此方法即可。
作者总结
尽量以non-member函数替换member,friend函数,这样做可以增强封装性,包裹弹性和机能扩充性。