三. 资源管理
除了内存,其他常见的资源还包括文件描述器、互斥锁、图形界面中的字型和笔刷、数据库连接、网络sockets。重要是不用就归还给系统。
条款13: 以对象管理资源
基于对象的资源管理办法,是十分有效的,手工释放资源容易发生某些错误。
一: 基于对象管理资源的方法
获得资源后立即放入管理对象内,许多资源被动态分配于heap内而后被用于单一区块或函数内,它们应该在控制流离开那个区块或函数时被释放。标准程序库提供auto_ptr"类指针对象,其析构函数自动对所指对象调用delete。用对象管理资源的方法即"以资源取得时机便是初始化时机"(Resource Acquisition Is Initialization;RAII),管理对象运用析构函数确保资源被释放。(不论控制流如何,只要对象被销毁,析构函数必会调用)在析构函数中有异常,请遵循条款8)。若提供拷贝构造函数或拷贝赋值运算符复制auto_ptr,他们就变为NULL。
void f()
{
auto_ptr<Investment> pInv(createInvestment());//由auto_ptr的析构函数来自动删除pInv。
}
//auto_ptr缺点,不能复制,编译提供,但是析构不通过
auto_ptr<Investment> pInv1=(createInvestment());
auto_ptr<Investment> pInv2(pInv1);
pInv1=pInv2;
因为auto_ptr不支持正常的资源复制行为,所以使用一种"引用计数型智慧指针"(reference-counting smart pointer,RCSP),RCSP提供一种类似垃圾回收,缺点是无法打破环状引用。典型指针:shared_ptr。因为RCSP支持复制行为,广泛应用于STL容器。
void f()
{
shared_ptr<Investment> pInv1(createInvestment());
shared_ptr<Investment> pInv2(pInv1);//可以
pInv3=pInv1;
}
因为auto_ptr和shared_ptr的析构函数内做的是delete而不是delete[],所以下列的用法是不正确的。
auto_pre<string> aps(new string[10]);
shared_ptr<int> spi(new int[1024]);
总结:
- 为了防止资源泄漏,请使用 RAII对象, 他们在构造函数中获得资源并在析构函数中释放资源
- 两个常被使用的对象 classes 分别是 std::shared_ptr 和 std::auto_ptr。
条款14: 在资源管理类中小心 copying 行为
资源的类型并非都是 heap_based 资源时,你需要建立自己的资源管理类
一: 体验资源管理类中copying行为带来的问题
void Lock(pthread_mutex_t *mutex){
pthread_mutex_lock(mutex);//互斥锁 锁定
}
void Unlock(pthread_mutex_t *mutex){
pthread_mutex_unlock(mutex);//互斥锁 解锁
}
class MutexLock{
private:
pthread_mutex_t *mutex_ptr_;
public:
explicit MutexLock(pthread_mutex_t *mutex)
:mutex_ptr_(mutex)
{
Lock(mutex);
cout<<"MuteLock Lock!"<<endl;
}
~MutexLock()
{
Unlock(mutex_ptr_);
cout<<"MuteLock Unlock!"<<endl;
}
};
pthread_mutex_t mutex;
void test(){
pthread_mutex_init(&mutex,nullptr);
MutexLock lock(&mutex);
MutexLock lock2(lock);//资源拷贝,重复加锁,死锁。
}
面对资源 copy 的动作有如下解决方案
-
禁止拷贝
-
对底层资源使用 RCSP ,引用计数法 (使用 shared_ptr)
-
深拷贝复制底部资源
对于一份资源可以无限复制,"资源管理类"的好处是当不需要某个复件时确保它被释放,再次情况下复制资源管理对象,应该同时复制其所包裹的资源,也就是说复制资源管理对象时,进行的是"深度拷贝"。 补充知识:某些标准字符串类似是由"指向heap内存"之指针构成(那内存被用来存放字符串的组成字符)。这种字符串对象内含一个指针指向一块heap内存。当这样的一个字符串对象被复制,不论指针或其所指内存都会被制作出一个复件。这就是深度复制行为。
-
转移底部资源的拥有权
二: 禁止 copy 行为
class Uncopyable{
protected:
Uncopyable() = default;
~ Uncopyable() = default;
private:
Uncopyable(const Uncopyable& rhs);
Uncopyable& operator=(const Uncopyable& rhs);
}
class MutexLock: private Uncopyable {
private:
pthread_mutex_t *mutex_ptr_;
public:
explicit MutexLock(pthread_mutex_t *mutex)
:mutex_ptr_(mutex)
{
Lock(mutex);
cout<<"MuteLock Lock!"<<endl;
}
~MutexLock()
{
Unlock(mutex_ptr_);
cout<<"MuteLock Unlock!"<<endl;
}
};
}
三: 引用计数 RCSP
//将mutex的底层资源交付给shared_ptr 管理即可,并传递给其删除其
class MutexLock {
private:
shared_ptr<pthread_mutex_t> mutex_ptr_;
public:
explicit MutexLock(pthread_mutex_t *pm)
:mutex_ptr_(pm,Unlock)
{
Lock(mutex);
cout<<"mutex_ptr_!"<<endl;
}
}
总结
- 复制 RAII 对象必须一并复制它所管理的资源,资源的copying行为决定RAII对象的copying行为
- 普遍而常见的 RAII class copying行为是: 抑制copying,施行引用计数RCSP法。
条款15: 在资源管理类中提供对原始资源的访问
提供原始资源的访问以方便客户
例如: shared_ptr 的get获取原始指针(返回智能指针内部的原始指针的复件,本例子为Investment* 指针,如果不加get,返回的是类型为shared_ptr的对象)
-
显式返回原始指针,本例中用get成员函数执行显示转换。
-
隐式直接进行转换,两种智能指针也重载了指针取值操作符(->和*)(在类中定义 转换资源类型的 运算符)
class Investment{ public: bool isTaxFree() const; } Investment* createInvestment(); shared_ptr<Investment> pi1(createInvestment());//令shared_ptr管理一笔资源 bool taxable1 = !(pi1 -> isTaxFree()); //经由 -> 访问资源 auto_ptr<Investment> pi2(createInvestment()); bool taxable2 = !((*pi2).isTaxFree()); //经由 * 访问资源
综合案例:
class MutexLock: private Uncopyable {
private:
pthread_mutex_t *mutex_ptr_;
public:
explicit MutexLock(pthread_mutex_t *mutex)
:mutex_ptr_(mutex)
{
Lock(mutex);
cout<<"MuteLock Lock!"<<endl;
}
//显式
pthread_mutex_t* GetMutexPtr() {return mutex_ptr_;}
//隐式
using MutexPtr = pthread_mutex_t* ;
operator MutexPtr() const {
return mutex_ptr_;
}
~MutexLock()
{
Unlock(mutex_ptr_);
cout<<"MuteLock Unlock!"<<endl;
}
};
}
//测试
pthread_mutex_t mutex;
void test(){
MutexLock mutex_lock(&mutex);
pthread_mutex_t *mutex_ptr = mutex_lock;//隐式使用??这里不懂
}
总结
- APIs 往往要求访问原始资源,所以每个 RAII class 应该提供一个
”访问原始管理之资源“ 的办法 - 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,
但隐式转换对客户比较方便。
条款16: 成对使用 new 和 delete 时要采取相同形式
当使用new,有两件事发生:(1):内存被分配出来。(2):针对此内存会有一个或更多的构造函数被调用。
当使用delete,有两件事发生:(1):针对此内存会有一个或更多的析构函数被调用。(2):内存被分配出来。
数组的内存布局不同于单一对象的内存布局,数组的内存还包括"数组大小"的记录,以便delete需要知道调用多少次析构函数。
- new 对应 delete, new[] 对应 delete[]
- 尽量少使用数组,多使用 vector string 标准库
- 尽量不要对数组使用typedef 好吗
void test(){
auto str_ptr = new string();
delete str_ptr;
auto str_ptr2 = new string[10]();
delete []str_ptr2;
}
条款17: 以独立语句将 newed 对象置入智能指针
一: 我们来演示一个复杂的错误(异常导致内存泄漏)
class Widget(){};
int priority(){
throw runtime_erroe("");
}
void ProcessWidget(shared_ptr<Widget> widget_sptr,int priority){
}
void test(){
// ProcessWidget(new Widget,priority());//编译不通过,原因是shared_ptr构造函数是显示转换,但是该处是隐式转换,所以采用下面的方式
ProcessWidget(shared_ptr<Widget>(new Widget),priority());//ProcessWidget决定对动态分配得来的Widget运用智能指针
//这里内存泄漏了
}
c++ 语言的函数传参调用顺序弹性很大
我们理想的调用顺序:
- ProcessWidget
- 执行new Widget表达式
- 执行shared_ptr构造函数
可能会出现调用顺序如下的表现:
- new Widget
- priority() //假若这里抛出了异常,new Widget返回的指针没有置于shared_ptr内,后者是我们用来防止内存泄漏的武器。引发内存泄漏问题
- shared_ptr
更好的办法是分离语句,将执行顺序定死,这点c++就没有c#和java好用了
shared_ptr<Widget> pw (new Widget);
ProcessWidget(pw,priority());
总结
- 尽量以独立的语句将newed 资源置入 智能指针,否则可能因为抛出异常导致资源泄漏。