设计与声明(二)

来源:互联网 发布:java什么是方法的定义 编辑:程序博客网 时间:2024/06/03 17:01

条款22:将成员变量声明为private

成员变量可以是public,也可以是private,但是前者为什么不建议采用呢?首先从语法的一致性说起。如果成员变量不是public,那么客户唯一能够访问对象的办法就是通过成员函数。如果public的都是函数,客户就不需要在访问class成员的时候迷惑的记着是否应该使用圆括号。因为public的都是方法,没有成员变量。

另一个理由是使用函数可以对成员变量进行更精确的控制。如果成员变量时public,每个人都可以读写,但如果以函数取得或设定其值,就可以实现“读写访问”,“只读访问”或“只写访问”等操作。以下为例:

classAccessLevels { public:     int getReadOnlay() const {return readOnly;}     void setReadWrite(int value){readWrite = value;}     int getReadWrite()const {return readWrite;}     void setWriteOnly(int value){writeOnly = value;} protected: private:     int noAccess;     int readOnly;     int readWrite;     int writeOnly; };

从上示例代码可以看到对private成员变量进行细微的访问控制。

最后一个理由:封装。如果通过函数访问成员变量,日后可改以某个计算换成这个成员变量,而class用户一点也不知道class内部实现已经起了变化。成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。

而对于protected呢?protected成员的封装性是否高过public成员变量呢?答案是否定的。成员变量的封装性与“成员变量的内容改变时所破坏的代码数量”成反比,假设一个public成员变量,我们取消了它。所有使用它的客户码都会被破坏,那是一个不可知的大量。所以public成员变量完全没有封装性。假设一个protected成员变量,我们取消了它,所有使用它的derivedclasses都会被破坏,往往也是一个不可知的大量。从封装的角度来看,其实只有两种访问权限:private(提供封装性)和其他(不提供封装)

请记住:

1. 切记将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性

2.protected并不比public更具封装性

 

条款23:宁以non-member、non-friend替换member函数

有个class表示网页浏览器,现在要写一个函数用来清除浏览器的高速缓存区、URLs、cookies。请问是使用member函数好,还是使用non-member函数好?

classWebBrowser{public:    void clearCache();    void clearHistory();    void removeCookies();};//member实现方式classWebBrowser{public:    void clearEverything();};//non-member实现方式voidclearBrowser(WebBrowser& wb){    wb.clearCache();    wb.clearHistory();    wb.removeCookies();}

面向对象守则要求数据应该尽可能被封装,member函数带来的封装型比non-member函数低。此外,提供non-member函数可允许对WebBrowser相关机能有较大的包裹弹性,而那最终导致较低的编译相依度,因此,在许多方面non-member做法比member做法好。

让函数成为non-member,并不意味着它不可以是另一个class的member。在C++中,可以使用namespace对non-member与class进行包含,除了看上去更自然以外,namespace还能够跨越多个源码文件。

在c++中,比较自然的做法是让clearBrowser成为一个non-member函数并且位于WebBrowser所在的同一个namespace(命名空间)内:

namespaceWebBrowserStuff{    class WebBrowser{...};    void clearBrowser(WebBrowser wb);    ...}

像clearBrowser这样的函数是个“提供便利的函数”。一个像WebBrowser这样的class可能拥有大量的便利函数,某些与书签有关,某些与打印有关,某些与cookie的管理有关…,通常,大多数客户只对其中某些感兴趣。没有道理一个只对书签感兴趣的客户却与一个cookie相关便利函数发生编译相依关系。分离他们最直接的方法是将他们放入不同的头文件:

//头文件webbrowser.h这一头文件针对class WebBrowser自身以及WebBrowser核心机能。namespaceWebBrowserStuff{    void clearBrowser(WebBrowser wb);    ...//WebBrowser核心机能,几乎所有客户都需要的non-member函数。} //头文件“webbrowserbookmarks.h”namespaceWebBrowserStuff{    ...//与书签相关的便利函数} //头文件“webbrowsercookies.h”namespaceWebBrowserStuff{    ...//与cookie相关的便利函数}

c++标准程序库正是这样的组织方式。数十个头文件,每个头文件声明std的某些机能。如果只想使用vector不用#include;如果不想使用list也不需要#include.这允许客户只对他们所用的那一小部分系统形成编译相依。这种切割机能并不适合class成员函数,因为class必须整体定义,不能被分割为片段。namespace可以跨越多个源码文件,而class不能。将所有便利函数放在多个头文件中但隶属同一个命名空间,意味着客户可以轻松扩展这一组便利函数。他们所要做的就是添加更多non-membernon-friend函数到此命名空间内。

请记住:

宁可拿non-member non-friend函数替换member函数。可以增加封装性、包裹弹性(packaging flexibility)、和机能扩充性.

 

条款24:若所有参数皆需类型转换,请为此采用non-member函数

令class支持隐式类型转换,往往是个糟糕的主意。但有些情况是合理的,比如数值类型。考虑,有理数Rational有分子,分母两个字段,缺省参数值为0,1。Rationa = 2;我们期望构造一个分子为2,分母为1的有理数,这是非常合理和自然的。考虑Rational 有个成员方法operator*,如下:

classRational { public:     Rational(int numerator = 0, int denominator= 1); //非explicit支持隐式转换     const Rational operator*(constRational& rhs) const; //先看member函数的写法     ... }; RationaloneHalf(1, 2); Rational result1= oneHalf * 2; //good Rational result2= 2 * oneHalf; //wrong

对于result2中的代码呢,编译器首先试着如下解释:

2.operator*(half);

很明显,2.operator()是不存在的东西,所以,编译器会尝试着寻找一个non-memboperator(),但结果是:没有找到。所以只能返回一个错误。

可是对于同一个操作符竟然有两种结果,这是不可接受的。只要把member函数改成non-member的,你就可以随意用上面的用法了:

const Rationaloperator*(const Rational& lhs, const Rational& rhs) { ... } 

这样是一个比较好的解决方法,但是operator*是否应该成为一个friend函数呢?就本例而言答案是否定的,因为operator*完全可以借由public接口完成任务。这导出一个重要的观察:member函数的反面是non-member函数,不是friend函数。根据经验,尽量避免使用friend,为啥?因为friend破坏了封装。因此,如果不需要访问Rational的private成员,就不要声明为friend。

请记住:

假如你需要为某个函数的所有参数都进行类型转化的时候,这个函数必须是non-member

参考:点击打开链接

 

条款25:考虑写出一个不抛异常的swap函数

不理解为什么作者将该条款取名为“不抛异常的swap函数”,纵观全条款,其实大部分在描述如何写出一个节省资源的swap函数,只在最后一段描述了不抛异常的原因。

swap函数最初由STL引入,已经成为异常安全编程(条款 29)的关键函数, 同时也是解决自赋值问题(条款 11:赋值运算符的自赋值问题)的通用机制。std中的基本实现如下:

namespace std{   template<typename T>    voidswap(T& a, T& b){        T tmp(a);        a = b;        b = tmp;    }}

有时候std::swap并不是那么高效(对于自定义类型而言)。比如采用pimpl设计的类中,只需要交换实现对象的指针即可:

class WidgetImpl;class Widget {          // 采用pimpl设计的一个类    WidgetImpl*pImpl;   // 指针,指向Widget的实现       public:    Widget(constWidget& rhs);}; namespace std {   template<>                     // 模板参数为空,表明这是一个全特化    void swap<Widget>(Widget& a,Widget& b){   //”T是widget”的特化版        swap(a.pImpl, b.pImpl);     // 只需交换它们实体类的指针,目前不能通过编译    }}

上述代码无法通过编译,因为它试图访问a和b的priavte成员。所以,Widget应当提供一个swap成员函数或友元函数。 惯例上会提供一个成员函数:

class Widget {public:        void swap(Widget& other){    using std::swap;          // 注意声明的必要    swap(pImpl, other.pImpl); // 若置换widget就置换pImpl指针  }};

全特化std::swap,在这个通用的swap中调用那个成员函数:

namespace std {  template<>                  // 修订后的std::swap全特化版  void swap<Widget>(Widget& a,Widget& b){      a.swap(b);              // swap成员函数  }}

到此为止,该做法不仅能通过编译,并且和stl容器保持一致性。因为所有的STL容器提供有swap成员函数和特化std::swap(用以调用前者)。

但是遇到类模板的情况时,假设Widget和WidgetImpl都是classtemplates而不是classes, 按照上面的swap实现方式,我们想这样写:

template<typenameT>classWidgetImpl { ... }; template<typenameT>class Widget {... }; namespace std {    template<typename T>    // swap后的尖括号表示这是一个特化,而非重载。错误,不合法    void swap<Widget<T>>(Widget<T>& a, Widget<T>& b){        a.swap(b);    }}

看起来合情合理,但是不合法。我们企图偏特化一个函数模板(std::swap)。但c++只允许对classtemplates偏特化,在function templates身上偏特化是行不通的。这段代码是不能通过编译的(有些编译器错误地接受了它)。

当你打算偏特化一个function templates时,惯常的做法是简单的添加一个重载版本:

namespace std {    template<typename T>    // 注意swap后面没有尖括号,这是一个新的模板函数。    // 由于当前命名空间已经有同名函数了,所以算函数重载。    void swap(Widget<T>& a,Widget<T>& b){        a.swap(b);    }}

这里我们重载了std::swap,相当于在std命名空间添加了一个函数模板。这在C++标准中是不允许的!C++标准中,客户只能特化std中的模板,但不允许在std命名空间中添加任何新的模板。 上述代码虽然在有些编译器中可以编译,但会引发未定义的行为。可以考虑不在std命名空间下添加swap函数,把swap定义在Widget所在的命名空间中:

namespaceWidgetStuff {    template<typename T>    class Widget { ... };     template<typename T>    void swap(Widget<T>& a,Widget<T>& b){        a.swap(b);    }}

用户在调用的时候,到底应该使用哪个swap呢?如果希望应该首先调用T专属版本,并在该版本不存在的情况下使用std::swap的一般化版本:

template<typenameT>voiddoSomething(T& obj1, T& obj2){  using std::swap;           // 令`std::swap`在此函数内可用  swap(obj1, obj2);          // 为T型对象调用最佳的swap版本}

最佳编程实践,如何实现一个高效的swap函数:

1.   提供一个public swap不抛异常的公有成员函数,高效地置换你的类型的两个对象的值(比如Widget::swap)

2.   在你类(或类模板)的同一命名空间下提供非成员函数swap,并令它调用上述swap成员函数

3.   如果你写的是类而不是类模板,请特化std::swap,并调用你的swap成员函数

4.   调用时,请首先用using使std::swap在函数内可见,然后不加任何namespace修饰符直接调用swap

请记住:

1. std::swap对你的类型效率不高时,提供一个swap成员函数,这个成员函数不抛出异常

2. 如果提供一个member swap,也该提供一个non-member swap来调用前者,对于普通类,也请特化std::swap

3. 调用swap时,区分是调用自身命名空间的swap还是stdswap,不能乱加std::符号

4. 为“用户自定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

1 0
原创粉丝点击