读书笔记中涉及到的所有代码实例可通过https://github.com/LuanZheng/EffectiveCPlusPlus.git进行下载获得。
Item13: 以对象管理资源
先看一下下面的例子:
void f()
{
Investment* pInv = createInvestment();
...
delete pInv;
}
如果…语句里面产生异常,或者有过早的return语句等,那么,delete语句将得不到执行的机会,因此,会造成内存的泄露。
为确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放哪些资源。
void f()
{
std::auto_ptr<Invesment> pInv(createInvestment());
}
获得资源后立刻放进管理对象内。
管理对象运用析构函数确保资源被释放。
由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr指向同一对象。
auto_ptr有一个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null.而复制所得的指针将取得资源的唯一使用权!
#include <memory>
#include <iostream>
#include "SimpleFactory.h"
int main()
{
if (true)
{
//Investment *pInv = SimpleFactory::getInstance()->createInv(); //这种做法将导致可能的内存泄露
std::auto_ptr<Investment> pInv(SimpleFactory::getInstance()->createInv()); //以对象管理资源(智能指针),在对象析构时,会自动释放资源
std::auto_ptr<Investment> pInv1(SimpleFactory::getInstance()->createInv()); //以对象管理资源(智能指针),在对象析构时,会自动释放资源
std::auto_ptr<Investment> pInv2(pInv1); //以对象管理资源(智能指针),在对象析构时,会自动释放资源
pInv1 = pInv2;
std::cout << "Auto Ptr will be finished." << std::endl;
}
else
{
}
std::cout << "Finished." << std::endl;
return 0;
}
auto_ptr的替代方案是“引用计数型智慧指针(RCSP)”。所谓RCSP也是个指针类型,持续追踪共有多少个对象指向某笔资源,并在无人指向它时自动删除该资源。
#include <memory>
#include <iostream>
#include "SimpleFactory.h"
void AutoPtrVersion()
{
//Investment *pInv = SimpleFactory::getInstance()->createInv(); //这种做法将导致可能的内存泄露
std::auto_ptr<Investment> pInv(SimpleFactory::getInstance()->createInv()); //以对象管理资源(智能指针),在对象析构时,会自动释放资源
std::auto_ptr<Investment> pInv1(SimpleFactory::getInstance()->createInv()); //以对象管理资源(智能指针),在对象析构时,会自动释放资源
std::auto_ptr<Investment> pInv2(pInv1); //拷贝构造函数,会导致pInv1变成null,pInv2变成所指的对象
pInv1 = pInv2; //赋值运算符,会导致pInv2变成null,pInv1变成所指的对象
std::cout << "Auto Ptr will be finished." << std::endl;
}
void RCSPVersion()
{
std::tr1::shared_ptr<Investment> pInv1(SimpleFactory::getInstance()->createInv());
std::tr1::shared_ptr<Investment> pInv2(SimpleFactory::getInstance()->createInv());
pInv1 = pInv2; //不会有auto_ptr的奇怪行为,pInv1,pInv2均指向同一内存(pInv2),pInv1的原来内存会被释放
std::cout << "RCSP will be finished." << std::endl;
}
int main()
{
if (true)
{
AutoPtrVersion();
RCSPVersion();
}
else
{
}
std::cout << "Finished." << std::endl;
return 0;
}
auto_ptr和tr1::shared_ptr两着都在析构函数中做delete,而不是delete[].因此,在动态分配的array身上使用auto_ptr或tr1::shared_ptr是个馊主意。这类动态分配的数组,可以利用vector和string来取代。
例子见Item13
Item14 在资源管理类中小心copying行为
资源管理类:构造时获得资源,析构时释放资源。
class Lock
{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm)
{lock(mutexPtr)};
~Lock() {unlock(mutexPtr);}
private:
Mutex *mutexPtr;
}
资源管理类与智能指针的区别是,智能指针需要在heap上分配,然而并非所有的资源都是分配在heap上的,对于分配在stack上的资源,就适用与资源管理类了。
当一个RAII对象被复制,大多数时候你会选择以下两种可能:
禁止复制
包括将拷贝构造函数和拷贝复制运算符定义为private,继承Uncopyable基类等等。
对底层资源祭出“引用计数法”
有时候我们希望保有资源,知道它的最后一个使用者被销毁。这种情况下复制RAII对象时,应该将资源的“被引用数”递增。
例子见Item13
Item15 在资源管理器类中提供对原始资源的访问
在现实情况中,许多API直接指涉资源,因此,常常不得不绕过资源管理对象直接访问原始资源。
如下面daysHeld方法
因此需要提供显示转换来获取原始指针。
Investment* Investment::get()
{
return this;
}
另外,tr1::shared_ptr和auto_ptr也重载了指针取值操作符(->和*),他们允许隐式转换至底部原始指针。在使用中,可以像原始指针一样(但类型不是原始指针类型)。
例子见Item13
Item16 成对使用new和delete时要采取相同形式
new <-> delete
new[] <-> delete []
要成对使用。
当撰写的构造函数中进行内存动态分配时,要特别注意各个重载的构造函数使用同样的new/new[],否则析构函数将无法决定使用那个delete/delete[].
typedef std::string AddressLines[4];
std::string* pa1 = new AddressLines; //实际上是new string[4]
则在销毁内存时,需要: delete [] pa1; //正确
而非 : delete pa1; //错误
为了避免错误,最好尽量不要对数组形式做typedef动作。取而代之,尽量使用string, vector等。
Item17 以独立语句将newed对象置入智能指针
研究以下代码:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
该行代码将做三件事情:
执行priority()
执行 new Widget
执行tr1::shared_ptr构造函数
这三步的执行顺序无法确定,可以确定的是,new Widget一定在tr1::shared_ptr的构造函数之前执行。
若按照如下的执行顺序,
执行 new Widget
执行priority()
执行tr1::shared_ptr构造函数
一旦在priority()执行过程中抛出异常,由于此时Widget尚未放置入对象中,则会导致内存泄露。
因此,我们应该做如下改进:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
则可保证执行顺序,保证以对象管理资源,从而避免异常导致的内存泄露。
例子见Item13