Effective C++ 学习笔记 3:资源管理
- Effective C++ 学习笔记 1:让自己习惯 C++
- Effective C++ 学习笔记 2:构造、析构、赋值运算
- Effective C++ 学习笔记 3:资源管理
- Effective C++ 学习笔记 4:设计与声明
- Effective C++ 学习笔记 5:实现
- Effective C++ 学习笔记 6:继承与面向对象设计
- Effective C++ 学习笔记 7:模板与泛型编程
- Effective C++ 学习笔记 8:定制 new 和 delete
- Effective C++ 学习笔记 9:杂项讨论
计算机资源,除了我们熟悉的内存,还包括其他需要在使用时占用,在使用后归还给系统的东西,还包括如文件描述符、互斥锁、图形控件、数据库连接、网络端口等。
条款 13: 以对象管理资源(重要)
Use objects to manage resources.
话题 1:不要让调用者回收资源
不要把 delete 的工作单独留给调用者。有些时候是担心调用者忘记 delete,但更隐晦的情况可能是调用程序写了 delete,但因为一些原因,不会执行到 delete,比如之前的一些异常,或者隐藏很深的 return 语句。程序在维护过程中,逐渐可能会暴露这种问题。
我们应该将资源管理的工作放到对象内,让对象的析构函数来完成资源回收的任务。
话题 2:智能指针
可以将资源赋给智能指针来引用,智能指针可以自动在退出作用域时,回收它指向的资源。
比如 unique_ptr:
1 | class Inv(); // 省略定义 |
两个关键工作:
- 获得资源后立即将其放入对象内管理。比如上边代码放到了
unique_ptr<Inv> pInv中。 这叫 RAII,即资源取得时便是初始化时(Resource Acquisition Is Initialization) - 管理对象运用析构函数确保资源被释放。比如上边代码中,unique_ptr 的析构函数会释放资源。因为 C++ 要求析构函数中不得抛出异常,所以一旦管理对象要被析构了,它所管理的资源也一定会被释放。
别让多个 unique_ptr 指针指向同一个对象,如果其中一个 unique_ptr 已经析构了,那其他 unique_ptr 将指向已经被释放的资源位置!unique_ptr 有个特殊的性质,它的 copy 构造函数和copy 运算符重载,是会控制唯一性,它们会让 copy 的指针为 null:
1 | std::unique_ptr<Inv> pInv1 (createInv()); // pInv1 指向 Inv 资源 |
注:unique_ptr 如果换成 auto_ptr(被 C++11 弃用的一种智能指针),这种操作可能导致访问非法内存,比如当 pInv1 为 null 时,用户错误的认为它依然指向资源而引用资源。unique_ptr 与其最大的不同是,unique_ptr 可以在编译期检查这种错误的引用。
对这个问题的替代方案是 shared_ptr,它是引用计数型智能指针。类似于互斥锁和共享锁的概念,当多个 shared_ptr 指向同一个资源时,它可计数,当计数不为 0 时,析构个别 shared_ptr 不会释放资源,只有当计数为 0 时,才会析构这些 shared_ptr 指向的资源。
但 shared_ptr 不能解决环状引用,也就是两个指针互相指向对方,所以它只是类似于资源回收。
unique_ptr 和 shared_ptr 只是 RAII 的一个典型应用,我们自己写的结构中也可以应用 RAII。
话题 3:智能指针不能用来指向数组
unique_ptr 和 shared_ptr 在释放资源时都是用 delete,而不是 delete[],所以它们不应该被用来指向数组结构,而且,编译器也无法检查这种错误,一旦指向了数组结构,那最后在析构时,就无法完全释放资源。
作者的理由是,C++ 中总有办法取代数组,比如 string 和 vector。不过,Boost 库中的 boost::scopted_array 和 boost::shared_array 可以实现指向数组的操作。
其实,createInv() 这个接口本身设计的有问题,它不应该把一个资源直接扔出来。
注:C++ 11 中的 unique_ptr 可以在模板类型中指定带 [] 的类型,从而可以正常释放数组成员。
总结
- 为了防止资源泄漏,请使用 RAII 对象,它们在构造函数中获取资源并在析构函数中释放资源。
- 两个常被使用的 RAII 对象是 shared_ptr 和 unique_ptr,前者通常更常用,区别在于 copy 时是否允许多个指针指向同一个资源对象。
条款 14 :在资源管理类中小心 copying 行为
Think carefully about copying behavior in resource-managing classes.
- 禁止资源管理类对象的复制,这种操作会导致意外情况。使用 private Uncopyable 方式来处理,见 条款6。
- 或者,使用引用计数,这和 shared_ptr 是类似的境况。shared_ptr 支持定义删除器(deleter),默认情况下,unique_ptr 和 shared_ptr 都是通过 delete 来释放资源,但有些时候,我们希望通过其他方式释放资源,比如
unlock(mutex)。 - 复制资源管理类时,进行深度拷贝。也就是并不是复制类对象本身,而是复制类对象管理的所有资源,都形成副本。比如 string 的复制,虽然 string 的内容是指向 heap 上字符内容的指针,但复制 string 对象时, heap 上的字符内容都会产生副本。
- 控制任何时刻,只允许最多 1 个资源管理类对象来管理资源,就像 unique_ptr 那样子。通过重载两个 copy 函数来实现。
总结
- 复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为。
- 普遍而常见的 RAII classes copying 行为是:禁止 copying、引用计数法。
条款 15:在资源管理类中提供对原始资源的访问
Provide access to raw resources in resource-managing classes.
虽然我们寄希望于把资源放到资源管理类中管理,从而避免资源泄漏的问题。但是有些时候,我们难免还是会遇到一些 APIs,他们需要直接操作资源。
对于智能指针,它们提供 get 方法来获取资源,将资源管理类的指针显式转换为对应指向的资源。
同时,它们也提供了隐式的指针取值操作,来获取资源。智能指针也是指针,通过取值操作(operator-> 和 operator*),隐式的获取原始资源。
还有一种自定义的办法,如果显示提供 get 方法来获取资源比较繁琐,可以隐式指定一个转换函数,也就是重载隐式类型转换函数:
1 | FH getFont(); // 这是一个 C API,我们希望做一个资源管理类来管理这个 API |
通常,get 方法更常用,因为隐式的类型转换可能会导致用户误用。
总结
- APIs 往往要求访问原始资源(raw resources),所以每个 RAII class 应该提供一个能够取得其所管理资源的方法。
- 对原始资源的访问可能经过显式转换或隐式转换,一般而言,显式转换比较安全,但隐式转换对用户使用更为方便。
注:从资源管理类中提供直接获取原始资源的句柄,会破坏资源管理类的封装性,导致资源意外泄露,需要酌情考虑是否提供获取原始资源的句柄。
条款 16: 成对使用 new 和 delete 时要采用相同形式
Use the form in corresponding uses of new and delete.
如果 new 时指定的是一个资源对象,那释放资源用 delete;如果 new 时指定的是一个资源对象的数组,那释放资源用 delete[]。
究其根源是因为,new 一个单一的资源对象时,内存中将直接保存这个资源,并将指向该资源的指针返回;new 一个资源对象的数组时,会在内存中开头先存放一个数组长度,然后才会分配资源内存,并将指向该资源内存开头的位置返回。
程序运行时无法得知一块资源内存中的开头,存放的是单一资源还是资源数组的长度,从而需要用户来自己控制。
虽然大多数情况下,我们不会犯这种错误,但有些错误比较隐晦:
1 | typedef std::string Address[4]; // 定义了一个类型 Address,其结构是 4 个 std::string 的对象数组 |
为了避免这个问题,作者建议不要将数组结构用 typedef 定义成自定义类型,可以使用 vector 或 string 等现成的对象来取代。
总结
- 如果你在 new 表达式中使用
[],必须在对应的 delete 表达式中同样使用[]。如果在 new 表达式中没有使用[],也务必不要在 delete 表达式中使用[]。
条款 17:以独立语句将 newed 对象置入智能指针
Store newed objects in smart pointers in standalone statements.
错误示例:
1 | int pR(); |
原因是,C++中不约定函数参数的初始化顺序,如果初始化参数的顺序是:
- new W;
- pR();
- shared_ptr 的构造函数
那么,一旦 pR() 中发生异常,走了其他路径,1 步骤获取的资源就无法被释放了。
避免办法就是,不要把资源获取并赋给智能指针的语句和其他语句放到一个语句中。
正确示例:
1 | std::shared_ptr<W> pw(new W); // 独立完成这个动作 |
总结
- 以独立语句将 newed 对象存储于智能指针中。如果不这样做,一旦异常发生,可能会导致无法察觉的资源泄漏。
注:更推荐的做法是,使用 std::make_unique() 和 std::make_shared() 接口来初始化 unique_ptr 和 shared_ptr,前者在 C++14 中提供,后者在 C++11 中提供。将 new 的行为封装在接口中,也可以有效的避免函数参数求值顺序导致的上述问题。
封面图片来自:Photo by Aedrian Salazar on Unsplash
本文同步发布在 CSDN 账号下:Effective C++ 学习笔记 第三章:资源管理



















