《Effective C++》资源管理:条款22-条款24
条款22:将成员变量声明为private
class AccessLevels{ public: …… int getReadOnly() const {return readOnly;} void setReadWrite(int value){readWrite=value;} int getReadWrite()const{return readWrite;} void setWriteOnly(int value){writeOnly=value;} private: int noAccess;//无任何访问动作 int readOnly;//只能读 int readWrite;//能读能写 int writeOnly;//只能写 };
如果上述理由还不够,那么还有一个更重要的理由:封装。如果通过函数访问成员变量,日后可以用某个计算替换这个变量,这时class的客户却不知道内部实现已经变化。
例如,你正在写一个自动测速程序,当汽车通过,其速度便被填入到一个速度收集器内:
class SpeedDataCollection{ …… public: void addValue(int speed);//添加一笔新数据 double averageSoFar() const;//返回平均速度 …… };
现在考虑怎么实现函数averageSoFar。一种做法是在class内设计一个变量,记录至今以来所有速度 的平均值;当averageSoFar被调用,只需要返回那个成员变量就好。另一种做法是让averageSoFar每次被调用时重新计算平均值,这个函数有权限读取收集器内的每一笔速度值。
上述第一种做法(随时保持平均值)会使每一个SpeedDataCollection对象变大,因为必须为用来存放目前平均值、累计总量、数据点数的每一个成员变量分配空间;但是这会使averageSoFar十分高效,它可以只是一个返回目前平均值的inline函数(条款30)。第二种做法,“每次被问询才计算平均值”会使得averageSoFar执行较慢,但是这时SpeedDataCollection对象占用空间比较小。
具体哪种做法比较好,要视具体情况而定。在内存吃紧的机器上(例如嵌入式设备),或者在不需要常常计算平均值的应用中,第二种做法比较合适。但是在一个频繁需要平均值的应用程序中,如果反应速度非常重要,内存不是考虑因素,这时候第一种做法 更好。上面这两种实现都是通过函数来访问平均值(即封装了它),你可以替换不同的实现方式,客户最多只需要重新编译。(如果遵循条款31,甚至你都不需要重新编译)
将成员变量隐藏在函数接口背后,可以为“所有可能的实现”提供弹性。例如这使得成员变量被读或被写时轻松通知其他对象、可以验证class的约束条件以及函数的前提和事后状态、可以在多线程环境中执行同步控制……等等。
封装的重要性或许比你想象中重要。如果你对客户隐藏成员变量(封装它们),可以确保class约束条件获得维护、保留了日后变更实现的权利。如果不封装,日后更改public事物的能力是极端收到束缚,因为修改public变量会影响太多客户代码。protected成员的封装貌似高于public,但是事实并非如此,修改protected成员变量,多少derived类需要修改或多少使用derived对象的客户代码需要修改。
条款23中,将会看到“某些东西的封装性”与“当期内容改变时可能造成的代码破坏量”成正比。一旦成员变量声明为public或protected,就能难改变那个成员变量所涉及的一切。因为太多代码需要重写、重新测试、重新编写文档、重新编译。从封装角度看,只有两种访问权限:private(封装)和其他(不封装)。
总结:
1、将成员变量声明为private。这可以赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保证,并提供class作者以充分弹性实现。
2、protected并不比public更具有封装性。
条款23:宁以non-member、non-friend替换member函数
这个条款讲解成员函数和友函数的区别。
考虑一个class用来清除浏览器的一些记录,这个class中有清除告诉缓存区的函数,有清除访问过URLs的函数,还有清除cookies的函数:
class WebBrowser{ public: …… void clearCash(); void clearHistory(); void removeCookies(); …… };一般情况下,需要同时执行这三个动作,因此WebBrowser可以提供这样一个函数:
class WebBrowser{ public: …… void clearEverything() { clearCash(); clearHistory(); removeCookies(); } …… };
另一种做法是用一个non-member函数调用适当的member函数
void clearBrowser(WebBrowser& wb) { wb.clearCash(); wb.clearHistory(); wb.removeCookies(); };
上面两种做法,哪种比较好呢?答案是non-member函数比较好。
面向对象思想要求,数据尽可能被封装,member函数clearEverything带来的封装性比non-member函数clearBrowser低。提供non-member函数,对class相关机能有较大包裹弹性(packaging flexibility),因此带来了较低的编译相依度,增加了class的可延展性。
封装意味着不可见。愈多东西被封装,欲少人可以看到它,我们就有愈大的弹性去改变它。愈少代码可以看到数据(访问数据),愈多数据可被封装,我们就更有自由来改变对象数据。愈多函数可以访问它,数据的封装性就愈低。
条款22有讲到,成员变量应该是private,否则就有无限多函数可以访问它,毫无封装可言。能访问private成员变量的函数只有class的member函数、friend函数而已。在一个member函数和一个non-member、non-friend函数之间做抉择,如果两者提供相同的机能,显然后者提供了更大的封装,这个就是上面选择clearBrowser函数的原因。
在封装这点上,需要注意两点。1、这个论述只适用于non-member、non-friend函数。2、因为封装,让函数成为class的non-member函数,但这并不意味着它不可以是另一个class的member函数。
在C++中,实现上述功能,比较自然的做法是把clearBrowser函数和WebBrowser类放到一个命名空间内:
namespace WebBrowserStuff{ class WebBrowser{……}; void clearBrowser(WebBrowser& we); …… }
这不仅仅是看起来整齐。namespace可以跨越多个源码文件,class不能。像clearBrowser这样的函数只是为了提供便利,它是non-member、non-friend,没有对WebBrowser的特殊访问权力。一个像WebBrowser这样的class可能拥有大量便利函数,例如某些与书签相关,某些与打印有关,某些与cookies相关……。通常客户使用是时只是对其中一些感兴趣。在编码时通常分离它们:将书签相关便利函数声明于一个头文件,将cookie相关函数声明于另一个头文件,再将打印相关函数声明到第三个头文件……。
//头文件webbrowser.h,这个头文件针对class WebBrowser自身及WebBrowser核心机能 namespace WebBrowserStuff{ class WebBrowser{……};//核心机能 ……//non-member函数 } //头文件webbrowserbookmarks.h namespace WebBrowserStuff{ ……//与书签相关的便利函数 } //头文件webbrowsercookies.h namespace WebBrowserStuff{ ……//与cookie相关的便利函数 }
这也正是C++标准库的组织方式。标准库有数十个头文件(<vector>,<algorithm>,<memroy>等等),每个头文件声明std的某些机能。如果客户想使用vector相关机能,只需要#include<vector>即可。这也允许客户只对他们所用的那一小部分形成编译相依(条款31,其中讨论降低编译依赖性的其他做法)。
将所有便利函数放到多个文件夹但隶属同一个命名空间,意味着客户可以轻松扩展这一组便利函数,他们要做的就是往命名空间添加更多non-member函数和non-friend函数,这也是class无法做到的。当然客户可以继承类来扩展出新类,但是derived class无法访问base class中封装的private成员,因此扩展的机能拥有的只是次级身份。
总结:用non-member、non-friend函数替换member函数,这样可以增加封装性、包裹弹性和机能扩充性。
条款24:若所有参数皆需要类型转换,请为此采用non-member函数
通常情况,class不应该支持隐式类型转换,因为这样可能导致我们想不到的问题。这个规则也有例外,最常见的例外是建立数值类型时。例如编写一个分数管理类,允许隐式类型转换
class Rational{ public: Rational(int numerator=0, int denominator=1);//非explicit,允许隐式转换 …… };如果要支持加减乘除等运算,这时重载运算符时是应该重载为member函数还是non-member函数呢,或者non-member friend函数?
如果写成member函数
class Rational{ public: …… const Rational operator*(const Rational& rhs); …… };
这样编写可以 使得将两个有理数相乘
Rational onEight(1,8); Rational oneHalf(1,2); Rational result=onEight*oneHalf; result=result*onEight;如果进行混合运算
result=oneHalf*2;//正确,相当于oneHalf.operator*(2); result=2*oneHalf;//错误,相当于2.operator*(oneHalf);
不能满足交换律。因为2不是Rational类型,不能作为左操作数。oneHalf*2会把2隐式转换为Rational类型。
上面两种做法,第一种可以发生隐式转换,第二种却不可以,这是因为只有当参数被列于参数列(parameter list)内,这个参数才是隐式类型转换的合格参与者。第二种做法,还没到到”参数被列于参数列内“,2不是Rational类型,不会调用operator*。
如果要支持混合运算,可以让operator*成为一个non-member函数,这样编译器可以在实参身上执行隐式类型转换。
const Rational operator*(const Rational& lhs, const Rational& rhs);
这样就可以进行混合运算了。那么还有一个问题就是,是否应该是operator*成为friend函数。如果可以通过public接口,来获取内部数据,那么可以不是friend函数,否则,如果读取private数据,那么要成为friend函数。这里还有一个重要结论:member函数的反面是non-member函数,不是friend函数。如果可以避免成为friend函数,那么最好避免,因为friend的封装低于非friend。
当需要考虑template时,让class变为class template时,又有一些新的解法。这个在后面条款46有讲到。
总结:如果需要为某个函数的所有参数(包括this指针所指向的隐喻参数)进行类型转换,这个函数必须是个non-member函数
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。