现在的位置: 首页 > 综合 > 正文

Effective C++学习笔记之第五章(2)

2017年11月21日 ⁄ 综合 ⁄ 共 4518字 ⁄ 字号 评论关闭

chapter 5 实现

item28:避免返回handles指向对象的内部成分
1)避免返回handles(指针、引用和迭代器)指向对象的内部成分,有利于加强封装性,保证const成员函数表现得像个const,减低创建一个悬吊指针的可能。下面从三个方面来说。
2)如果class的成员变量本来应该是private,其它人没有访问权,但是如果这里有一个public的函数来返回它的指针,那么这个变量跟public的有什么区别呢?所以这就破坏了类的封装性了。
3)比如说有一个矩形的类,用左上角和右下角的点来表示这个矩形。

class Point {  // class for representing points
public:
  Point(int x, int y);
  ...
  void setX(int newVal);
  void setY(int newVal);
  ...
};
struct RectData { // Point data for a Rectangle
  Point ulhc;     // ulhc = " upper left-hand corner"
  Point lrhc;      // lrhc = " lower right-hand corner"

};
class Rectangle {
  ...
private: // see Item 13 for info on tr1::shared_ptr
  std::tr1::shared_ptr<RectData> pData; 
}; 
//可能用户需要知道矩形的大小,OK,下面两个函数来提供这个接口
class Rectangle {
public:
  ...
  Point& upperLeft() const { return pData->ulhc; }
  Point& lowerRight() const { return pData->lrhc; }
  ...
};
//但是假设用户像下面这样用了呢?
Point coord1(0, 0);
Point coord2(100, 100);
// rec is a const rectangle from (0, 0) to (100, 100)
const Rectangle rec(coord1, coord2); 
// now rec goes from (50, 0) to (100, 100)!
rec.upperLeft().setX(50); 
//rec是const,但是却被改变了。因为upperLeft返回了指针。OK,很明显的一个解决办法
class Rectangle {
public:
  ...
//在返回的指针前面加上const就不会被改变了
  const Point& upperLeft() const { return pData->ulhc; }
  const Point& lowerRight() const { return pData->lrhc; }
  ...
};

4)但是这样有可能导致悬挂指针,比如说这里有一个返回GUI边界框的方法

class GUIObject { ... };
//returns a rectangle by value; see Item 3 for why return type is const
const Rectangle boundingBox(const GUIObject& obj);
//用户可能就这样调用了
GUIObject *pgo; // make pgo point to some GUIObject
... 
// get a ptr to the upper left point of its bounding box
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());

哦哦~boundingBox返回的是值,所以返回的时候有一个临时的变量,这里调用upperLeft返回的正是这个临时变量的内部成员的指针,但是这个语句结束之后,这个临时变量就会被销毁,pUpperLeft就成了一个悬吊指针。
5)当然不是说在什么情况下都不能让成员函数返回一个handles。比如说operator[]就允许返回string或者vector中的单个元素的引用,因为单个元素被销毁之时也是整个容器被销毁之时。当然这只是特例啦~

item29:为异常安全而努力是值得的

1)考虑用一个类来代表GUI菜单的背景图片,用于单线程的环境,所以需要一个mutex来实现并发控制
class PrettyMenu {
public:
  ...
  void changeBackground(std::istream& imgSrc); // change background image
  ...   
private:
  Mutex mutex;     // mutex for this object 
  Image *bgImage;   // current background image
  int imageChanges; // # of times image has been changed
};
//changeBackground的一种可能的实现方式
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
  lock(&mutex);                 // acquire mutex (as in Item 14)
  delete bgImage;               // get rid of old background
  ++imageChanges;              // update image change count
  bgImage = new Image(imgSrc);  // install new background
  unlock(&mutex);               // release mutex
}

上述代码有两个问题,一是资源泄露,如果在"new Image(imgSrc)"的时候抛出一个异常,那么unlock就永远不会被执行,mutex就一直处于lock状态。二是如果"new Image(imgSrc)"的时候抛出一个异常,那么bgImage会指向一个已经被delete的对象,并且这里没有做任何背景的改变,但是背景改变的计数器已经加1了,这样可能用户还认为操作成功了呢!
2)解决的办法。首先用一个智能指针来管理资源,这里用tr1::shared_ptr,因为它的copy动作比auto_ptr更符合直觉一点。然后把changeBackground里面代码的执行顺序调整一下就可以了。这也提醒了我们,在某件事情确实发生之前,不要去改变预示着这件事情已经发生的那个变量。

class PrettyMenu {
  ...
  std::tr1::shared_ptr<Image> bgImage;
  ...
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
//mutex是item14?设计的一个类,他的析构函数不是delete,而是unlock
  Lock ml(&mutex);
//replace bgImage's internal pointer with the result of the "new Image" expression
  bgImage.reset(new Image(imgSrc));
  ++imageChanges;
}

有了智能指针,就不需要人工的delete,并且只能在new成功之后才会进入函数。旧资源的释放是在reset函数内部进行的,所以不用担心了,并且这样还削减了代码长度,代码当然是越短越好啦~越短抛出异常的可能就越小~
3)说说异常安全函数可以提供的三种保证。
基本的保证:也就是如果有异常抛出,保证程序中所有的变量都处于有效的状态,也就是没有变量被破坏,并且所有类中的变量也都是安全的。但是在这种情况下,可能异常抛出过后,没办法判定那些变量的状态。比如说换背景图片,如果更换失败,那么原来的背景图片的指针是指向更换之前的那个图片呢,还是系统设置一个默认的图片,只要更换失败,就换成那个图片。这需要调用某些函数还获得这个信息。
强烈保证:也就是说当有异常抛出的时候,保证所有的变量都不变。也就是函数结束之后,要么是我们达到预想的状态,要么就是调用函数之前的状态。
不抛出异常的保证:也就是说保证这个函数一定不会抛出异常。如果只是对内置类型操作,那是肯定能保证的。
一般来说,会想提供最强烈的保证。从异常安全性的观点来看,nothrow函数很好,但很难在C++的C部分找到完全没有调用任何一个可能抛出异常的函数的函数。任何使用动态内存的东西,如果申请内存失败(没有足够的空间),都会抛出一个bad-alloc。
4)上面改进过后的代码虽然可以提供强烈的安全保证,但还是有美中不足的地方。如果说Image的构造函数抛出一个异常,那么有可能输入流的读取标记已经被移走了,这种搬移对程序的其余的部分是一种可见的状态改变。(这句话怎么听,怎么别扭,没搞明白啊。难道是说抛出异常后还是有可能改变数据?)
5)copy and swap策略。先copy一个打算修改的对象的副本,在副本上做想做的修改。如果在修改的过程中抛出异常,那么原来的对象保持不变。待所有的改变成功之后,再将修改过的副本和原对象在一个不抛出异常的操作中做置换。

//为什么要用结构体,因为更方便,并且将pImpl声明为private,保证了封装性
struct PMImpl {  // PMImpl = "PrettyMenu Impl." 
  std::tr1::shared_ptr<Image> bgImage; 
  int imageChanges; 
};
class PrettyMenu {
  ...
private:
  Mutex mutex;
  std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
  using std::swap;                            // see Item 25
  Lock ml(&mutex);                            // acquire the mutex
  std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));  // copy obj. data   
  pNew->bgImage.reset(new Image(imgSrc));     // modify the copy
  ++pNew->imageChanges;
  swap(pImpl, pNew);  // swap the new data into place
}
//即使用了copy and swap也不一定提供强烈保证 
void someFunc()
{
  ...    // make copy of local state
  f1();
  f2();
  ...    // swap modified state into place
}

6)当然强烈保证不是对于每个函数都实用的。上面的someFunc,如果f1或者f2中有任何一个提供了基本保证,那someFunc很难提供强烈保证。当然如果f1和f2都提供的是强烈保证,情况也不会好转。比如在f2中抛出一个异常,f2是没有对数据进行修改,但f1修改了啊!这里有异常抛出,按理说是不能修改数据的。所以someFunc只能提供基本保证。如果努力之后仍然只能提供基本保证,那就选择它吧,毕竟曾经努力过,没人会怪你的。
7)一个函数可以提供的安全保证一定不会强于它所调用的函数中最弱的安全保证,这就相当于水桶效应。

 

抱歉!评论已关闭.