3. 资源管理
条款13 以对象管理资源
void>
{
Investment *pInv =>//这里存在诸多“不定因素”,可能造成delete pInv;得不到执行,这可能就存在潜在的内存泄露。
>auto_ptr正是针对这种形势而设计的特制产品。 auto_ptr是个“类指针对象”,也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。
void>
{
std::auto_ptr<Investment> pInv(createInvestment());
...
} //函数退出, auto_ptr调用析构函数自动调用delete,删除pInv;无需显示调用delete。
“以对象管理资源”的 两个关键想法:
看下面例子:
std:: auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std:: auto_ptr<Investment> pInv2(pInv1); //现在pInv2指向对象,而pInv1被设为NULL;
> //现在pInv1指向对象,而pIn2被设为NULL;
受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它。即“ 有你没我,有我没你”。
auto_ptr的替代方案是“引用计数型智能指针”(reference-counting>
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::tr1::shared_ptr<Investment> pInv2(pInv1); // pInv1, pInv2指向同一个对象 ;
> //同上,无变化
...
} //函数退出,pInv1, pInv2被销毁,它们所指的对象也竟被自动释放。
>请记住:
{
Investment *pInv =>//这里存在诸多“不定因素”,可能造成delete pInv;得不到执行,这可能就存在潜在的内存泄露。
>auto_ptr正是针对这种形势而设计的特制产品。 auto_ptr是个“类指针对象”,也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。
void>
{
std::auto_ptr<Investment> pInv(createInvestment());
...
} //函数退出, auto_ptr调用析构函数自动调用delete,删除pInv;无需显示调用delete。
“以对象管理资源”的 两个关键想法:
- 获得资源后立刻放进管理对象内(如auto_ptr)。每一笔资源都在获得的同时立刻被放进管理对象中。“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII)。
- 管理对象运用析构函数确保资源被释放。即一旦对象被销毁,其析构函数被自动调用来释放资源。
看下面例子:
std:: auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std:: auto_ptr<Investment> pInv2(pInv1); //现在pInv2指向对象,而pInv1被设为NULL;
> //现在pInv1指向对象,而pIn2被设为NULL;
受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它。即“ 有你没我,有我没你”。
auto_ptr的替代方案是“引用计数型智能指针”(reference-counting>
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::tr1::shared_ptr<Investment> pInv2(pInv1); // pInv1, pInv2指向同一个对象 ;
> //同上,无变化
...
} //函数退出,pInv1, pInv2被销毁,它们所指的对象也竟被自动释放。
>请记住:
- 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
- 两个常被使用的RAII类分别是auto_ptr和tr1::shared_ptr。后者通常是较佳选择,因为其拷贝行为比较直观。若选择auto_ptr,复制动作会使他(被复制物)指向NULL
条款14:在资源管理类中小心拷贝行为
我们在条款13中讨论的资源表现在堆上申请的资源,而有些资源并不适合被auto_ptr和tr1::shared_ptr所管理。可能我们需要建立自己的资源管理类。
例:
>
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
但是,如果Lock对象被复制,会发生什么事???
“当一个RAII对象被复制,会发生什么事?”大多数时候你会选择一下两种可能:
>
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm, unlock)
{
我们在条款13中讨论的资源表现在堆上申请的资源,而有些资源并不适合被auto_ptr和tr1::shared_ptr所管理。可能我们需要建立自己的资源管理类。
例:
>
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
但是,如果Lock对象被复制,会发生什么事???
“当一个RAII对象被复制,会发生什么事?”大多数时候你会选择一下两种可能:
- 禁止复制。如果复制动作对RAII类并不合理,你便应该禁止之。禁止类的copying函数参见条款6。
- 对底层资源使用”引用计数法“。有时候我们又希望保有资源,直到它的最后一个使用者被销毁。这种情况下复制RAII对象时,应该将资源的”被引用计数“递增。tr1::shared_ptr便是如此。
>
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm, unlock)
{
//由于tr1::shared_ptr缺省行为是”当引用计数为0时删除其所指物“,幸运的是
//我们可以指定”引用计数“为9时被调用的所谓”删除器“,即第二个参数
unlock
lock(mutexPtr .get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
本例中,并没说明析构函数,因为没有必要。编译器为我们生成的析构函数会自动调用其non-static成员变量(mutexPtr)的析构函数。而mutexPtr的析构函数会在互斥量”引用计数“为0时自动调用tr1::shared_ptr的删除器(unlock)。
Copying函有可能被编译器自动创建出来,因此除非编译器所生成版本做了你想要做的事,否则你得自己编写它们。
请记住:
lock(mutexPtr .get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
本例中,并没说明析构函数,因为没有必要。编译器为我们生成的析构函数会自动调用其non-static成员变量(mutexPtr)的析构函数。而mutexPtr的析构函数会在互斥量”引用计数“为0时自动调用tr1::shared_ptr的删除器(unlock)。
Copying函有可能被编译器自动创建出来,因此除非编译器所生成版本做了你想要做的事,否则你得自己编写它们。
请记住:
- 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
- 普遍而常见的RAII类拷贝行为是:抑制拷贝,施行引用计数法。不过其它行为也可能被实现。
条款15:
// 显示转换
FontHandle getFont();
void releaseFont(FontHandle fh);
// Font 类
class Font
{
public:
explicit Font(FontHandle fh):f(fh) {} // 禁止 FontHandle 隐式转换为 Font
~Font() { releaseFont(f); }
// 提供 访问 FontHandle 的显示访问函数
// 显示 装换
FontHandle get() const { return f; }
private:
FontHandle f;
};
// 对于客户比较方便就是封装一个隐式转换
class Font
{
public:
explicit Font(FontHandle fh):f(fh) {} // 禁止 FontHandle 隐式转换为 Font
~Font() { releaseFont(f); }
// 隐式转换
operator FontHandle() const
{
return f;
}
private:
FontHandle f;
};
- APIs往往要求访问原始资源,所以每一个RAII类应该提供一个“取得其所管理之资源”的方法。
- 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便
条款16:成对使用new和delete时要采取相同形式
先看下一下代码:
> ...
delete stringArray;
使用了new动态申请了资源,也调用了delete释放了资源。但这代码存在“不明确行为”。stringArray对象中的99个不太可能被适当删除,因为它们的析构函数很可能没被调用。
当我们使用new,有两件事情发生:第一,内存被分配出来;第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或多个)析构函数被调用,然后内存才被释放。 delete的最大问题在于:即将被删除的内存之内究竟有多少对象?这个问题的答案决定了有多少个析构函数必须被调用起来。
解决以上问题事实上很简单:如果你调用new时使用[],你必须在对应调用delete时也使用[]。如果你调用new时没有使用[],那么也不该在对应调用delete时使用[]。
最好尽量不要对数组形式作typedefs动作。因为这样容易引起delete操作的“疑惑”(需不需要[]呢???)。
请记住:
先看下一下代码:
> ...
delete stringArray;
使用了new动态申请了资源,也调用了delete释放了资源。但这代码存在“不明确行为”。stringArray对象中的99个不太可能被适当删除,因为它们的析构函数很可能没被调用。
当我们使用new,有两件事情发生:第一,内存被分配出来;第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或多个)析构函数被调用,然后内存才被释放。 delete的最大问题在于:即将被删除的内存之内究竟有多少对象?这个问题的答案决定了有多少个析构函数必须被调用起来。
解决以上问题事实上很简单:如果你调用new时使用[],你必须在对应调用delete时也使用[]。如果你调用new时没有使用[],那么也不该在对应调用delete时使用[]。
最好尽量不要对数组形式作typedefs动作。因为这样容易引起delete操作的“疑惑”(需不需要[]呢???)。
请记住:
- 如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。
条款17:以独立语句将newed对象置入智能指针
为了避免资源泄漏的危险,最好在单独语句内以智能指针存储newed所得对象。
即:
即:
int priority();
void processWidget( std::tr1::shared_ptr<Widget> pw, int priority);
void processWidget( std::tr1::shared_ptr<Widget> pw, int priority);
或者
processWidget(std::tr1::share_ptr<Widget>(new Widget), priority());
编译器不一定会先调用priority, 再调用new Widget, 最后调用 std::tr1::share_ptr
如果先调用new Widget, 再调用priority, 当priority()执行出问题了,则会造成原先new的对象得不到释放,造成内存泄露。
所以
std::tr1::shared_ptr<Widget> pw(new Widget); //即在传入函数之前对智能指针初始化,而不是在传入参数中 //对其初始化,因为那样可能引起操作序列的问题。
processWidget(pw, priority());
请记住:
std::tr1::shared_ptr<Widget> pw(new Widget); //即在传入函数之前对智能指针初始化,而不是在传入参数中 //对其初始化,因为那样可能引起操作序列的问题。
processWidget(pw, priority());
请记住:
- 以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。