简单来说就是要用类来管理资源,最好使用C++11新标准提供的几种智能指针
请记住:
RAII:RAII即 “资源获取即初始化”,它是一种方法,定义一个类来封装资源的分配和释放,在类的构造函数中来实现资源的分配和初始化,在类的析构函数中实现资源的释放和清理,是C++中一种管理资源,避免资源泄漏的方法,因为在C++中任何类构造的对象最终都会调用析构函数销毁
在上述中讲述了基于堆分配的资源管理,然而并非所有资源都是基于堆的,对那种资源而言,像auto_ptr和shared_ptr这样的智能指针往往不适合作为资源掌管着。既然如此,有可能你需要建立自己的资源管理类
假设使用C API函数处理类型为Mutex的互斥器对象,共有lock和unlock两函数可用:
void lock(Mutex &m);
void unlock(Mutex &m);
为确保绝不会忘记将一个被锁住的Mutex解锁,你可能会希望建立一个类用来管理锁。
class Lock{
public:
// 获得资源
explicit Lock(Mutex &pm) : mutexPtr(pm){ lock(mutexPtr); }
// 释放资源
~Lock(){ unlock(mutexPtr); }
private:
Mutex *mutexPtr;
};
// 客户对Lock的用法符合RAII方式
Mutex m;
{ Lock m1(&m); } // 在区块最末尾,自动解锁
这很好,但是如果Lock对象被复制,会发生什么事
Lock m11(&m);
Lock m12(m11);
这是一个一般化问题的特定例子,大多数时候会选择以下两种可能:
class Lock{
public:
// 以某个Mutex初始化shared_ptr,并以unlock函数为删除器
explicit Lock(Mutex &pm) : mutexPtr(pm, unlock){ lock(mutexPtr.get()); }
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
请记住:
简单来说,就是在自己设计的资源管理类中要提供对其所指或所含资源的访问方法,而不是直接处理原始资源。这样你对资源的所有操作就不会离开设计的资源管理类,也会更加安全。
考虑以下代码:
class Investment { };
std::tr1::shared_ptr<Investment> pInv(createInvestment());
// 假设你希望以某个函数处理Investment对象
int daysHeld(const Investment* pi); // 返回投资天数
int days = daysHeld(pInv); // 这样使用会报错,因为函数的形参与实参不匹配
解决方法有两种,分别是显示类型转换和隐式类型转换:
int days = daysHeld(pInv.get()); // 这样就不会报错
由于有时候还是必须取得RAII对象内的原始资源,于是我们可以在类里面提供一个隐式转换函数。考虑以下代码:
FontHandle getFont(); // 这是个C API,用来获取字体
void releaseFont(FontHandle fh); // 释放字体
class Font{
public:
explicit Font(FontHandle fh) : f(fh) { }
~Font() { release(f); }
private:
FontHandle f;
假设有大量与字体相关的C API,它们处理的是FontHandle,那么“将Font对象转换为FontHandle”会是一种很频繁的需求。除了可以在 Font 类里面定义一个显示类型转换函数外,还可以提供一个隐式类型转换函数:
class Font{
public:
...
operator FontHandle() const { return f; } // 隐式类型转换
...
};
// 在调用时我们就可
Font f(getFont());
int newFontSize;
changeFontSize(f, newFontSize); // 将Font隐式转换为FontHandle
但这样的转换会增加错误发生的机会,例如
Font f1(getFont());
FontHanle f2 = f1; // 此时我们希望复制一个Font,但是f1可能隐式地转换为了FontHandle类型
// 此时由f1管理的对象也可以通过f2直接取得,那么如果当f1或f2被销毁时,
// 另外一个就悬空了
是否该提供一个显示转换函数将 RAII 类转换为其底部资源,或是应该提供隐式转换,答案主要取决于 RAII 类被设计执行的特定工作,以及它被使用的情况
最好是使用显示类型转换,因为它将 “非故意的类型转化” 的可能性最小化了,但是有时候隐式类型带来的方便也会更适用
请记住:
简单来说就是 new 一个数组的时候,别忘了要采用 delete [] 的形式
delete怎么知道它要删除的是单个对象还是一个对象数组呢。原因在于单一对象和对象数组的内存结构不同,对象数组的内存结构里面还存储了数组的长度(size),以便 delete 知道需要调用多少次析构函数,单一对象的内存则没有这条记录
当你使用 typedef 关键字定义对象数组时要注意使用 delete [],例如:
typedef std::string AddressLines[4];
std::string *pal = new AddressLines; // 注意此时new返回的是一个数组
delete pal; // 错误,行为未定义
delete [] pal; // 正确
为避免此类错误,最好不要对数组进行 typedef 动作
请记住:
考虑一个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的 Widget 上进行某些带有优先权的处理:
class Widget{
public:
explicit Widget();
};
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority());
// 现在我们进行如下调用
processWidget(new Widget, priority());
这样调用会报错,因为 Widget 的构造函数是个 explicit ,不能进行隐式类型转换,此时我们可以
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
这样可以编译通过,但可能会发生内存泄漏的问题,原因在于 C++ 对函数传递参数的核算顺序并没有明确规定。上面这个函数有两个传入参数,第二个实参只是单纯的调用 priority 函数,但第一个实参由两步组成:
于是编译器可能会采取以下的执行顺序:
如果在第二步执行 priority 的过程中发生错误,则 new 出来的指针将会遗失,因为它并没有被装入智能指针中,因为在 “资源被创建” 和 “资源被转换为资源管理对象” 两个时间点之间有可能发生异常干扰
解决办法就是将这两个实参的核算语句分离开,如:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
这样就不会发生错误,因为编译器对于 “跨越语句的各项操作” 没有重新排列的自由。
请记住: