文章目录
  1. 1. 2 构造/析构/赋值运算
    1. 1.1. 条款10:令 operator= 返回一个 reference to *this
    2. 1.2. 条款11:在operator=中处理“自我赋值”
    3. 1.3. 条款12:复制对象时勿忘其每一个成分
  2. 2. 3. 资源管理
    1. 2.1. 条款13: 以对象管理资源
    2. 2.2. 条款14:在资源管理类中小心copying行为
    3. 2.3. 条款15:在资源管理类中提供对原始资源的访问
    4. 2.4. 条款16:成对使用new和delete时要采用相同形式
    5. 2.5. 条款17:以独立语句将newed对象置入智能指针

2 构造/析构/赋值运算

条款10:令 operator= 返回一个 reference to *this

  • 令赋值(assignment)操作符返回一个 reference to *this。

有以下两个主要原因:

  1. 允许连锁赋值。比如说,int x, y, z; x = y = z;
  2. 这个协议被普遍的遵守(包括标准库中的很多类型),为了保持类的行为与其他类一致性。

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

  • 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为为仍然正确。

一个对象可能被自我赋值,比如:

1
2
3
Widget w[];
// some operation
w[i] = w[j]; //w[i]和w[j]指向同一个对象

可以在operator= 操作中做一下简单的指针比较来避免不必要的操作

1
2
3
4
Widget& Widget::operator=(const Widget& rhs) {
if (this == &rhs) return *this;
...
}

当你需要在赋值操作中做一些复杂的控制逻辑,你不仅需要保证起“自我赋值安全性”还要保证“异常安全性”。这个会在条款xx中具体进行说明。

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

  • Copying 函数应该确保复制“对象内的所有成员变量”及“所有base class成分”
  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

copy构造函数和copy assignment操作符我们统称为copying函数。当我们自己定义copying务必要确保所有的字段都进行了复制,同事如果有基类要保证相对应的copying函数被正确得调用。

作者认为虽然说copy构造函数和copy assignment操作符做的操作绝大多数是相同的,但是不应该尝试以某一个copying函数实现调用另一个copying函数。这样的操作并不合理。

3. 资源管理

条款13: 以对象管理资源

  • 为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的RAII classes分别是tr1::shared_ptrauto_ptr(C++ 11 中有std::shared_ptrstd::auto_ptr)

内存泄漏和资源泄露很多情况都是因为一个内存/资源分配在堆上,被用于一个函数内,然后在控制流离开函数的时候没有得到正确的释放。对于这种情况引起的资源泄露,可以把资源包装成对象来实现合理的释放。

“以对象管理资源” 通常也被称为”资源取得时机便是初始化时机”(Resource Acquisition Is Initialization: RAII)。RAII的两个核心思想:

  • 获得资源后立刻放进管理对象内。
  • 管理对象运用析构函数确保资源得到释放。

我们可以根据自己的需求定制RAII类,同时也可以使用系统提供的智能指针可以用来作为管理对象。

  • auto_ptr - auto_ptr被销毁时会自动删除它所指之物。它的缺点是,如果多个auto_ptr指向同一个对象,那么那个对象会被删除一次以上。另外通过copy构造函数或copy assignment操作符复制它们的时候,它们会变成null。
  • shared_ptr - shared_ptr是一个引用计数原理的智能指针。它能够持续追踪有多少个对象指向某个资源,而且它的复制行为也看上去正常许多。它的缺点是,它无法打破循环引用的问题。

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

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  • 普遍而常见的RAII class copying行为是:禁止copying、施行引用计数法(reference counting)。不过其他行为也都可能被实现。

当我们定制RAII对象的时候,需要根据具体的需求来控制其拷贝的行为。不然可能会出现一些“不愉快”的事情。

拷贝的行为基本有下面几种:

  • 禁止复制。 很多情况对RAII对象进行复制是不合理的,比如说一个Lock类,保存着一个锁。合理的行为是禁止这个类型的复制行为。

  • 对底层资源祭出“引用计数法”。 如果我们希望保有资源,知道它的最后一个使用者被销毁。这种情况我们就应该在内部使用一个引用计数来管理资源,我们可以用shared_ptr来做引用计数。

  • 复制底部资源。 拷贝时将内部的资源进行一个完整的深层拷贝。比如说,一个字符串被复制的时候,它不仅要复制指向内存中字符串的指针,同时也应该复制对应的字符串内存。

  • 转移底层资源的拥有权 在一些比较特殊的场合可能希望只有一个RAII对象指向一个资源,当被复制的时候,资源的拥有权将会进行转移。这个行为跟auto_ptr的行为是一样的。

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

  • APIs 往往要求访问原始资源(raw resources),所以每一个RAII classes应该提供一个“取得其所管理之资源”的办法。
  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显示转换比较安全,但是隐式转换对客户比较方便。

在一些情况下我们的需要访问资源管理类中的原始资源,这个时候我们就应该提供一个能够获得原始资源的方法。

这个看上去破坏了封装性,但是资源管理类设计的初衷是为了更好地管理资源的申请和释放,并非对资源进行完全的封装。所以提供获取原始资源的方法也还是比较合理的做法。

可以通过显式和隐式转换两种方法。

显式转换

1
2
3
4
5
6
class Font {
public:
...
FontHandle get() const { return f; }
...
}

隐式转换

1
2
3
4
5
6
class Font {
public:
...
operator FontHandle() const { return f; }
...
}

虽然两种方式都可行,但个人认为显式的这种方式更好一些。因为这个可以避免很多不小心进行的转换,只有在需要的时候进行显式的调用。

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

如果你在new表达式中使用[],必须在相对应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相对应的delete表达式中使用[]

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

以独立语句将newed对象存储于智能指针内。如果不这么做,一旦异常被抛出,有可能导致难以察觉的资源泄露。

例子:

1
2
3
4
5
6
7
8
9
int priority();
void processWidget(std::shared_ptr<Widget> pw, int p);

// Bad
processWidget(std::shared_ptr<Widget>(new Widget), priority());

// Good
std::shared_ptr<Widget> pw(new Widget)
processWidget(pw, priority());

为什么第一个例子有问题?因为在C++编译器生成代码的时候有可能会出现以下这种情况:

  1. 执行”new Widget”
  2. 调用priority()
  3. 调用std::shared_ptr构造函数

如果第二部抛出异常,那么第一步申请出来的资源还未放入智能指针内,所以没有人去释放它。于是就造成了资源泄露。

文章目录
  1. 1. 2 构造/析构/赋值运算
    1. 1.1. 条款10:令 operator= 返回一个 reference to *this
    2. 1.2. 条款11:在operator=中处理“自我赋值”
    3. 1.3. 条款12:复制对象时勿忘其每一个成分
  2. 2. 3. 资源管理
    1. 2.1. 条款13: 以对象管理资源
    2. 2.2. 条款14:在资源管理类中小心copying行为
    3. 2.3. 条款15:在资源管理类中提供对原始资源的访问
    4. 2.4. 条款16:成对使用new和delete时要采用相同形式
    5. 2.5. 条款17:以独立语句将newed对象置入智能指针