《Effective C++》资源管理:条款22-条款24

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

讲解这个条款的思路是,首先看看成员变量为什么不应该是public,这个观点同样适用于protected成员变量,最后得出结论:成员变量应该是private。
首先从语法一致性开始(条款18),如果成员变量不是public,那么客户访问该成员的唯一方法就是通过成员函数(如果没有友函数)。如果public接口内的每样东西都是函数,客户在使用这个对象时,就不需要疑问到底是访问变量还是函数了,因为这个时候不能访问成员变量。
或许一致性不是令你信服的理由。还有一个理由:使用函数可以让你对成员变量的处理有更加精确的控制。如果成员变量为public,那么每个人都能读和写,但是如果通过函数读或写其值,那么就能实现“不准访问”、“只读访问”以及“读写访问”,甚至实现“惟写访问”,这个性质有点像C#中的get、set。
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函数



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值