Effective C++ (笔记) : 条款11 -- 条款17

来源:互联网 发布:linux search命令 编辑:程序博客网 时间:2024/05/16 02:00

条款11:在operator=中处理“自我赋值”

自我赋值有时候不是那么明显,在处理循环(a[i] = a[j])、指针(*px = *py)和参数传递(func(const Base &rb, const Derived *pd))的时候可能会发生。

  1. Widget::operator=(const Widget& rhs)
  2. {
  3. delete pb;
  4. pb = new Bitmap(*rhs.pb);
  5. return *this;
  6. }

这是一份不安全的实现版本,在自赋值的情况下会形成野指针。

第一种方式:

  1. Widget::operator=(const Widget& rhs)
  2. {
  3. if(this == &rhs) return *this;
  4. delete pb;
  5. pb = new Bitmap(*rhs.pb);
  6. return *this;
  7. }

这种方式具备“自我赋值安全”,但不具备异常安全:当new Bitmap抛出异常("不管是内存不足还是Bitmap的拷贝构造函数异常"),Widget最终会有一个指针指向被删除的Bitmap

第二种方式:

  1. Widget::operator=(const Widget& rhs)
  2. {
  3. Bitmap * pOrig = pb;
  4. pb = new Bitmap(*rhs.pb);
  5. delete pOrig;
  6. return *this;
  7. }

第三种方式:

  1. void swap(Widget& rhs); //详见条款29
  2. Widget::operator=(const Widget& rhs)
  3. {
  4. Widget temp(rhs);
  5. swap(temp);
  6. return *this;
  7. }

确保当对象自我赋值时 operator= 有良好的行为,其中技术包括:比较来源对象和目标对象的地址、精心周到的语句顺序和copy-and-swap

条款12:复制对象时勿忘其每一个成分

在处理派生类的构造函数和拷贝构造函数时除了要考虑派生类自身的局部变量以外,还要保证基类的成员变量(通过构造函数或者拷贝构造函数)能够正确拷贝。

如果只是考虑了局部变量,派生类的构造函数或者拷贝构造函数没有传递参数给基类,因此基类部分会被基类默认构造函数缺省默认初始化(一定存在默认构造函数,否则不能通过编译)。通常这不是我们期望发生的,也就是说,错误发生了。

解决方案也很直接:让派生类的构造函数、拷贝构造函数或者赋值运算符除完成局部变量复制以外一定要调用基类对应的函数完成基类部分的赋值。传递给基类对应函数的参数会发生切割。

如果这些函数中间有相同的部分,不要尝试互相调用,而是添加一个私有的init工具函数。

条款13:以对象管理资源

假设我们获取了一种资源,使用指针指向了它,那么最后释放资源的时候会使用delete释放掉这个资源。问题是,我们的函数不一定执行到释放语句,无论delete如何被略过去,我们泄露的不只是内含投资对象的那块内存,还包括对象所包含的资源。这也就发生了资源泄露,申请资源之后没有将资源归还回系统。正因为如此,单纯的依靠函数f在最后总是会执行其delete语句是不可行的。

为了确保指针指向对象的资源总是能被释放,我们需要将资源放进对象内,当控制流离开函数的时候,该对象的析构函数会自动释放这些资源。

因此在函数内,使用智能指针(是个类指针对象)指向这个对象,当控制流离开时,调用智能指针的析构函数,析构函数会自动对其所指对象调用delete。这里有两个关键想法:

  • 获得资源后立刻放进管理对象内。这也就是RAII(Resource Acquisition Is Initialization) - 资源取得的时机便是初始化时机。
  • 管理对象运用析构函数确保资源被释放。不论控制流如何离开函数,都会析构对象从而释放资源,如果可能抛出异常,问题就有点棘手,但是条款08已经给出一种方案。

毙掉你那想自己手动管理资源(delete ptr)的想法吧,借助对象,借助析构函数的隐式调用delete。因为这比你愚蠢的犯错误要明智的多。

条款14:在资源管理类中小心copying行为

除了上述条款涉及到的智能指针类,我们有时候也需要建立自己的资源管理类。

比如,建立自己的互斥器管理类,有两个函数供使用:

  1. void lock(Mutex* pm); //锁定pm所指的互斥器
  2. void unlock(Mutex* pm); //解除锁定

我们构建下面的类来管理资源:

  1. class Lock{
  2. public:
  3. explicit Lock(Mutex* pm):mutexPtr(pm)
  4. { lock(mutexPtr); }
  5. ~Lock() { unlock(mutexPtr); }
  6. private:
  7. Mutex *mutexPtr;
  8. };
Mutex m;    Lock m1(&m);

这样使用起来是没有问题的,但是如果Lock对象被复制,那会是怎样呢?

Lock m2(m1);

"当一个RAII对象被复制,会发生什么?", 这是一个不得不回答的问题,通常你会根据需要选择下面的一种:

  • 禁止复制。还记得前面怎么说过的吗?将copying(包含拷贝构造和赋值构造)声明为私有的。还有要使用私有基类,记得吗?
  • 对底层资源使用“引用计数法”。希望保有资源,直到它的最后一个使用者被销毁。应该使用引用计数,shared_ptr就是这样。通常只要内含一个shared_ptr成员变量便可以实现这种行为。当然,针对这个智能指针,我们不应该使用它默认的释放动作(delete它所指对象),而应该定制释放动作。
  1. class Lock{
  2. public:
  3. explicit Lock(Mutex* pm):mutexPtr(pm, unlock)
  4. { lock(mutexPtr.get()); //这里需要原始资源的访问}
  5. private:
  6. shared_ptr<Mutex> mutexPtr;
  7. };

上面不再需要析构函数,编译器会生成。析构函数会调用non-static成员变量的析构函数--也就是智能指针的释放动作(也是在智能指针类的析构函数中调用,本来应该调用delete,但是我们重新定制了)。

  • 复制底部资源。有的时候你就是那么任性, 复制一次你就想生成一份副本,那么请记住,你需要资源管理类的唯一理由就是当年不再需要某个复件时确保它被释放。那么你也应该复制底部的资源,也就是常说的深拷贝
  • 转移底部资源的拥有权。有时候只想让一个对象拥有控制权,那么发生拷贝的时候就转移权限吧。

条款15:在资源管理类中提供对原始资源的访问

资源管理类真的很棒。但是很多的API接口直接指涉资源,所以你在资源管理类中也要能处理这种情况。

这个时候你需要一个函数可将RAII class对象(比如智能指针)转换为其所内含之原始指针。有两个方法可以实现:显示转换和隐式转换。

直白点说,显示转换就是提供一个get()函数,让用户显示的调用,就行shared_ptr那样。隐式转换就是operator FontHandle() const { return handle; },这样就能这样调用func(f)就可以。其中f是对象。

条款16:成对使用 new 和 delete 时要采用相同形式

delete的最大问题在于:即将被删除的内存之内究竟有多少对象?这个问题的答案决定有多少个析构函数必须被调用起来。也就是说,我们要删除的指针指向的是单一对象还是对象数组。数组所用的内存通常包括“数组大小”的记录,以便delete时知道要调用多少次析构函数。然而必须由你显示来告诉delete是否存在这条记录。

声明的是数组,释放的时候就是用delete [],否则就是delete不能乱掉

条款17:以独立语句将 newed 对象置于智能指针

有两个函数:

  1. int priority();
  2. void processWidget(shared_ptr<Widget> pw, int priority);

这样调用:

processWidget(new Widget, priority());

不能通过编译,智能指针构造函数需要一个原始指针,但它是个explicit,不能隐式调用。这样就可以:

processWidget(shared_ptr<Widget>(new Widget), priority());

但是这样子还是会有问题,造成资源泄露,因为这两个参数需要3个过程才能生成:

  • 调用priority()
  • 执行new Widget
  • 调用shared_ptr构造函数

这三个步骤的顺序是不确定的(2会早于3,因为要使用生成的对象作为构造函数的参数),当prioruty()执行在另外两者中间并且导致异常,那么new Widget返回的指针将会遗失。造成资源泄露。

解决的方式也很简单:使用分离语句,分别写出(1)创建Widget(2)将它置于智能指针内,然后才传递给函数。

  1. shared_ptr<Widget> pw(new Widget);
  2. processWidget(pw, priority());
0 0
原创粉丝点击