概述
- 资源就是一旦用了它,以后必须还给系统的东西。C++中最常用的资源就是动态内存分配。其它的资源还包括文件描述符器、互斥锁、图形界面中的字型和笔刷、数据库连接、以及网络socket。
- 无论是哪一种资源,我们都要确保当自己使用完之后还给系统。
条款13:以对象管理资源
1. 资源并没有还给系统。
概述中已经说到,资源用完之后要还给系统。 我们考虑以下函数会发生什么:
void f()
{
Investment * pInv = createInvestment();
...
delete pInv;
}
1.1 倘若我们在delete之前的函数中有一个分支,会进行return。那么我们就永远不会执行delete,这样函数执行结束之后并没有将动态分配的资源还给系统,会有内存泄漏的风险。
1.2 倘若中间有goto语句,也是如此。
1.3 倘若抛出了异常,delete函数也无法被执行到。
2.1 使用智能指针————auto_ptr.
void f()
{
std::auto_ptr<Investment> pInv(createInvesment());
...
}
当智能指针出了函数的作用域,会调用其析构函数自动删除pInv.
这个简单例子示范“以对象管理资源”的两个关键想法:
- 获得资源后立即放入管理对象。“资源取得时机便是初始化时机”。(个人认为如果不第一时间放进管理对象,那么很有可能代码因为某种原因return了,内存就泄漏了。)
- 管理对象运用析构函数确保资源被释放。
auto_ptr的一个性质:
auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向对象
auto_ptr<Investment> pInv2(pInv1); //pInv指向对象,pInv1为null
pInv1 = pInv2; //pInv1指向对象,pInv2为null
这块代码已经注释,也就是aotu_ptr的性质:
如果通过copy构造函数或copy assignment函数复制它们,原来的auto_ptr将变成null,新的指向才指向对象。
原因:
如果让多个auto_ptr指向了同一个对象,那么如果多个auto_ptr的析构函数调用,会对一个对象进行多次删除,但实际上第一次删除之后就不存在了,后面的删除会造成未定义的行为。
2.2 使用“引用计数型智慧指针”替代auto_ptr
也就是shared_ptr:
void f()
{
shared_ptr<Investment> pInv1(createInvestment()); //pInv1指向对象
shared_ptr<Investment> pInv2(pInv1); //pInv2指向对象,pInv1也指向对象
pInv1 = pInv2; //pInv1指向对象,pInv2也指向对象
}
在这个函数里面,shared_ptr允许多个指针指向同一个对象。它会对指针进行计数,直到计数为0的时候才会调用析构函数,删除对象。所以并不会出现多次删除同一个对象的情况。
注意:
auto_ptr和shared_ptr在其析构函数中做的都是delete操作而没有delete[]操作。所以我们要注意别在动态分配的array中使用这两个指针。
auto_ptr<string> aps(new string[10]);
shared_ptr<int> spi(new int[1024]);
这两个操作都是十分危险的。
作者总结:
为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
两个常被使用的RAII classes分别是shared_ptr和auto_ptr.前者是较佳的选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它指向null.
条款14:在资源管理类中小心copying行为
当我们使用shared_ptr的时候,当引用计数变为0时,会删除这个对象。但实际情况里,我们可能不想要删除。
比如一个互斥锁的类,构造的时候就加锁,析构的时候解锁,这样也符合RAII对象的特性。
class Lock
{
public:
explicit Lock(Mutex *pm)
:mutexPtr(pm)
{
lock(mutexPtr); // 获得资源
}
~Lock()
{
unlock(mutexPtr); // 释放资源
}
private:
Mutex *mutexPtr;
}
现在让我们看看,在调用的时候进行copy的行为会造成什么:
Mutex m;
...
{
Lock m1(&m);
...
}
这段代码是正确的行为,在出作用域的时候,析构函数被调用,就会自动解锁。
但是如果客户端出现copy行为:
Lock m1(&m);
Lock m2(m1);
这会造成m2还在使用资源的时候,假如m1析构函数调用了,那么资源就被释放了,m2也就无法使用了。
解决方法:
-
禁止复制。用条款6中所述的使用一个Uncopyable类,通过将拷贝构造声明为private的来禁止调用。
-
对底层资源祭出“引用计数法”。 这个方法要注意shared_ptr在计数为0的时候会删除这个对象,但是我们只需要释放它,所幸shared_ptr允许我们指定一个删除器。我们可以改成:
class Lock
{
public:
explicit Lock(Mutex *pm)
:mutexPtr(pm,unlock)
{
lock(mutexPtr.get()); // 获得资源,get函数是获取原始指针
}
private:
shared_ptrmutexPtr;
}
我们指定看一个unlock函数作为删除器,计数器为0的时候会执行。所以我们无需一个析构函数。 -
复制底部资源。就是执行“深拷贝”。“深拷贝”是会在新的内存中存一份和原数据一模一样的数据,不会使用原来的地址。那么就是两份相同数据存在不同的地方。在这个例子里,就是两份资源了,释放掉原来的资源并不影响新的资源。
-
转移底部资源所有权。 确保只有一个RAII对象指向一个资源,复制时候资源的所有权从被复制物转向目标物。 可以参考auto_ptr的做法,讲原来的指针置为null,确保只有一份资源被使用。
作者总结
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定对象的copying行为。
普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。不过其他行为也都可能被实现。
条款15:在资源管理类中提供对原始资源的访问
-
shared_ptr获得原始指针,只需要调用get函数就可以。
shared_ptr
pInt;
int *p = pInt.get(); -
shared_ptr和auto_ptr都重载了指针取值的方法。
class Investment
{
public:
bool isTaxFree() const;
...
};
此时执行:
shared_ptr<Investment> pInv(createInvestment());
bool taxFree1 = pInv->isTaxFree();
bool taxFree2 = (*pInv).isTaxFree();
以上"->"运算符和"."运算符都是适用的。