(一)
有时候为了让一个对象尽量小,可以把数据放在另外一个辅助的struct中,然后再让一个类去指向它。看下面的代码:
class Point { public: Point(int x, int y); void setX(int newVal); void setY(int newVal); }; struct RectData { Point ulhc; Point lrhc; }; class Rectangle { public: Point& upperLeft() const { return pData->ulhc; } Point& lowerRight() const { return pData->lrhc; } private: std::tr1::shared_ptr<RcetData> pData; };
这样的设计看上去很beautiful。但是却是错误的,实际上它是自相矛盾的,看下面的代码:
Point coord1(0, 0); Point coord2(100, 100); const Rectangle rec(coord1, coord2); rec.upperleft().setX(50);
错误的理由:(1)upperLeft()跟lowerRight()这两个函数都是const的,所以客户不能修改Rectangle;
(2)两个函数都返回reference指向private内部数据,调用者于是可通过这些reference更改内部数据。
upperLeft的调用者能够使用被返回的引用来更改成员。但rec其实应该是不可变的(const)!
所以上面那种类的设计是错误的!!!
所以从这个例子中,我们可以得到以下的教训:
(1)成员变量的封装性会被引用破坏。
(2)如果const成员函数传出一个reference,后者所指的数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。
同样的道理,返回对象的引用、指针、迭代器都会造成这种局面,它们都是“句柄”。返回一个代表对象内部数据的句柄,会降低对象的封装。
(二)解决办法:
只要对这两个函数的返回类型加上const即可:
class Rectangle { public: const Point& upperLeft() const { return pData->ulhc; } const Point& lowerRight() const { return pData->lrhc; } private: std::tr1::shared_ptr<RcetData> pData; };
有了这样的改变,客户就只能读取矩形的Points,但是不能涂写它们。
(三)
上面那种解决方法虽然确保了内部对象不会被修改。但是却可能导致dangling handles(空悬的号码牌):这种handles所指东西(的所属对象)不复存在。
这种“不复存在的对象”最常见的来源就是函数返回值。
class GUIObject {...}; const Rectangle boundingBox(const GUIObject& obj); GUIObject* pgo; const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());
你会发现(boundingBox(*pgo).upperLeft())这是一个point对象。但是当这一句执行完后,这个临时对象temp会被析构。这时,pUpperLeft会指向一个空的对象。也就出现了悬空现象。
因此,这就是为什么函数如果“返回一个handle代表对象内部成分“总是危险的原因。
请记住:
(1)避免返回handles指向对象的内部。遵守这个条款可增加封装性,帮助const成员函数更加像一个const,并将“虚号码牌“的可能性降低到最低。
Effective C++:条款28:避免返回 handles 指向对象内部成员