《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********************************************

时间: 2024-08-25 09:22:51

《Effective C++》学习笔记——条款28的相关文章

effective c++学习笔记条款23-25

条款23:宁可用非成员,非友元函数来替代成员函数 1.非成员函数提供了更好的封装性,这个函数内不能访问类的私有成员,封装的越严密我们对类的数据就可以弹性越大的操纵,因为可见这些数据的客户越少,反之数据影响的客户也就越少. 2.c++比较自然的做法-(关系到标准库numplace的组织结构),可以把不同便捷函数放到不同Namespace去,让客户来决定要用的非成员函数功能,这是类不能提供的. 条款24:若所有参数皆需类型转换,请为此采用非成员函数. 1.如果你需要为某个函数的所有参数(包括被thi

effective c++学习笔记条款11-13

条款11: 1.令赋值运算符返回一个&,因为STL,string都是这样做的,除非你有足够好的理由不这样做. 2.处理自我赋值的方法----(1).在没有成功获取对象数据时不要删除自己的数据,避免发生异常后原对象指针是一个悬浮指针 (2).判断自我赋值的检查操作会耗费不少时间,可以用swap交换数据技术来优化---(1)形参为赋值而来,(2)形参为静态引用,多加一个函数内拷贝操作.

effective c++学习笔记条款8-10

条款7:为多态基类声明虚析构函数 1.一个基类指针接受一个派生类对象的地址时,对该指针delete,仅仅释放基类部分 2.给所有类都带上虚析构函数是个馊主意,会带有vptr指向一个函数指针数组,扩大不必要的对象大小,除非补偿vptr,否则没有移植性. 3.string类和STL不含有虚析构函数,然而一些用户 却将他们作为基类,运用   delete指向派生类的基类指针,导致错误[c++11添加了禁止派生性质],他们不适合当基类. 4,手头上没有合适的纯虚函数,但你确实需要一个抽象类,把析构函数声

effective c++学习笔记条款20-22

条款20:用引用传递代替值传递 1.尽量以引用传递来代替传值传递,前者比较高效,并且可以避免切割问题 2.以上规则不适用于内置类型,以及STL的迭代器,和函数对象 条款21:必须返回对象时,别妄想返回对象的引用 1.绝对不要返回指针和引用指向一个局部对象或者静态局部对象而有可能需要多个这样的对象,条款4已经为在单线程环境合理返回&指向一个局部静态提供了一份设计实例.(保护初始化顺序) 条款22:将成员变量声明为private 1.切记将成员变量声明为private.这可赋予客户访问数据的一致性,

effective c++学习笔记条款4-7

条款4:确定对象被使用前已经初始化 一. 变量在不同情况下可能会初始化,也可能不会初始化. 注意初始化和赋值的区别. 1.在类中内置类型不会发生隐式初始化,自定义有默认构造函数的能被默认初始化 所以在构造类时务必初始化内置类型,最好给自定义的对象显示初始化避免在函数体中赋值浪费资源. 2.内置类型在函数体内不会初始化,在函数体外自动初始化为0. 二. 1.const和引用类型必须初始化,不可能赋值 三 1.当类实在是有较多构造函数,并且总是要对一些成员数据重复初始化,可以考虑将那些“赋值和初始化

effective c++学习笔记条款17-19

条款17:以独立语句将New对象放置入智能指针. 1.以独立语句将newed对象放置入智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露. void name(shared_ptr<管理对象类型>(new 管理对象类型),其它函数)),New被分配内存不一定马上放入管理对象,因为有其它函数干扰,这不是独立语句. 条款18:让接口容易被正确使用,不易被误用. 1.好的接口很容易被正确使用,不容易被误用.你应该在你的所有接口中努力达成这些性质. 2.“促进正确使用”的办法包括接

effective c++学习笔记条款35-37

#include<iostream> using namespace std; class A { public: void asd() { pri(); } private: /*virtual*/ void pri() { cout << "基类函数" << endl; } }; class B :public A { private: void pri() /*override*/ { cout << "派生类函数&quo

effective c++学习笔记条款29-31

条款29:为异常安全而努力是值得的[回顾] 1.异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏,这样的函数分为3种可能的保证:基本型,强烈型,不抛异常型 2.“强烈保证”往往能通过copying and swap 来实现出来,但并非所有函数都可实现或者具备现实意义. 3.函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全中”的最弱者.

effective c++学习笔记条款26-29

条款26:尽可能延后变量定义式的时间 1.中途抛出异常浪费构造函数 2.在循环内定义变量,消耗n个构造函数,n个析构函数:在循环外定义变量消耗n个赋值函数,1个构造,一个析构: 除非赋值的消耗比构造和析构少的不少,或者你处理的代码效率高度敏感,还是在循环内定义变量吧. 条款27:尽量少做转型动作 1.const_cast-----脱离常量属性,static_cast(隐式转换显示化),dynamic_cast(从一个寄放派生类的基类指针或引用调用派生类的成分),reinterpret_cats低