《Effective C++》读书笔记(4)
2 构造/析构/赋值运算
条款10:令 operator= 返回一个 reference to *this
- 令赋值(assignment)操作符返回一个 reference to *this。
有以下两个主要原因:
- 允许连锁赋值。比如说,
int x, y, z; x = y = z;
- 这个协议被普遍的遵守(包括标准库中的很多类型),为了保持类的行为与其他类一致性。
条款11:在operator=中处理“自我赋值”
- 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为为仍然正确。
一个对象可能被自我赋值,比如:1
2
3Widget w[];
// some operation
w[i] = w[j]; //w[i]和w[j]指向同一个对象
可以在operator= 操作中做一下简单的指针比较来避免不必要的操作
1 | Widget& Widget::operator=(const Widget& rhs) { |
当你需要在赋值操作中做一些复杂的控制逻辑,你不仅需要保证起“自我赋值安全性”还要保证“异常安全性”。这个会在条款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_ptr
和auto_ptr
(C++ 11 中有std::shared_ptr
和std::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 | class Font { |
隐式转换
1 | class Font { |
虽然两种方式都可行,但个人认为显式的这种方式更好一些。因为这个可以避免很多不小心进行的转换,只有在需要的时候进行显式的调用。
条款16:成对使用new和delete时要采用相同形式
如果你在new表达式中使用[],必须在相对应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相对应的delete表达式中使用[]
条款17:以独立语句将newed对象置入智能指针
以独立语句将newed对象存储于智能指针内。如果不这么做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
例子:
1 | int priority(); |
为什么第一个例子有问题?因为在C++编译器生成代码的时候有可能会出现以下这种情况:
- 执行”new Widget”
- 调用priority()
- 调用std::shared_ptr构造函数
如果第二部抛出异常,那么第一步申请出来的资源还未放入智能指针内,所以没有人去释放它。于是就造成了资源泄露。