条款 11 - 条款 20 | 《Effictive C++》笔记
Effictive C++ 条款 11 - 条款 20。
条款 11:在 operator=
中处理“自我赋值”
确保当对象自我赋值时 operator= 有良好行为,应当保证有自我赋值安全性和异常安全性。
自我赋值不安全,异常不安全的实现(pb 会指向一个被删除的对象):
1 | Widget& Widget::operator=(const Widget& rhs) { |
自我赋值安全,异常不安全的实现(如果构建 Bitmap 失败,则 pb 可能会是一个 nullptr):
1 | Widget& Widget::operator=(const Widget& rhs) { |
自我赋值安全,异常安全的实现(如果 new Bitmap 抛出异常,pb 保持原状):
1 | Widget& Widget::operator=(const Widget& rhs) { |
自我赋值安全,异常安全的实现(让 operator= 具备异常安全性往往自动获得自我赋值安全性):
1 | Widget& Widget::operator=(const Widget& rhs) { |
条款 12:复制对象时勿忘其每一个成分
- 对于基类中的 private 成员,派生类无法访问,所以在复制派生类时,应该让派生类的拷贝构造函数调用相应的基类构造函数
1
2
3
4
5DerivedClass::DerivedClass(const DerivedClass& rhs)
: BaseClass(rhs), // 调用基类的拷贝构造函数
member(rhs.member) {
// do something
} - 不要尝试以某个拷贝函数实现另一个拷贝函数,应该将共同功能放进第三个函数中,并由两个拷贝函数共同调用
条款 13:以对象管理资源
- 资源取得时机便是初始化时机(Resource Acquisition Is Initialization, RAII),即初始化一个对象后立即放进管理对象中
1
std:share_ptr<Instance> pIns(createInstance());
- 依赖析构函数确保资源被释放
条款 14:在资源管理类中小心 copying 行为
当一个 RAII 对象被复制时,大多数情况下有两种选择:
- 禁止复制:许多时候允许 RAII 对象复制并不合理(如锁),将拷贝构造函数和拷贝赋值操作符声明为 private 并且不予实现
- 使用引用计数法
条款 15:在资源管理类中提供对原始资源的访问
- APIs 往往要求访问原始资源,所以每一个 RAII 类应该提供一个“取得其所管理之资源”的办法
- 对原始资源的访问可能经由显式转换或隐式转换
- 一般而言显式转换比较安全:保证每个原始资源都有对象管理,不至于产生野指针(比如资源管理类 A 管理原始资源 B,如果使用隐式转换,则容易误用
B b = a
使 b 指向 a 中的原始资源,当 a 析构时其管理的原始资源也被释放,导致 b 成为虚吊的) - 但隐式转换对客户比较方便:不需要每次都要显式地调用函数
- 一般而言显式转换比较安全:保证每个原始资源都有对象管理,不至于产生野指针(比如资源管理类 A 管理原始资源 B,如果使用隐式转换,则容易误用
条款 16:成对使用 new
和 delete
时要来取相同形式
- delete 和 delete[] 的区别在于 delete 会将 new 时申请的空间释放(new 时记录了分配空间的大小),但是只会调用第一个元素的析构函数;而 delete[] 则不仅会将 new 时申请的空间释放,还会调用所有元素的析构函数
- 所以当 new 一个数组的时候,对于基本数据类型 delete 和 delete[] 都能完成内存的释放;而对于自定义数据类型,delete 只会调用第一个对象的析构函数,造成了资源泄漏问题
- 如果在 new 表达式中使用 [],在相应的 delete 表达式中也要使用 [];如果在 new 表达式中不使用 [],则不要在相应的 delete 表达式中使用 []
条款 17:以独立语句将 new
ed 对象置入智能指针
对于这样一个函数:
1 | void ProcessWeidget(std::shared ptr<Widget> pw, int prio); |
以如下方式使用:
1 | ProcessWidget(std::shared_ptr<Widget>(new Widget), priority()); |
在调用 ProcessWidget 函数之前如果编译器选择以执行 new Widget、调用 priority 函数、执行 shared_ptr 构造函数的顺序执行,当 priority 函数发生异常时,new Widget 返回的指针将会遗失,从而造成资源泄漏。
正确的写法:
1 | std::shared_ptr<Widget> pw(new Widget); |
条款 18:让接口容易被正确使用,不易被误用
- 促进正确使用
- 接口的一致性:如 STL 中容器都有 size 函数
- 与内置类型的行为兼容
- 阻止误用
- 建立新类型
- 限制类型上的操作
- 束缚对象值
- 消除使用者的资源管理责任
- std::shared_ptr 支持定制型删除器,这可防范 cross-DLL 问题,可被用来自动解除互斥锁
- cross-DLL 问题产生于“对象在一个动态库中被 new 创建,却在另一个动态库内被 delete 销毁”,在许多平台上这种情况会导致运行期错误,而 std::shared_ptr 没有这个问题,因为它默认的删除器是来自 std::shared_ptr 诞生所在的那个动态库的 delete
- 好的接口可以防止无效的代码通过编译
条款 19:设计 class 犹如设计 type
设计一个新的 class 需要考虑以下问题:
- 新 type 的对象应该如何被创建和销毁?
- 对象的初始化和对象的赋值该有什么样的差别?
- 新 type 的对象如果被以值传递,意味着什么?
- 什么是新 type 的“合法值”?
- 你的新 type 需要配合某个继承图系吗?
- 你的新 type 需要什么样的转换?
- 什么样的操作符和函数对此新 type 而言是合理的?
- 什么样的标准函数应该驳回?
- 谁该取用新 type 的成员?
- 什么是新 type 的“未声明接囗”?
- 你的新 type 有多么一般化?
- 你真的需要一个新 type 吗?
条款 20:宁以 pass-by-reference-to-const 替换 pass-by-value
- pass-by-reference-to-const 通常比较高效
- 没有任何构造函数或析构函数被调用,因为没有任何新对象被创建
- pass-by-reference-to-const 可避免切割问题
- 当一个派生类对象以传值方式传入一个函数,但是该函数的形参是基类,则只会调用基类的构造函数构造基类部分,派生类的新特性将会被切割掉,而此时使用引用方式传递则可以保留派生类特性
条款 11 - 条款 20 | 《Effictive C++》笔记