条款 11 - 条款 20 | 《Effictive C++》笔记

Effictive C++ 条款 11 - 条款 20。

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

确保当对象自我赋值时 operator= 有良好行为,应当保证有自我赋值安全性和异常安全性。

自我赋值不安全,异常不安全的实现(pb 会指向一个被删除的对象):

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

自我赋值安全,异常不安全的实现(如果构建 Bitmap 失败,则 pb 可能会是一个 nullptr):

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

自我赋值安全,异常安全的实现(如果 new Bitmap 抛出异常,pb 保持原状):

1
2
3
4
5
6
Widget& Widget::operator=(const Widget& rhs) {
Bitmap* dummy = pb; // 记住原先的 pb
pb = new Bitmap(*rhs.pb); // 令 pb 指向 *pb 的一个副本
delete dummy; // 删除原先的 pb
return *this;
}

自我赋值安全,异常安全的实现(让 operator= 具备异常安全性往往自动获得自我赋值安全性):

1
2
3
4
5
Widget& Widget::operator=(const Widget& rhs) {
Widget temp(rhs); // 为 rhs 数据制作一份副本
swap(temp); // 将 *this 数据和上述副本的数据交换
return *this;
}

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

  • 对于基类中的 private 成员,派生类无法访问,所以在复制派生类时,应该让派生类的拷贝构造函数调用相应的基类构造函数
    1
    2
    3
    4
    5
    DerivedClass::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 成为虚吊的)
    • 但隐式转换对客户比较方便:不需要每次都要显式地调用函数

条款 16:成对使用 newdelete 时要来取相同形式

  • delete 和 delete[] 的区别在于 delete 会将 new 时申请的空间释放(new 时记录了分配空间的大小),但是只会调用第一个元素的析构函数;而 delete[] 则不仅会将 new 时申请的空间释放,还会调用所有元素的析构函数
  • 所以当 new 一个数组的时候,对于基本数据类型 delete 和 delete[] 都能完成内存的释放;而对于自定义数据类型,delete 只会调用第一个对象的析构函数,造成了资源泄漏问题
  • 如果在 new 表达式中使用 [],在相应的 delete 表达式中也要使用 [];如果在 new 表达式中不使用 [],则不要在相应的 delete 表达式中使用 []

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

对于这样一个函数:

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
2
std::shared_ptr<Widget> pw(new Widget);
ProcessWidget(pw, priority());

条款 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++》笔记

http://www.zh0ngtian.tech/posts/ec3ff284.html

作者

zhongtian

发布于

2020-08-30

更新于

2023-12-16

许可协议

评论