Questions:
(1)返回reference的成员函数有什么害处?
(2)如何避免成员函数返回handler?
Answers:
(1)返回reference的成员函数有什么害处?
有个矩形类Rectangle,每个矩形由其左上角和右下角坐标表示,坐标用Point类表示,横坐标和中坐标分别为x、y,下面是代码:
#include <iostream>
#include <memory>
using namespace std;
class Point{
private:
int x, y;
public:
Point():x(0), y(0){};
Point(int x, int y):x(x),y(y){};
void setX(int newVal){
x = newVal;
}
void setY(int newVal){
y = newVal;
}
int getX(){
return x;
}
int getY(){
return y;
}
};
struct RectData{
Point ulhc; //左上角
Point lrhc; //右下角
};
class Rectangle{
public:
Rectangle(){}
Rectangle(Point& p1, Point& p2){
struct RectData *r = (struct RectData*)malloc(sizeof(struct RectData));
r->ulhc = p1;
r->lrhc = p2;
shared_ptr<RectData> rd(r);
pData = rd;
}
shared_ptr<RectData>& getPData() const{
return pData;
}
Point& upperLeft() const{
return pData->ulhc;
}
Point& lowerRight() const{
return pData->lrhc;
}
void Print() const{
cout<<pData->ulhc.getX()<<" "<<pData->ulhc.getY()<<" "<<pData->lrhc.getX()<<" "<<pData->lrhc.getY()<<endl;
}
private:
shared_ptr<RectData> pData;
};
这样的设计是可以通过编译的,然而实际上是错误的!为什么呢?
我们首先来看实际的调用:
int main(int argc, _TCHAR* argv[])
{
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec1(coord1, coord2); //rec1为const变量
rec1.Print();
Point &p = rec1.upperLeft(); //获取rec1的成员变量的成员变量ulhc的引用
p.setX(50); //直接改变rec1的成员变量的值,破坏了封装性,间接改变了const变量的值
rec1.Print();
Rectangle rec2(coord1, coord2);
shared_ptr<RectData> &r = rec2.getPData(); //获取rec2的成员变量的reference
r->ulhc.setX(100); //改变rec2的成员变量的值,破坏了其封装性
rec2.Print();
return 0;
}
以下是运行结果:
一方面,upperLeft和lowerRight被声明为const成员函数,为的是只提供坐标点但不允许客户修改Rectangle,而另一方面呢,两个函数都返回了指向内部private数据的引用pData->ulhc和pData->lrhc,这样调用者就可以通过这些返回的reference来更改内部数据,如上面的main函数所示!所以呢,Rectangle的设计是自相矛盾的!
而且,在main函数中,我们虽然声明了rec1为const常量,但是后面依然可以通过p.setX(50)来改变rec1的值!
所以说:
返回一个“代表对象内部数据”的handle(引用、指针和迭代器都是所谓的handles),随之而来的便士“降低对象的封装性”的风险。同时,可能导致“虽然调用const成员函数确造成对象状态被更改”。
而造成这一切的罪魁祸首就是“返回一个代表对象内部数据的handle”。
(2)如何避免成员函数返回handler?
以上遇到的两个问题很好解决,只要在“返回handle的函数”的返回值前面加上const即可,这样就可以禁止调用者更改返回值所指向的变量!
// Item28Test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <memory>
using namespace std;
class Point{
private:
int x, y;
public:
Point():x(0), y(0){};
Point(int x, int y):x(x),y(y){};
void setX(int newVal){
x = newVal;
}
void setY(int newVal){
y = newVal;
}
int getX(){
return x;
}
int getY(){
return y;
}
};
struct RectData{
Point ulhc; //左上角
Point lrhc; //右下角
};
class Rectangle{
public:
Rectangle(){}
Rectangle(Point& p1, Point& p2){
struct RectData *r = (struct RectData*)malloc(sizeof(struct RectData));
r->ulhc = p1;
r->lrhc = p2;
shared_ptr<RectData> rd(r);
pData = rd;
}
const shared_ptr<RectData>& getPData() const{
return pData;
}
const Point& upperLeft() const{
return pData->ulhc;
}
const Point& lowerRight() const{
return pData->lrhc;
}
void Print() const{
cout<<pData->ulhc.getX()<<" "<<pData->ulhc.getY()<<" "<<pData->lrhc.getX()<<" "<<pData->lrhc.getY()<<endl;
}
private:
shared_ptr<RectData> pData;
};
int main(int argc, _TCHAR* argv[])
{
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec1(coord1, coord2); //rec1为const变量
rec1.Print();
//Point &p = rec1.upperLeft(); //error,返回值是const
//p.setX(50);
//rec1.Print();
Rectangle rec2(coord1, coord2);
//shared_ptr<RectData> &r = rec2.getPData(); //获取rec2的成员变量的reference
//r->ulhc.setX(100); //改变rec2的成员变量的值,破坏了其封装性
//rec2.Print();
return 0;
}
通过这种方式,就可以避免通过p和r来修改rec1和rec2,保护了其封装性和常量性,更加合理!如果想通过upperLeft的返回值来修改rec1的成员pData的值,其编译是通不过的!
我在实现这个例子的时候,遇到了一个奇怪的现象,代码如下:
// Item28Test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <memory>
using namespace std;
class Point{
private:
int x, y;
public:
Point():x(0), y(0){};
Point(int x, int y):x(x),y(y){};
void setX(int newVal){
x = newVal;
}
void setY(int newVal){
y = newVal;
}
int getX(){
return x;
}
int getY(){
return y;
}
};
struct RectData{
Point ulhc; //左上角
Point lrhc; //右下角
};
class Rectangle{
public:
Rectangle(){}
Rectangle(Point& p1, Point& p2){
struct RectData *r = (struct RectData*)malloc(sizeof(struct RectData));
r->ulhc = p1;
r->lrhc = p2;
shared_ptr<RectData> rd(r);
pData = rd;
}
const shared_ptr<RectData>& getPData() const{
return pData;
}
const Point& upperLeft() const{
return pData->ulhc;
}
const Point& lowerRight() const{
return pData->lrhc;
}
void Print() const{
cout<<pData->ulhc.getX()<<" "<<pData->ulhc.getY()<<" "<<pData->lrhc.getX()<<" "<<pData->lrhc.getY()<<endl;
}
private:
shared_ptr<RectData> pData;
};
int main(int argc, _TCHAR* argv[])
{
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec1(coord1, coord2); //rec1为const变量
rec1.Print();
Point p = rec1.upperLeft();
p.setX(50);
rec1.Print();
Rectangle rec2(coord1, coord2);
shared_ptr<RectData> r = rec2.getPData();
r->ulhc.setX(100); //改变rec2的成员变量的值,破坏了其封装性
rec2.Print();
return 0;
}
这个例子跟上个的唯一区别就是接收返回值的变量由引用改成了非引用,同样是改成非引用,为什么第一个不可以改变成员变量,而第二个可以改变呢?
其实原因很简单,在这个例子中,Point p = rec1.upperLeft(),p存放的是upperLeft()的返回值的引用所指向的对象,Point &p = rec1.upperLeft(),p存放的是upperLeft()返回值的引用!所以前者的值只是一个对象副本,后者和返回值指向的同一个对象!
而shared_ptr<RectData> r = rec2.getPData()中,getPData()的返回值是一个智能指针,用这个返回的智能指针(不管是不是引用)初始化r之后,r这个智能指针也同样指向的是rec2的成员变量pData,我们可以通过
cout<<r.use_count();打印出r的引用数量,结果是“2”,这就相当于两个智能指针指向同一个变量pData,引用次数为2,只要通过一个指针改变了pData的值,两一个指针所指向的内容当然也会改变!同样可以验证:假设将rec2.getPData()的返回值用一个智能指针的引用来接收,那么结果会是什么样的呢?
cout<<r.use_count();打印出r的引用数量,结果是“2”,这就相当于两个智能指针指向同一个变量pData,引用次数为2,只要通过一个指针改变了pData的值,两一个指针所指向的内容当然也会改变!同样可以验证:假设将rec2.getPData()的返回值用一个智能指针的引用来接收,那么结果会是什么样的呢?
为了让程序编译通过,我们修改了两个地方,第一个是成员函数getPData()的返回值和常量性,去掉了那两个const,另一个是把接收返回值的变量改成了引用,代码如下:
// Item28Test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <memory>
using namespace std;
class Point{
private:
int x, y;
public:
Point():x(0), y(0){};
Point(int x, int y):x(x),y(y){};
void setX(int newVal){
x = newVal;
}
void setY(int newVal){
y = newVal;
}
int getX(){
return x;
}
int getY(){
return y;
}
};
struct RectData{
Point ulhc; //左上角
Point lrhc; //右下角
};
class Rectangle{
public:
Rectangle(){}
Rectangle(Point& p1, Point& p2){
struct RectData *r = (struct RectData*)malloc(sizeof(struct RectData));
r->ulhc = p1;
r->lrhc = p2;
shared_ptr<RectData> rd(r);
pData = rd;
}
shared_ptr<RectData>& getPData() {
return pData;
}
const Point& upperLeft() const{
return pData->ulhc;
}
const Point& lowerRight() const{
return pData->lrhc;
}
void Print() const{
cout<<pData->ulhc.getX()<<" "<<pData->ulhc.getY()<<" "<<pData->lrhc.getX()<<" "<<pData->lrhc.getY()<<endl;
}
private:
shared_ptr<RectData> pData;
};
int main(int argc, _TCHAR* argv[])
{
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec1(coord1, coord2); //rec1为const变量
rec1.Print();
Point p = rec1.upperLeft();
p.setX(50);
rec1.Print();
Rectangle rec2(coord1, coord2);
shared_ptr<RectData>& r = rec2.getPData();
r->ulhc.setX(100);
rec2.Print();
cout<<r.use_count();
return 0;
}
唯一的不同就是:r.use_count()的返回值是“1”。原因不言而喻!
Remember:
- 避免返回handles指向对象的内部(主要是对象的数据成员有其他类对象)。遵守这个条款可增加封装性(防止通过类的public接口修改对象的值),帮助const成员函数更加像一个const(通过handle间接修改const常量的内部值),并将“虚号码牌“的可能性降低到最低(handle指向一个已经析构了的对象)。