考虑下面这个例子:
class Point{
public:
Point(int x, int y) : m_x(x), m_y(y){}
void setX(int x)
{
m_x = x;
}
void setY(int y)
{
m_y = y;
}
private:
int m_x{ 0 };
int m_y{ 0 };
}
struct RectData{ //表示一个矩形
Point ulhc; //upper left-handl corner
Point lrhc; //lower right-handl corner
}
class Rect{
public:
Rect(){}
Point& upperLeft() const { return pData->ulhc; } //返回左上角的点
Point& lowerRight() const { return pData->lrhc; } //返回右下角的点
private:
stl::t1::shared_ptr<RectData> pData;
}
上面的这个例子中,我们最终的目的是为了定义一个矩形,但是为了使得Rect
对象尽可能的小,我们使用了一个辅助的结构体来保存矩形的某些数据。
为了能够计算得出这个矩形的范围,我们提供了两个函数,这两个函数分别返回矩形对象的左上角和右下角的点。而在这之前,假设我们已经知道了通过 by reference 方式传递的用户自定义类型往往比通过 by value 的方式
要高效。上面的Point类是用户自定义类型。所以也就有了上面的这两个函数。
如果我们仔细看看上面的这两个函数和我们本来应该是要设计的目的的话,我们就能知道,这样的设计虽然可以正常编译,但他其实是自相矛盾的。
我们的目的是为了能够让调用Rect
类的客户能够获取到Rect
的范围,并不希望他能够随便的更改Rect的属性。因此我们才会将该函数设计为const类型。
看下下面的这个使用例子:
Point pt1(0, 0);
Point pt2(100, 50);
const Rect rec(pt1, pt2);
rec.upperLeft().setX(50);
这样使用之后, const 类型的 Rect
对象 rec
的内部数据,也就是左上角的点会被改变。 但他原本的设计是不允许被修改的。
这其实会让我们明白:
- 成员变量的封装性与 “返回其 reference” 的函数的访问级别是一样的。就像上面这个对象,虽然声明了两个点的类型均为 private类型,但他们实际上还是public的。因为public的两个函数返回了他们的引用。
- 如果const传出一个引用, 引用所指的对象与该对象本身关联,而他又被储存于对象之外,那么这个函数的调用者可以随便修改引用所指对象的数据。
可能我们会很容易的想到解决办法。
const Point& upperLeft() const { return pData->ulhc; } //返回左上角的点
const Point& lowerRight() const { return pData->lrhc; } //返回右下角的点
有了上面的这个改变,用户是可以随意读取数据,但是不能随意修改他们的。这个封装中我们让渡了读取权,却没有让渡修改权。
我们看下下面这个使用:
class Dialog{ ... };
const Rect boundingBox(const Dialog& dlg);
上面这个函数提供一个返回对话框的外边框的功能。
那么下面这个调用:
Dialog dlg;
const Point* pt = &(boundingBox(dlg).upperLeft());
上面这个调用中,调用 boundingBox
之后产生了一个新的Rect
对象,这个对象在这个调用中没有名称,我们暂时称作为 temp
。随后,temp
调用了 upperLeft
函数返回了Rect
对象的左上角点。当然这个过程是没有问题的。
但是当这行代码执行完成了之后,因为 temp
是一个临时变量,他会被释放,他被析构的同时,他自己内部的 Point 对象也会跟着被释放。
那样会发生什么事情?
最终会导致我们定义的对象 pt
指向一个不存在的对象,程序崩溃。也就是说一旦上面的语句执行完成, pt
也就指向了一个不存在的对象,空悬、虚吊。
从上面来看,如果我们返回了一个类内部成员的句柄之后,就可能存在一下不可避免的风险。无论返回的是指针,或者是引用,也不管返回的是不是const
类型。都会有这种风险存在。
当然并不是任何时候我们都不能这样做,比如 operator[] 操作符,返回的就是数组或者容器中的某个元素的引用。这个引用指向容器中的某些数据。而这些数据会跟着容器的销毁被销毁。
因此:避免返回类内部成员的句柄,可增加类的封装性。降低指针或对象被空悬的风险。