设计与声明(二)
来源:互联网 发布: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还是std的swap,不能乱加std::符号
4. 为“用户自定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
- 设计与声明(二)
- Effective C++阅读笔记(二):设计与声明
- Effective C++笔记: 设计与声明(二)
- 设计与声明(一)
- Effective C++(四)设计与声明
- 设计与声明
- 4.设计与声明
- 4 设计与声明
- 第四章 设计与声明
- 第四章 设计与声明
- Effective C++ -- 设计与声明
- C++ 设计与声明原则
- Oracle12C--变量的声明与赋值(二十五)
- Effective C++读书笔记(四) 设计与声明
- 如何写出高效C++(设计与声明)
- C++(4)设计与声明
- (二)分析与设计
- Effective C++笔记: 设计与声明(一)
- Python参数小结
- 查看jks文件信息
- Android中关于Volley的使用(五)从RequestQueue开始来深入认识Volley
- Linux-Ubuntu14.04下配置ftp,安装java1.8和tomcat8
- 14章 解答4 根据指定不同字符,生成不同三角形
- 设计与声明(二)
- codeforces373#A
- 占位符解析@#
- Java 变量初始化总结
- QList<T> 的释放分两种情况
- Python(x,y)安装
- 【Spring】Spring之我见
- codeforces373B#
- BZOJ 2502 清理雪道