四、设计和声明--条款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;
}
方案一的错误之处
  1. 在这个函数中,我们还是没有避免拷贝构造函数的调用。即使放回了引用,我们还是需要定义个变量来和引用绑定。=
  2. 这个函数有个致命的错误:返回了一个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函数,这样做可以增强封装性,包裹弹性和机能扩充性。

posted @ 2018-09-18 15:46  _NewMan  阅读(211)  评论(0编辑  收藏  举报