条款 14:在资源管理类中小心拷贝行为
我们应该永远保持这样的思考:当一个RAII对象被复制,会发生什么事?
选择一:禁止复制(unique_ptr)(常用)
许多时候允许RAII对象被复制并不合理,如果确是如此,那么就该明确禁止复制行为,条款 6 已经阐述了怎么做这件事。
选择二:对底层资源祭出“引用计数法”(shared_ptr)(常用)
正如std::shared_ptr
所做的那样,每一次复制对象就使引用计数+1,每一个对象离开定义域就调用析构函数使引用计数-1,直到引用计数为0就彻底销毁资源。std::shared_ptr的缺省行为是“当引用次数为0时删除其所指物”,也允许指定所谓的“删除器”(仿函数或lambda表达式)
选择三:复制底层资源
在复制对象的同时复制底层资源的行为又被称作深拷贝(Deep copying),例如在一个对象中有一个指针,那么在复制这个对象时就不能只复制指针,也要复制指针所指向的数据。
选择四:转移底层资源的所有权
和std::auto_ptr
的行为类似,永远保持只有一个对象拥有对资源的管理权,当需要复制对象时转移资源的管理权。
条款 15:在资源管理类中提供对原始资源的访问
和所有的智能指针一样,STL 中的智能指针也提供了对原始资源的隐式访问和显式访问:
Investment* pRaw = pSharedInv.get(); // 显式访问原始资源
Investment raw = *pSharedInv; // 隐式访问原始资源
当我们在设计自己的资源管理类时,也要考虑在提供对原始资源的访问时,是使用显式访问还是隐式访问的方法,还是两者皆可。
class Font {
public:
FontHandle Get() const { return handle; } // 显式转换函数
operator FontHandle() const { return handle; } // 隐式转换函数
private:
FontHandle handle;
};
一般而言显式转换比较安全,但隐式转换对客户比较方便。
条款 16:成对使用 new 和 delete 时要采用相同形式
当使用new时,内存被分配出来(通过operator new函数),针对此内存会有一个会多个构造函数被调用。当使用delete时,针对此内存会有一个或多个析构函数被调用,内存被释放(通过operator delete函数)。
使用new
来分配单一对象,使用new[]
来分配对象数组,必须明确它们的行为并不一致,分配对象数组时会额外在内存中记录“数组大小”,而使用delete[]
会根据记录的数组大小多次调用析构函数,使用delete
则仅仅只会调用一次析构函数。对于单一对象使用delete[]
其结果也是未定义的,程序可能会读取若干内存并将其错误地解释为数组大小。
int* array = new int[10];
int* object = new int;
delete[] array;
delete object;
需要注意的是,使用typedef
定义数组类型会带来额外的风险:
typedef std::string AddressLines[4];
std::string* pal = new AddressLines; // pal 是一个对象数组,而非单一对象
delete pal; // 行为未定义
delete[] pal; // 正确
条款 17:以独立语句将 newed 对象置入智能指针
“资源被创建”和“资源被转换为资源管理对象”两个时间点之间有可能发生异常干扰。解决办法:
std::shared_ptr<Widget> pw(new Widget);//创建Widget,再将其置入一个智能指针内。
processWidget(pw,priority());
原书此处所讲已过时,现在更好的做法是使用std::make_unique
和std::make_shared
:
auto pUniqueInv = std::make_unique<Investment>(); // since C++14
auto pSharedInv = std::make_shared<Investment>(); // since C++11