Effective C++读书笔记~3 资源管理
条款13:以对象管理资源
Use objects to manage resources.
传统new/delete 申请、释放资源的问题
class Investment { ... };
class Factory
{
public:
static Investment* createInvestment(); // 工厂函数
};
void f( ) // 客户函数
{
Investment* pInv =Factory::createInvestment(); // 调用工厂函数,创建Investment对象
...
delete pInv; // 释放pInv所指对象
}
正常时,没有问题。而"..."代码段如果提前return,或者出现异常,提前退出f()函数,那么pInv所指Investment对象就无法释放。
为解决这个问题,可以使用RAII(Resource Acquisition Is Initialization,资源取得时机便是初始化时机)对象的机制,即对象创建时便初始化资源,对象析构时便销毁资源。而智能指针便是一种较好的管理资源对象的方式。
使用智能指针unique_ptr管理对象
对于上述问题,可以使用智能指针管理资源。
unique_ptr是一种智能指针,能确保某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。C++11引入,可用来替换auto_ptr。
void f()
{
unique_ptr<Investment> pInv(Factory::createInvestment()); // OK
//...
unique_ptr<Investment> pInv2(pInv); // 错误:unique_ptr不支持2个指针指向同一个对象
unique_ptr<Investment> pInv3;
pInv3 = pInv; // 错误:unique_ptr不支持2个指针指向同一个对象
// f退出前调用unique_ptr析构函数,自动删除pInv
}
使用引用计数智能指针shared_ptr管理对象
unique_ptr的缺点是无法复制,同一时刻只能有一个指针指向被管理的对象,也就是说资源无法共享。
可以使用引用计数型智能指针shared_ptr,来追踪有多少个对象指向某个资源,并且在无人指向它时自动删除该资源。
shared_ptr的缺点:无法打破环状引用,即2个已经没被使用的对象彼此互指,资源无法正常释放。
void f()
{
shared_ptr<Investment> pInv(Factory::createInvestment()); // OK
//...
shared_ptr<Investment> pInv2(pInv); // OK
shared_ptr<Investment> pInv3;
pInv3 = pInv; // OK
// f退出前调用shared_ptr析构函数,自动删除pInv, pInv2, pInv3
}
需要注意的是:unique_ptr和shared_ptr不能释放array。
因为两者在其析构函数内做delete,而不是delete[]。如果要使用自动释放的数组,建议用vector、string,或者boost::scoped_array和boost::shared_array classed。
小结
1)为防止资源泄漏,建议使用RAII对象,在构造函数中获得资源,在析构函数中释放资源;
2)两个经常被使用的RAII classes是:unique_ptr, shared_ptr。排他性的资源,前者较佳;共享性的资源,后者较佳。两者都只适合用来管理head-based资源。
[======]
条款14:在资源管理类中心小心coping行为
Think carefully about copying behavior in resource-managing classes.
对于不是heap-based资源,unique_ptr和shared_ptr 往往不适合作为资源的掌管着(resource handlers)。
我们以对POSIX 的pthread库里的互斥量用C++进行封装为例:
// Lock掌管不是heap-based的资源
class Lock {
public:
// 这里互斥锁是RAII资源,用Lock进行管理
explicit Lock(pthread_mutex_t *pm): mutexPtr(pm) { // 构造即获得互斥锁
pthread_mutex_lock(mutexPtr); // 获得互斥锁
cout << "locked" << endl;
}
~Lock() { // 析构即释放锁
pthread_mutex_unlock(mutexPtr); // 释放互斥锁
cout << "unlocked" << endl;
}
private:
pthread_mutex_t *mutexPtr; // RAII资源
};
// 客户多Lock的用法
pthread_mutex_t m; // 定义需要的互斥锁
void func()
{
Lock ml1(&m); // OK
Lock ml2(ml1); // 拷贝ml1到ml2上,会发生什么?
}
当一个RAII对象被复制时,会发生什么?
通常,会有2种选择:
1)禁止复制。参见条款06,可以将基类的copying操作声明为private,也可以使用delete禁止编译器合成copy构造函数和copy assignment运算符。
2)对底层资源使用“引用计数法”(reference-count)。有时,我们希望保持资源,共享给多个用户,直到最后一个使用者(某个对象)被销毁。此时应该使用shared_ptr。
使用shared_ptr当引用次数为0时,默认行为是调用delete操作符 删除所指物。而使用向互斥锁时,我们期望的是引用计数为0时,就解锁而非删除,因此需要手动指定其“删除器”(deleter)。删除器的本质是一个函数或函数对象(function object),当引用次数为0时调用。
// RAII资源管理类
class Lock {
public:
explicit Lock(pthread_mutex_t *pm): mutexPtr(pm, pthread_mutex_unlock) {
lock(mutexPtr.get());
}
Lock(const Lock& lck) {
mutexPtr = lck.mutexPtr; // mutexPtr次数-1, lck.mutexPtr次数+1. mutexPtr引用次数为0时, 指向内存释放, 并指向lck.mutexPtr指向的内存
lock(mutexPtr.get());
}
#if 1 // 析构函数可以依靠自动合成的, 也可以手动编写
~Lock() { // 因为编译器自动合成的析构函数, 调用每个non-static对象成员的析构函数
// mutexPtr的析构函数会在互斥锁引用次数为0时, 自动调用其创建时指定的删除器
unlock(mutexPtr.get()); // get获得shared_ptr指向的内存, i.e. shared_ptr第一个参数
}
#endif
private:
shared_ptr<pthread_mutex_t > mutexPtr; // 使用shared_ptr替换了raw pointer, 指向RAII资源
int lock(pthread_mutex_t *pm) {
int res = pthread_mutex_lock(pm);
cout << "shared locked" << endl;
return res;
}
int unlock(pthread_mutex_t *pm) {
int res = pthread_mutex_unlock(pm);
cout << "unlocked" << endl;
}
};
pthread_mutex_t m;
void func()
{
Lock ml1(&m); // OK
Lock ml2(ml1); // OK. 使用shared_ptr管理mutex后, 允许多个用户同时指向同一个互斥锁资源, i.e. 互斥锁资源被共享了
cout << "exit func" << endl;
}
int main() {
func();
cout << "exit main" << endl;
return 0;
}
这里用shared_ptr<pthread_mutex_t > mutexPtr
替换了原来的pthread_mutex_t *mutexPtr
,允许共享资源。
复制底部资源
需要“资源管理类”的唯一理由:当不再需要某个副本时,可以确保它被释放。此时,复制资源管理对象,应该同时也复制其所包含的资源。i.e. 复制资源管理器对象时,进行的是“深度拷贝”(deep copying)。
深度拷贝是指:不仅复制指针本身,还复制指针指向的内存内容。
转移底部资源的拥有权
某些场合下,你可能希望确保永远只有一个RAII对象指向一个未加工资源(raw resource),即使RAII对象被复制依然如此。此时,资源的拥有权会从被复制物转移到目标物。如条款13所述,unique_ptr的复制意义。
同一时刻,只允许一个unique_ptr指向给定对象。如果要从一个unique_ptr转移到另外一个,可以使用如下方法:
unique_ptr
unique_ptr
小结
1)复制RAII对象必须一并复制它所管理的资源(深度拷贝),所以资源的copying行为决定RAII对象的copying行为;
2)通常的RAII class copying行为是:抑制copying(禁用copying函数),或者引用计数法(reference counting)(可使用shared_ptr)。
[======]
条款15:在资源管理类中提供对原始资源的访问
Provide access to raw resources in resource-managing classes.
条款13中的pInv是对象,如果要访问shared_ptr指向的那块Investment对象(称为原始资源,raw resource),要怎么办?
shared_ptr<Investment> pInv(Factory::createInvestment()); // OK
比如,希望通过daysHeld返回投资天数
int daysHeld(const Investment* pi); // 返回投资天数
要如何调用?
要将RAII class对象转换为内含的原始资源,可以分为两种方式:
1. 显示转换
使用shared_ptr的get成员函数(unique_ptr也提供),返回智能指针内部的原始指针(的副本),来进行显式转换。
int days = daysHeld(pInv.get()); // get获取shared_ptr指向的内存
2. 隐式转换
使用shared_ptr重载了的指针取值操作符(operator ->和operator *),允许隐式转换至所指向的资源的原始指针。unique_ptr同样重载了这2个操作符。
class Investment {
public:
bool isTaxFree() const;
...
};
class Factory {
public:
static Investment* createInvestment() // 工厂函数
{ return new Investment;}
};
shared_ptr<Investment> pi1(Factory::createInvestment()); // shared_ptr管理资源
bool taxable1 = !(pi1->isTaxFree()); // 利用shared_ptr的 -> 操作符, 访问raw resource对象的成员函数
unique_ptr<Investment> pi2(Factory::createInvestment()); //unique_ptr管理资源
bool taxable2 = !((*pi2).isTaxFree()); // // 利用unique_ptr的 * 操作符, 访问raw resource对象的成员函数
小结
1)APIs往往要求访问原始资源(raw resources),所以每个RAII class应该提供一个“取得其所管理的资源”的办法(就像shared_ptr的get方法,解引用方法(*),指针访问所指目标的-> 方法);
2)对原始资源的访问可能经由显示转换/隐式转换。一般而言,显示转换比较安全,但隐式转换对客户比较方便。
[======]
条款16:成对使用new和delete时要采取相同形式
Use the same from in corresponding uses of new and delete.
对象、数组的申请与释放
string* stringPtr1 = new string; // 申请一个string对象
string* stringPtr2 = new string[100]; // 申请一个string对象数组,大小为100
delete stringPtr1; // 删除一个对象
delete[] stringPtr2; //删除一个由对象组成的数组
1)如果在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果在new表达式中不用[],一定不要在相应的delete表达式中使用[]。
[======]
条款17:以独立语句将newed对象置入智能指针
Store newd objects in smart pointers in standalone statements.
资源创建和资源转换之间的异常
假设我们有函数priority用于揭示程序的优先权,而函数processWidget用于动态分配的Widget上进行带有优先权的处理,
class Widget {};
int priority();
void processWidget(shared_ptr<Widget> pw, int priority);
我们“以对象管理资源”(条款13),在processWidget中使用Widget的raw pointer来构造智能指针shared_ptr,来管理Widget:
processWidget(shared_ptr<Widget>(new Widget), priority());
上述代码看似没有问题,却可能造成资源泄漏。原因是,priority()发送异常时,Widget的raw pointer丢失,无法被正常释放。
调用processWidget之前,编译其会创建代码做以下三件事:
- 调用priority
- 执行new Widget
- 调用shared_ptr构造函数
然而,编译器以何种次序完成这三件事却没有定数,唯一能确定的是“执行new Widget”发生在“调用shared_ptr构造函数”之前。如果编译器为了取得更高效的代码,可能最终的操作序列会是这样:
1)执行new Widget
2)调用priority
3)调用shared_ptr构造函数
当执行到第2)步时,如果调用priority发生异常,那么第1)步new Widget得到的指针就会丢失,从而造成内存泄漏。
注意:
1)调用惯例(cdecl, thiscall)只能确保参数的入栈顺序,并不能确保在这之前的参数构造、核算顺序。
2)其他语言如Java、C#不存在这个问题,因为它们总是以特定次序完成函数参数的核算。
解决办法
这类发生在“资源被创建(new Widget)”和“资源被转换为资源管理对象(shared_ptr
办法很简单,就是分离语句,让资源的创建、转换和可能异常语句不要在同一行,分别写出:
1)创建Widget并置入智能指针内;
2)把指针传递给processWidget;
shared_ptr<Widget> pw(new Widget); // 单独语句内以智能指针存储newed所得对象
processWidget(pw, priority()); // 该调用不会因异常而导致Widget指针丢失, 从而造成泄漏的问题
这么做有效的原因是因为:编译器对于“跨越语句的各项操作”没有重新排列的资源(只有在语句内才拥有那个自由度)
小结
1)以独立语句将newd对象存储于(置入)智能指针内。如果不这样做,一旦被抛出异常,有可能导致难以察觉的资源泄漏。
[======]