《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函数

时间: 2024-08-02 10:54:16

《Effective C++》资源管理:条款22-条款24的相关文章

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

(一)为什么不采用public成员变量 (1)首先,从语法一致性考虑,客户唯一能访问对象的方法就是通过成员函数,客户不必考虑是否该记住使用小括号(). (2)其次,使用函数可以让我们对成员变量的处理有更精确的控制.如果我们令成员变量为public,那么每个人都可以读写它! 但如果我们以函数取得或设定其值,我们就可以实现出"不准访问"."只读访问"以及"读写访问",我们甚至可以实现"惟写访问". class AccessLeve

EC读书笔记系列之12:条款22、23、24

条款22 将成员变量声明为private 记住: ★切记将成员变量声明为private.这可赋予客户访问数据的一致性.可细微划分访问控制.允诺约束条件获得保证,并提供class作者以充分的实现弹性. ★protected并不比public更具封装性 条款23 宁以non-member-non-friend替换member函数 记住: ★宁可拿non-member-non-friend函数替换member函数.这样可增加封装性.包裹弹性和机能扩充性. -----------------------

Effective C++_笔记_条款08_别让异常逃离析构函数

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) C++并不禁止析构函数吐出异常,但它不鼓励你这样做.考虑如下代码: 1: class Widget{ 2: public: 3: ... 4: ~Widget() {...} //假设这个可能吐出一个异常 5: }; 6:  7: void doSomething() 8: { 9: vector<Widget> v ; //v在这里被自动销毁 10: ...

Effective C++ 阅读笔记_条款27 尽量少做转型动作

Effective C++ 阅读笔记_条款27 尽量少做转型动作 1.转型的三种形式,可以分为两大类. (1)旧式转型(old-style casts) (1.1) (T) expresstion (1.2) T (expression) (2) 新式转型(c++-style casts) (2.1)const_cast<T> (expression) (2.2)dynamic_cast<T> (expression) (2.3)reinterpret_cast<T>

Effective C++_笔记_条款12_复制对象时勿忘其每一个成分

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 编译器会在必要时候为我们的classes创建copying函数,这些“编译器生成版”的行为:将被烤对象的所有成员变量都做一份拷贝. 如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省实现中的某些行为.编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当你的实现代码几乎必然出错时却不告诉你.所以自己实现copying函数时,请遵循一条规则:如果你为c

Effective C++ (笔记) : 条款18 -- 条款23

条款18:让接口容易被正确使用,不易被误用 在(参数)类型上客户不知道怎么使用的时候,可以导入简单的"外覆"类型来区别参数.也就是,自定义数据类型,使客户明确调用相关的类型,防止误用. 尽量让自定义类型的行为和内置类型的行为相同,因为客户会想当然的和使用内置类型一样使用自定义类型,这也就是上面说的让接口容易被正确的使用.STL容器的接口十分一致,这也是他们非常容易使用的一个原因. 任何接口如果要求客户必须记得做某些事情,那么就有着"不正确的使用"的倾向,因为客户可能

《MORE EFFECTIVE C++》条款20 条款21

条款20 协助编译器实现返回值优化 当重载运算符的时候,比如+ - * / 这类运算符,该函数返回的值一定是个右值(即不能是引用),那么执行一次运算的开销可能会在临时对象上调用多次构造函数和析构函数,这笔开销还是很大的.现在的新编译器已经可以对这种情况进行优化了,甚至优化到连开销都没有,只是有一定的适用范围.如果可以返回一个匿名的临时对象,并且利用构造函数来得到结果对象,那么就有可能被优化到零开销.注意,有名字的对象意味着返回值优化不可用. 假设有如下的代码: 1 node a(2); 2 no

Effective C++_笔记_条款11_在operator=中处理“自我赋值”

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 为什么会出现自我赋值呢?不明显的自我赋值,是“别名”带来的结果:所谓“别名”就是“有一个以上的方法指涉对象”.一般而言如果某段代码操作pointers或references而它们被用来“指向多个相同类型的对象”,就需要考虑这些对象是否为同一个.实际上两个对象来自同一个继承体系,它们甚至不需要声明为相同类型就可能造成“别名”.因为一个base class的refe

Effective C++_笔记_条款07_为多态基类声明virtual析构函数

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 这个规则只适用于polymorphic(带多态性质的)base class身上.这种base class的设计目的是为了用来“通过base class接口处理derived class对象”.假如我们在程序中设计factory(工厂)函数,让它返回base class指针,指向新生成的derived class对象,假设base class有个non-virtu