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

reference、指针和迭代器统统都是所谓handles(号码牌,用来取得某个对象)。 

先来看个例子,

假设你的程序涉及矩形,每个矩形由其左上角和右下角表示。

为了让一个Rectangle对象尽可能小,你可能会决定不把定义矩形的这些点存放在Rectangle对象内,而是放在一个辅助的struct内再让Rectangle去指它:

class Point {//这个class用来表示点
public:
	Point(int x, int y);
	void setX(int newVal);
	void setY(int nexVal);
	//...
};
struct RectData {//用来表示矩形
	Point ulhc;//左上角
	Point lrhc;//右下角
};
class Rectangle {
public:
	//...
private:
	std::tr1::shared_ptr<RectData> pData;
};

Rectangle的客户必须能够计算Rectangle的范围,所以这个class提供upperLeft函数和lowerRight函数。Point是个用户自定义类型,所以根据条款20,这些函数返回reference,代表底层的Point对象:

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

这样的设计能够通过编译,但却是错误的。实际上它是自我矛盾的。

upperLeft和lowerRight被声明为const成员函数,因为它们的目的只是为了提供客户一个得知Rectangle相关坐标点的方法,而不是让客户修改Rectangle。但这两个函数却都返回reference指向private内部数据,调用者于是可通过这些reference更改内部数据。

例如,

int main()
{
	Point coord1(0, 0);
	Point coord2(100, 100);
	const Rectangle rec(coord1, coord2);
	rec.upperLeft().setX(50);
	return 0;
}

upperLeft的调用者能够使用被返回的reference(指向rec内部的Point成员变量)来更改成员。但rec实际上应该是不可变的。

 上述讨论可以得出两个结论:

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

在上述代码中,虽然rec中的ulhc和lrhc都被声明为private,它们实际上却是public,因为public函数upperLeft和lowerRight传出了它们的reference。

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

 上述所讨论的每一件事都是由于“成员函数返回reference”。若它们返回的是指针或迭代器,相同的情况还是会发生,原因也相同。

reference、指针和迭代器统统都是所谓handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。同时,它也可能导致“虽然调用const成员函数却造成对象状态被更改”。

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

因此也应该留心不要返回它们的handle。这意味着你绝对不该令成员函数返回一个指针指向“访问级别较低”的成员函数。

若你这么做了,后者的实际访问级别就会提高如同前者,因为客户可以取得一个指针指向那个“访问级别较低”的函数,然后通过那个指针调用它。

上述讨论的两个函数所遇到的问题可以轻松解决,只要对它们的返回类型加上const即可:

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

有了这样的改变,用户可以读取矩形的Point,但不能涂写它们。

但即使如此upperLeft和lowerRight还是返回了“代表对象内部”的handle,有可能在其他场合带来问题。更明确地说,它可能导致了dangling handles(空悬的号码牌):这种handles所指东西(的所属对象)不复存在。这种“不复存在的对象”最常见的来源就是函数返回值。

例如,某个函数返回GUI对象的外框(bounding box),这个外框采用矩形形式:

class GUIObject{...};
const Rectangle boundingBox(const GUIObject& obj);
//以by value的方式返回一个矩形

现在,用户可能这么使用这个函数:

GUIObject* pgo;//让pgo指向某个GUIObject
//...
const Point* pUpperLeft=&(boundingBox(*pgo).upperLeft());
//取得一个指针指向外框左上点

对boungingBox的调用获得一个新的、暂时的Rectangle对象。这个对象没有名称,所以暂且称它为temp。随后upperLeft作用于temp身上,返回一个reference指向temp的一个内部成分,更具体地说是指向一个用以标示temp的Point。于是pUpperLeft指向那个Point对象。到目前为止,都很正常。但在那个语句结束之后,boundingBox的返回值,即temp,将被销毁,而那间接导致temp内的Point析构。最终到导致pUpperLeft指向一个不再存在的对象;也就是说一旦产出pUpperLeft的那个语句结束,pUpperLeft也就变成空悬、虚吊(dangling)。

这就是为什么函数如果“返回一个handle代表对象内部成分”总是危险的原因。不论handle是指针或迭代器或reference,不论这个handle是否为const,也不论返回handle的成员函数是否为const。这里唯一的关键是,有个handle被传出去了,一旦如此,你就是暴露在“handle比其所指对象更长寿”的风险下。

但有时成员函数返回handle是必要的。例如operator[]允许你“摘采”string和vector的个别元素,而这些operator[]就是返回reference指向“容器内的数据”,那些数据会随着容器被销毁而销毁。

总结

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值