常见的资源类型
内存、文件描述器(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、网络socks。
条款13: 以对象管理资源
普通示例:
class Investment {...}; // "投资类型"继承体系中的root class
// 投资类的具体创建,指向Investment继承体系内的动态分配对象。调用者有责任释放
Investment * createInvestment();
void f()
{
Investment* pInv = createInvestment(); // 调用factory函数
...
delete pInv; // 释放pInv所指对象
}
使用智能指针方式自动析构:
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
...
// 经由auto_ptr的析构函数自动删除pInv
}
注:现代C++中已将auto_ptr废弃,建议使用更安全的unique_ptr等智能指针。
展示的两个关键想法:
- 获得资源后立刻放进管理对象(managing object)内:上面示例放入auto_ptr作为初值, “以对象管理资源”就是通常所说的RAII(Resource Acquisition Is Initialization; 资源取得时机便是初始化时机)。
- 管理对象(managing object)利用析构函数确保资源被释放:使用析构释放简化流程。
注意点: 不要让多个auto_ptr指向同一个对象,会导致未定义的多次销毁。这一点由auto_ptr的性质:若通过copy构造函数或者copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权。
std::auto_ptr<Investment> pInv1(createInvestment());
sd::auto_ptr<Investment> pInv2(pInv1); // pInv2指向对象, pInv1被设置为null
pInv1 = pInv2; // pInv1指向对象,pInv2为null
使用RCSP(reference-counting smart pointer; RCSP)的版本:
void f()
{
...
std::shared_ptr<Investment> pInv(createInvestment());
...
// 释放经由shared_ptr析构函数自动删除pInv
}
指针复制:
void f()
{
...
std::shared_ptr<Investment> pInv1(createInvestment());
std::shared_ptr<Investment> pInv2(pInv1); // pInv1和pInv2指向同一个对象
pInv1 = pInv2; // 同上
...
}
注意: 不论auto_ptr和shared_ptr两者都在其析构函数内做delete而不是delete[]动作,不要在动态分配的array身上使用。编译上可以通过!
std::auto_ptr<std::string> aps(new std::string[10]);
std::shared_ptr<std::string> spi(new std::int[1024]);
现代指针管理资源补充说明:
- 现代智能指针对比:
// C++11+ 推荐使用 unique_ptr(独占所有权)
std::unique_ptr<Investment> pInv(createInvestment()); // 无法复制,但可移动
// C++11+ 推荐使用 shared_ptr(共享所有权)
std::shared_ptr<Investment> pInv(createInvestment()); // 支持复制,引用计数管理
// C++14+ 推荐使用 make_unique(更安全的创建方式)
auto pInv = std::make_unique<Investment>(args...);
// C++11+ 推荐使用 make_shared(更高效的创建方式)
auto pInv = std::make_shared<Investment>(args...);
- 禁止使用裸指针初始化多个智能指针:
Investment* ptr = new Investment;
std::shared_ptr<Investment> p1(ptr); // 正确
std::shared_ptr<Investment> p2(ptr); // 错误!多个独立的shared_ptr管理同一资源,导致重复释放
条款14: 在资源管理类中小心copying行为
针对某些非heap-based的资源,需要自己按照RAII(Resource Acquisition Is Initialization, 资源取得时机便是初始化时机“)自己定义管理类。RAII类型的资源管理保证资源在构造期间获得,在析构期间释放。
例如mutex。
但是在使用时,如果资源管理类进行copy操作,其结果不一符合预期,此时需要小心处理。采用的策略通常有两种:禁止和引用计数。
- 禁止
class Lock : private Uncopyable { // 条款6
};
- 引用计数
class Lock {
public:
explicit Lock(Mutex *pm) : mutexPtr(pm, unlock) // 这里通过指定unlock为删除器
{
lock(mutexPtr.get());
}
private:
std::shared_ptr<Mutex> mutexPtr;
};
补充说明:
- C++11+的禁止拷贝方法
class Lock {
public:
Lock(const Lock&) = delete; // 禁用拷贝构造
Lock& operator=(const Lock&) = delete; // 禁用赋值运算符
...
};
- 移动语义支持
class Lock {
public:
Lock(Lock&& other) noexcept { ... } // 移动构造函数
Lock& operator=(Lock&& other) noexcept { ... } // 移动赋值运算符
};
条款 15:在资源管理类中提供对原始资源的访问
场景:
std::shared_ptr<Investment> pInv(createInvestment());
int daysHeld(const Investment *pi); // 返回投资天数
int days = daysHeld(pInv); // 编译报错
// 正确方式
int days = daysHeld(pInv.get());
智能指针重载了->和*操作符。
class Investment {
public:
bool isTaxFree() const;
...
};
Investment * createInvestment();
std::shared_ptr<Investment> pi1(createInvestment());
bool taxable1 = !(pi1->isTaxFree()); // 经由operator->访问资源
...
std::shared_ptr<Investment> pi2(createInvestment());
bool taxable2 = !((*pi2).isTaxFree()); // 经由operator*访问资源
使用隐式转换使调用更自然,不需要使用get()获取原始指针,但是会有副作用,需要再使用时注意。
class Font {
public:
...
operator FontHandle() const { return f; } // 隐式转换函数
};
FontHandle getFont();
void changeFontSize(FontHandle f, int newSize);
Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize); // 使用隐式转换方式
...
FontHandle f2 = f1; // 错误,原意是想拷贝一个Font对象,却反而将f1隐式转换为其底部的FontHandle然后才复制它。 如果f1被销毁,那么f2将变为虚吊的
条款 16:成对使用 new 和 delete 时要采取相同形式
错误示例:
std::string *stringArray = new std::string[100];
...
delete stringArray;
正确示例应为:
std::string *stringArray = new std::string[100];
...
delete [] stringArray;
同样的,如果你使用new创建一个非数组的指针,那么不应当使用delete []方式删除。
需要明白new: 是先创建内存,然后调用构造函数,delete: 是先调用析构函数,然后再调用释放内存。数组和非数组的场景下申请和释放是存在次数差异的。
补充:
数组的智能指针管理:
// C++11+ 使用 unique_ptr 管理数组
std::unique_ptr<int[]> arr(new int[10]); // 自动使用 delete[]
// C++17+ 推荐使用 std::array 或 std::vector 替代动态数组
std::array<int, 10> arr; // 栈上分配,无需手动释放
std::vector<int> vec(10); // 动态大小,自动管理内存
条款 17:以独立语句将 newed 对象置入智能指针
可能的因时序导致问题的代码:
int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);
// 可能产生异常的调用
void processWidget(new Widget, priority()); //假设构造函数是一个explicit的函数,此时无法隐式转换
void processWidget(std::shared_ptr<Widget>(new Widget), priority()); // 参数执行顺序在C++不确定
对第二种情况主要的问题点:
- 执行new Widget
- 执行priority
- 执行shared_ptr构造函数
如果执行priority函数抛出异常,那么shared_ptr将得不到执行,从而无法管理申请到的new Widget原始指针。
所以为了避免上面的情况,建议使用下面的方式进行处理:
std::shared_ptr<Widget> pw(new Widget); // 在单独语句内执行
processWidget(pw, priority());
补充:
C++17 规定函数参数按从左到右的顺序求值,因此以下代码在 C++17+ 中是安全的:
processWidget(std::shared_ptr<Widget>(new Widget), priority()); // C++17+ 安全
但为了兼容性,仍建议使用独立语句初始化智能指针。
也可以使用工厂函数消除手动new:
// 提供工厂函数创建对象并返回智能指针
auto createWidget() {
return std::make_unique<Widget>();
}
// 安全调用
processWidget(createWidget(), priority());