《Effective C++》学习笔记——条款28

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************





五、Implementations




 

Rule 28:Avoid returning "handles" to object internals

规则 28:避免返回handles指向对象内部成分




假设我们的程序涉及矩形。每个矩形由其左上角和右下角表示。为了让一个Rectangle对象尽可能小,你可能会决定不把定义矩形的这些点放在Rectangle对象内,而是放在一个辅助的struct内再让Rectangle去指它:

class Point {    // 描述点 的类
public:
Point( int x ,int y );
…
void setX(intnewVal);
void setY(intnewVal);
…
};
struct RectData  {    //用这些”点”数据用来表现一个矩形
Point ulhc;    // 左上角
Point lrhc;    // 右下角
};
class Rectangle  {
…
private:
 std::tr1::shared_ptr<RectData> pData;
};


为了计算Rectangle的范围,这个类提供upperLeft函数 和 lowerRight函数,因为Point是用户自定义函数,所以这些函数是返回reference,代表底层的Pointer对象:

class Rectangle  {
public:
…
Point&upperLeft() const {  returnpData->ulhc; }
Point&lowerRight() const {  returnpData->lrhc; }
…
};


虽然这种设计可以通过编译,但是它是错误的。实际上它是自我矛盾的——

①upperLeft和lowerRight被声明为const成员函数,因为它们的目的只是为了提供客户一个得知Rectangle相关坐标点的方法,而不是让客户修改Rectangle;

②两个函数却都返回reference指向private内部数据,调用者于是可通过这些 reference 更改内部数据。

一个明显的例子:

Point coord1(0,0);
Point coord2(100,100);
const Rectangle rec(coord1,coord2);    // 定义矩阵 rec,左上角(0,0),右下角(100,100)
rec.upperLeft().setX(50);    // 将rec变成 左上角(50,0),右下角(100,100)


显然,upperLeft的调用者能够使用被返回的reference来更改成员。但rec实际上应该是不可变的。

上面这个例子,告诉我们两点:

? 成员变量的封装性最多只等于”返回其reference“的函数的访问级别。

? 如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。这正是bitwise constness的一个附带结果。

 

我们上面说的每件事情都是由于“成员函数返回reference”。如果它们返回的是指针或迭代器,相同的情况还是会发生,原因也相同——reference、指针 和 迭代器统统都是所谓的 handles,而返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。

PS:我们通常认为的对象的“内部”就是指它的成员变量,但其实不被公开使用的成员函数(也就是 protected 或 private)也是对象“内部”的一部分。

 

解法:“返回指针指向某个成员函数”的情况并不多见,所以让我们把注意力收回,专注于Rectangle class和它的 upperLeft以及lowerRight成员函数。我们在这些函数身上遭遇的两个问题可以轻松去除,只要对它们的返回类型加上const即可:

class Rectangle  {
public:
…
const Point& upperLeft() const { return pData->ulhc;  }
const Point& lowerRight() const { return pData->lrhc;  }
…
};


经过这种改变,用户就只能读取矩形的Points,但不能修改它们。所以,当初将 upperLeft 和 upperRight为const就不再是空壳。至于封装性,因为我们愿意让用户看到Rectangle的外围Points,所以这里刻意的放松了封装。

但是,即便如此,upperLeft和lowerLeft还是返回了“代表对象内部”的handles,这些还是有可能在其他场合带来问题。更准确的来说,可能导致 dangling handles(空悬的号码牌):这种handles所指东西(的所属对象)不复存在。这种“不复存在的对象”最常见的来源就是函数返回值。例如某个函数返回GUI对象的外框,这个外框采用的是矩形形式:
<span style="font-family:KaiTi_GB2312;font-size:14px;">class GUIObject  { ... };
const Rectangle boundingBox(const GUIObject& obj );    // 通过by value方式返回一个矩形
</span>


现在,用户有可能这么使用这个函数:
<span style="font-family:KaiTi_GB2312;font-size:14px;">GUIObject* pgo;    // 让pgo指向某个GUIObject
...
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());    // 取得一个指针指向外框左上点</span>


? 对boundingBox的调用获得一个新的、暂时的Rectangle对象。这个对象暂时叫它temp(为了后面叙述的方便)。之后upperLeft作用于temp身上,返回一个reference指向temp的一个内部成分,更具体地说是指向一个用以标示temp的Points。于是pUpperLeft指向那个Point对象。目前为止一切还好,但故事尚未结束,因为在那个语句结束之后,boundingBox的返回值,也就是我们所说的temp,将被销毁,而那间接导致temp内的Points的析构。最终导致pUpperLeft指向一个不再存在的对象;也就是说一旦产出pUpperLeft的那个语句结束,pUpperLeft也就变成空悬、虚吊!
? 这就是为什么函数如果“返回一个handle代表对象内部成分”总是危险的原因。不论这所谓的handle是个指针或迭代器或reference,也不论这个handle是否为const,也不论那个返回handle的成员函数是否为const。这里唯一的key是,有个handle被传出去了,这就导致了“handle可能比其所指对象存在时间更长”。
? 但是这并不意味着绝对不可以让成员函数返回handle,有时候不得不这么做。比如,operator[]就允许你获取 strings 和 vectors的个别元素,而这些operator[]s就是返回references指向“容器内的数据”,那些数据会随着容器被销毁而销毁。尽管如此,这样的函数毕竟是例外,不是常态。



☆请记住★
避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降至最低。





***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。