Shuffy

不断的学习,不断的思考,才能不断的进步.Let's do better together!
posts - 102, comments - 43, trackbacks - 0, articles - 19
【转】http://www.cppblog.com/tiandejian/archive/2007/09/23/ec_28.html

第28条:     不要返回指向对象内部部件的“句柄”

假设你正在设计一个与矩形相关的应用程序。每个矩形的区域都由它的左上角和右下角的坐标来表示。为了 Rectangle 对象尽可能的小巧,你可能会做出这样的决定: Rectangle 自身并不保存这些点的坐标的信息,取而代之的是将这些信息保存在一个辅助结构中,然后让 Rectangle 指向它:

class Point {                   // 表示点的类

public:

 Point(int x, int y);

 ...

 

 void setX(int newVal);

 void setY(int newVal);

 ...

};

 

struct RectData {               // Rectangle 类使用的点的数据

 Point ulhc;                    // ulhc = " 左上角点的坐标

 Point lrhc;                    // lrhc = " 右下角点的坐标 "

};

 

class Rectangle {

 ...

 

private:

 std::tr1::shared_ptr<RectData> pData;

                                // 关于 tr1::shared_ptr 请参见第 13

};

因为 Rectangle 的客户端程序员可能需要了解矩形的区域,所以这个类就应该提供 upperLeft lowerRight 函数。然而, Point 却是一个用户自定义的类型,因此你可能会回忆起第 20 条的经验:通过引用传递用户自定义类型的对象要比直接传值更高效,这些函数可以返回引用来指向更底层的 Point 对象:

class Rectangle {

public:

 ...

 Point& upperLeft() const { return pData->ulhc; }

 Point& lowerRight() const { return pData->lrhc; }

 ...

};

这样的设计可以通过编译,但是它却是错误的。实际上,它是自我矛盾的。另外,由于 upperLeft lowerRight 的设计初衷仅仅是为客户端程序员提供一个途径来了解 Rectangle 的两个顶点坐标在哪里,而不是让客户端程序员去修改它,因此这两个函数应声明为 const 成员函数。另外,这两个函数都返回指向私有内部数据的引用——通过这些引用,调用者可以任意修改内部数据!请看下边的示例:

Point coord1(0, 0);

Point coord2(100, 100);

 

const Rectangle rec(coord1, coord2);// rec 是一个 Rectangle 常量

                                    // 两顶点是 (0, 0), (100, 100)

 

rec.upperLeft().setX(50);            // 但现在 rec 的两顶点却变为

                                    // (50, 0), (100, 100)!

upperLeft 返回了 rec 内部的 Point 数据成员,在这里请注意:虽然 rec 本身应该是 const 的,但是调用者竟可以使用 upperLeft 所返回的引用来修改这个数据成员!

上面的现象立刻引出了两个议题:首先,数据成员仅仅与访问限制最为宽泛的函数拥有同等的封装性。在这种情况下,即使 ulhc lrhc 声明为私有的,它们实际上仍然是公共的,这是因为公共函数 upperLeft lowerRight 返回了指向它们的引用。其次,如果一个 const 成员函数返回一个引用,这一引用指向的数据与一个对象相关,但这一数据却保存在该对象以外,那么函数的调用者就可以修改这一数据。(这样恰巧超出了按位恒定的范畴——参见第 3 条。)

我们所做的一切都与返回引用的成员函数有关,但是如果它们返回的是指针或者迭代器,同样的问题仍然会因为同样的理由发生。引用、指针、迭代器都可以称作“句柄”(获取其它对象的渠道),返回一个指向对象内部部件的句柄,通常都会危及到对象的封装性。就像我们看到的,即使成员函数是 const 的,返回对象的状态也是可以任意更改的。

大体上讲,对象的“内部部件”主要是它的数据成员,但是非公用的成员函数同样也是对象的内部部件。与数据成员相同,返回指向成员函数的句柄也是糟糕的设计。这意味着你不应该让一个公用成员函数 A 返回一个指向非公用成员函数 B 的指针。如果你这样做了, B 的访问权层次就与 A 一样了,这是因为客户端程序员将能够取得 B 的指针,然后通过这一指针来调用它。

索性的是,返回指向成员函数指针的函数并不常见,所以让我们还是把精力放在 Rectangle 类和他的 upperLeft lowerRight 成员函数上来吧。我们所发现的关于这些函数所存在的两个问题都可以简单的解决,只要将它们的返回值限定为 const 的就可以了:

class Rectangle {

public:

 ...

 const Point& upperLeft() const { return pData->ulhc; }

 const Point& lowerRight() const { return pData->lrhc; }

 ...

};

使用这一改进的设计方案,客户端程序员就可以读取用来定义一个矩形的两个点,但是他们不可以修改这两个点。这就意味着将 upperLeft lowerRight 声明为 const 的并不是一个假象,因为它们将不允许调用者来修改对象的状态。至于封装问题,我们一直坚持让客户端程序员能能够看到构造一个 Rectangle 的两个 Point ,所以说这里我们故意放松了封装的限制。更重要的是,这一放松是有限的:这些函数仅仅提供了读的访问权限。写权限仍然是禁止的。

class GUIObject { ... };

 

const Rectangle boundingBox(const GUIObject& obj); 

// 以传值方式返回一个矩形。关于返回值为什么是 const 的,请参见第 3

现在请考虑一下客户端程序员可能怎样来使用这个函数:

GUIObject *pgo;                 // pgo 指向某个 GUIObject

 

const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());

// 取得一个指向 boundingBox 左上角点的指针

调用 boundingBox 将会返回一个新的、临时的 Retangle 对象。这个对象没有名字,所以姑且叫它 temp 。随后 temp 将调用 upperLeft ,然后此次调用将返回一个指向 temp 内部部件的引用,特别地,指向构造 temp 的一个点。 pUpperleft 将会指向这一 Point 对象。到目前为止一切都很完美,但是任务尚未完成,因为在这一语句的最后, boundingBox 的返回值—— temp ——将会被销毁,这样间接上会导致 temp Point 被销毁掉。于是, pUpperLeft 将会指向一个并不存在的对象。这条语句创建了 pUpperLeft ,可也让它成了孤魂野鬼。

为什么说:任何返回指向对象内部部件句柄的函数都是危险的,这个问题已经一目了然了。至于句柄是指针还是引用还是迭代器,函数是否是 const 的,成员函数返回的句柄本身是不是 const 的,这一切都无关紧要。只有一点,那就是:只要返回了一个句柄,那么就意味着你正在承担风险:它可能会比它指向的对象存活更长的时间。

这并不意味着你永远也不能让一个成员函数返回一个句柄。有些时候你不得不这样做。比如说, operator[] 允许你获取 string vector 中的任一元素,这些 operator[] 的工作就是通过返回容器内部的数据来完成的(参见第 3 条)——当容器本身被销毁时,这些数据同时也会被销毁。然而,这仅仅是一个例外,不是惯例。

铭记在心

避免返回指向对象内部部件的句柄(引用、指针或迭代器)。这样做可以增强封装性,帮助 const 成员函数拥有更加“ const ”的行为,并且使“野句柄”出现的几率降至最低。


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理