使用的资源就必须还给系统, 常见的资源:内存, 文件描述器(file descriptors)、互斥锁(mutex locks)、网络sockets、图形界面中的字型和笔刷、数据库连接。
以对象管理资源
把资源放入对象中,这样就可以使用C++的“析构函数自动调用机制”确保资源被释放。许多资源被动态分配于heap内而后被用于单一区块或函数内,他们应该再控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形式设计的特制产品, auto_ptr是一个“类指针(pointer-like)”对象, 也就是所谓的“智能指针”, 其析构函数自动对其所指对象调用delete。举例
void f( )
{
std::auto_ptr<Investment> pInv(creatInvestment( ));
//调用factory函数
//使用pInv
//经由auto_ptr的析构函数自动删除pInv
}
两个关键想法:
1.获得资源后立即放进管理对象(managing object)内。
资源获得时机就是初始化时机(Resource Acquisition Is Initialization, RAII)
2.管理对象中运用析构函数确保资源被释放
注意:由于auto_ptr会自动删除所指之物, 所以不能多个auto_ptr指向同一个对象。因此,如果通过copy构造函数或者copy assignment操作符复制他们, 他们会变成null。
auto_ptr的替代方案, shared_ptr, “引用型计数型智能指针”(reference-counting smart pointer,RCSP),持续追踪有多少对象指向某个资源, 无人指向时自动删除。
void f( )
{
...
std::trl::shared_ptr<Investment>
pInv(createInvestment);
...
}
其中auto_ptr和shared_ptr都是在析构函数内做delete而不是delete[]动作, 所以动态分配而得的array上使用时会出现问题。
在资源管理类中小心copying行为
假设我们建立自己的资源管理类,例如,使用C API函数处理类型为Mutex的互斥器对象(mutex objects), 共有lock和unlock两函数可用:
void lock(mutex* pm);
void unlock(mutex* pm);
为确保不会忘记将一个锁住的mutex解锁, 需要建立一个class来管理锁, 这样类的基本结构由RAII守则支配。
class Lock{
public:
explicit Lock(Mutex* pm)
: mutexPtr(pm)
{ lock(mutexPtr);}
~Lock() (unlock(mutexPtr); }
private:
Mutex *mutexPtr;
};
客户对Lock的用法
Mutex m; //定义需要的互斥器
...
{//建立一个区块用来了定义critical sect
Lock m1(&m);//锁定互斥器
...//执行critical section内的操作
}//在区块最末尾
但是当Lock对象被复制时
Lock ml1(&m); //锁定m
Lock ml2(ml1);
当一个RAII对象被复制时,会产生什么后果:
1.禁止复制
许多时候允许RAII对象被复制并不合理,因为很少能够合理拥有“同步化基础器物”(synchronization primitives)的复件。如果复制动作对RAII class不合理,就应该禁止之。将copying声明为private
class Lock: private Uncopyable{
public:
...
};
2.对底层资源祭出”引用计数法“
通常只要内含一个trl::shared_ptr成员变量,RAII classes便可以实现出reference-counting copying行为。然而将Mutex*改变为trl::shared_ptr < Mutex >,然而我们希望的是解除锁定而不是删除, 所以我们使用shared_ptr调用指定的所谓”删除器“(deleter), 即一个函数或者函数对象(function object),删除器对shared_ptr构造函数而言是可有可无的第二参数,实例
class Lock {
public:
explicit Lock(Mutex* pm)
: mutexPtr(pm, unlock)
{
lock(mutexPtr.get());
}
private:
std::trl::shared_ptr<Mutex> mutexPtr;
}
本例的Lock class不再声明析构函数, 没有必要。mutexPtr的析构函数会在引用次数为0时自动调用trl::shared_ptr的删除器
复制底部资源
复制资源管理对象(当不需要某个复件时确保它被释放)时, 应该复制其所包裹的资源,复制资源管理对象, 进行的"深度拷贝"。
转移底部资源的拥有权
某些罕见场合可能确保一个RAII对象指向一个未加工资源(raw resource),即使RAII对象被复制时依然如此。此时资源的所有权会从被复制物转移到目标物, 这也是auto_ptr的复制意义。(同一个对象只能有一个auto_ptr指向,其它的都变为null)
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为
==普遍常见的RAII class copying行为时:一直copying、实行引用计数法(reference counting)。不过其它行为也都可能被实现。
在资源管理类中提供对原始资源的访问
避免资源泄露是良好设计系统的根本性质
举例
std::trl::shared_ptr<Investment> pInv(createInvestment());
假设希望某个函数处理Investment对象, 如题:
int daysHeld(const Investment* pi); //返回投资天数
int days = daysHeld(pInv); //错误
int days = daysHeld(pInv.get()); //将pInv的内部原始指针传给daysHeld
原因,daysHeld需要的是Investment*指针, 两个做法, 显示转换或者隐式转换。shared_ptr和auto_ptr都可以通过get()函数来执行显式转换。
shared_ptr和auto_ptr都重载了指针取值操作符(pointer dereferenceing),(->, *)。
class Investment{ //Investment继承体系的根类
public:
bool isTaxFree() const;
...
};
Investment* createInvestment(); //工厂函数
std::trl::shared_ptr<Investment>; //令trl::shared::ptr
pil(createInvestment( )); //管理一笔资源
bool taxable1 = !(pil->isTaxFree()); //经由operator->访问资源
...
std::auto_ptr<Investment> pi2(createInvestment()); //领auto_ptr管理资源
bool taxable2 = !((*pi2).isTaxFree());//经由operator*访问资源
隐式转换
FontHandle getFont(); //暂时忽略参数
void releaseFont(FontHandle fh); //来自同一组C API
class Font{ //RAII class
public:
explicit Font(FontHandle fh) //获得资源
: f(fh) //pass-by-value
{ } //因为C这样做
~Font( ) { releaseFont(f ); } //释放资源
private:
FontHandle f; //原始(raw)字体资源
};
假设大量与字体相关的C API, 他们处理的是FontHandle, 那么将Font转换为FontHandle是一种非常频繁的需求。Font函数可以提供显示的转换函数:
class Font {
public:
...
FontHandle get() const { return f; } //显式转换函数
...
};
然而到处使用显示转换可能比较繁琐, 不愿意被人使用,从而造成资源泄露的风险(Font就是被设计用于防止资源泄露)。
因此提供隐式转换:
class Font {
public:
...
operator FontHandle() const //隐式转换函数
{return f;}
...
};
这样客户调用C API时轻松且自然:
Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontsize);//Font被隐式转换为FontHandle
但是可能会增加错误发生的机会
Font f1(getFont( ));
...
FontHandle f2 = f1; //原意拷贝一个Font对象, 但是f1被隐式转换为FontHandle才被复制。
APIs往往要求访问原始资源(raw resources), 所以每一个RAII class应该提供一个“取得所管理资源”的办法。
对原始资源的访问可能是通过显示转换或者隐式转换, 显示转换比较安全, 隐式转换对客户访问比较方便。
成对使用new和delete时要采取相同形式
如下所示:
std::string* stringArray = new std::string[100];
...
delete stringArray;
程序行为不明确,stringArray中的100个对象中的99个不太可能被适当删除,因为他们的析构函数可能没有被调用。
当使用new(使用new动态生成一个对象),有两件事情发生, 第一,内存被分配出来。第二, 针对此内存会有一个(或更多)构造函数被调用。当使用delete时,内存中先有一个或多个析构函数被调用, 然后内存被释放。delete的最大问题在于,将要删除的内存中有多少对象,或者说,即将被删除的那个指针, 所指的是单一对象还是对象数组。。数组所用的内存通常还包括“数组大小”的记录, 以便delete知道需要调用多少次析构函数。单一对象的内存则没有次记录。
==当使用delete时加上“[]”, delete便认定指针指向一个数组,否则它便认定指针指向单一对象。
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1; //删除一个对象
delete [ ] stringPtr2; //删除一个由对象组成的数组
对stringPtr1使用“delete []”格式, 未定义,delete可能调用多次析构函数,删除一个并不存在的对象。
对stringPtr2使用“delete []”格式, 未定义,可能过少的析构函数被调用
当调用new时使用[], 则调用delete时也使用[], 当调用new时没有使用[], 那么调用delete时也不适用[]。
当以new创造某种typedef对象时, 应当说明以哪一种delete删除之,如下所示:
typedef std::string AssressLines[4]; //每个人地址4行, 每行为一个string
由于AddressLines是个数组, 如果这样使用new:
std::string* pal = new AddressLines; //注意, “new AddressLines”返回一个
//string*,就像"new string[4]"一样
那就必须匹配“数组形式”的delete:
delete pal; //行为没有定义
delete [ ] pal;//很好
为避免诸如此类的错误, 尽量不要对数组形式做typedef动作。
以独立语句将newed对象置入智能指针
举例:
假设有某个函数用来揭示处理程序的优先权,另一个函数用来在动态分配所得的widget上进行某些带有优先权的处理
int priority();
void processWidget(std::trl::shared_ptr<Widget> pw, int priority);
但是如果如此调用processWidget:
processWidget(new Widget, priority());
上述调用将无法通过编译,trl::shared_ptr的构造函数需要一个原始指针, 但是该构造函数是一个explicit函数,无法进行隐式转换,用如下形式书写可以通过编译:
processWidget(std::trl::shared_ptr<Widget>(new Widget), priority());
当编译器产出一个processWidget调用码之前,必须首先核算即将被传递的各个实参。编译器创建代码,做以下三件事:
- 调用priority
- 执行“new Widget”
- 调用trl::shared_ptr构造函数
c++编译器完成这些时间的先后次序弹性很大(Java或者C#就有确定的次序)。“new Widget”肯定先于trl::shared_ptr构造函数被调用之前, 但是对priority的调用顺序可以随意。当priority被编译器选择在第二位进行执行时:
- 执行“new Widget”
- 调用priority
- 调用trl::shared_ptr构造函数
当priority调用异常时,"new Widget"返回的指针会遗失, 因为它还没有被植入trl::shared_ptr中, 后者则是用来防止资源泄露的, 因此上文的调用形式虽然能够通过编译,但是在"资源被创建"(经由"new Widget")和"资源被转换为资源管理对象"两个时间蒂娜之间有可能发生异常干扰。
解决方法:分离语句
使用分离语句,先将其置入一个智能指针内, 然后再把那个智能指针传给processWidget:
std::shared_ptr<Widget> pw(new Widget); //在单独语句以智能指针存储
processWidget(pw, priority()); //这个调用动作不至于会造成泄露
此操作不会让编译器有重新排列各项操作的自由度。所以我们需要用独立语句将newed对象存储于智能指针内, 如果不这样做, 一旦异常被抛出, 有可能导致难以察觉的资源泄露。