容器在STL中被认为是智能的。它们支持向前和向后的迭代器;它们能告诉你它所保存的对象类型(通过typedef value_type);在插入和删除过程中它们进行了良好的内存管理;它们将报告自己包含了多少对象和自己最多能包含多少对象(分别通过size和max_size取得);并且,当容器销毁时,它自动销毁每个被包含的对象。
拥有如此聪明的容器,许多程序员自己不再担心清理问题。他们认为容器会为他们操心。多数情况下,他们正确,但是当容器包括由new生产对象指针时,他们就不是太正确。毫无疑问,指针容器在销毁时,会销毁它所包容的每一个元素,但是指针的“析构函数”只是一个空操作。它不会调用delete。
结果是,以下代码直接导致内存资源泄漏:
void doSomething()
{
vector<Widget*> vwp;
for (int i = 0; i < SOME_MAGIC_NUMBER; ++i)
vwp.push_back(new Widget);
… // use vwp
} //Widgets are leaked here!
当离开vwp的作用范围时,vwp的每一个元素都会被销毁,但是这并不改变new所产生的对象没有被delete这个事实。这个删除动作是程序员的责任,而不是vector的。这其实是一个功能,因为只有程序员才知道指针是否需要删除。
通常,程序员希望它们那样(删除指针)。在那种情况(上例)中,使它发生其实很简单。
void doSomething()
{
vector<Widget*> vwp;
… // as before
for (vector<Widget*>::iterator i = vwp.begin();i != vwp.end(),++i) {
delete *i;
}
}
这能行,如果你不是十分在意它只是“能行”。问题之一是新的for循环做了很多for_each做的事,但它不像for_each一样清析。另一个问题是代码不是异常安全。如果一个异常在vwp填上指针之后,而这些指针还没有删除之前被抛出。资源泄漏再次出现。幸运的是两个问题都可以克服。
修改for_each类似的代码以使用真正的for_each,需要将delete操作置于(仿)函数对象中。这像一个儿童游戏,假设你有一个喜欢与STL一起玩游戏的小孩。
template<typename T>
struct DeleteObject: // Item 40 describes why
public unary_function<const T*, void> { //this inheritance is here
void operator()(const T* ptr) const
delete ptr;
}
};
现在你可以这样做:
void doSomething()
{
… // as before
for_each(vwp.begin(), vwp.end(), DeleteObject<Widget>);
}
不太走运,它要求指明DeleteObject删除的对象类型(这里是:Widget )。令人讨厌,vwp是一个vector<Widget*>,因此DeteleObject删除的当然是Widget指针!这种冗余不只是令人讨厌,因为它可能导致难以检查的bug出现。试想一下,例如,有人故意决定要从string继承:
class SpecialString: public string { ...};
这是一种危险的产物,因为string与其它标准STL容器一样,没有virtual析构函数。公有继承一个没有虚析构函数的对象是C++一个主要的误区(a major C++ no-no)(近一步的细节,在任何一个很好的C++书中都有讨论。在Effective C++中,它被放在Item 14)。不论如何,有人如此作了。考虑一下以下代码的行为:
void doSomething()
{
deque<SpecialString*> dssp;
…
for_each( dssp.begin(), dssp.end(), // undefined behavior! Deletion
DeleteObject<string>()); //of a derived object via a base
} // class pointer where there is
//no virtual destructor
注意dssp声明为保存SpecialString的指针,但是for_each循环的作者告诉DeleteObject,它准备删除string的指针。很容易发现什么样的错误会发生。SpecialString无疑在很大程度上表现为string。因此有人会忘记它的用户,他们会不记得他们使用的是SpecialString而不是string。
可以排除这个错误(也可以减少DeleteObject用户敲打键盘的次数)使用编译器推绎出传给DeleteObject::operator()的指针类型。所有工作只是把模板从DeleteObject类移到operator()上。
struct DeleteObject { // templatization and base
// class removed here
template<typename T> II templatization added here
void operator()(const T* ptr) const
{
delete ptr;
}
}
编译器知道传递给DeleteObject::operator()的指针类型,因此它将自动为该类型指针生成一个operator的实例。这种类型推绎的产生,取决于我们放弃DeleteObject的可适应性。考虑一下DeleteObject被设计为如何使用,就很难找出可能发生问题的地方。
使用这一新版的DeleteObject,SpecialString客户的代码看起来像这样:
void doSomething()
{
deque<SpecialString*> dssp;
…
for_each( dssp.begin(), dssp.end(),
DeleteObject ()); // ah! well-defined behavior!
}
直接而且类型安全,就像我们所喜欢的那样。
但是它还不是异常安全。如果SpecialString生产了,但还没有调用for_each,一个异常被抛出,泄漏将出现。这个问题可以用很多方法解决,但最简单的也许是使用智能指针容器取代指针容器,通常使用一个引用记数的智能指针(如果不熟悉智能指针的概念,可以在中高级C++读物中找到。在More Effective C++中,这些材料在Item 28。)
STL本身并不包括引用记数的智能指针,编写一个好的-所有情况下都正确-太富有技巧,因此除非真的需要,并不需要这样做。我(作者)1996年在More Effective C++发布了了一个引用记数的智能指针,尽管它基于一些确定的智能指针实现,而且在发布前由多位有经验的开发者讨论过,但是这些年还是有一堆准确的Bug被发现。很多引用记数的智能指针可能失败的微妙情况被说明。(细节在More Effective C++勘误中讨论)
幸运地,几乎不需要自己写一个智能指针,因为已验正的实现并不难找。在Boost库(参考条款50)中就有一个这样的share_ptr。使用Boost的share_ptr,本条款最初的例子可以重写为:
void doSomething()
{
typedef boost::shared_ ptr<Widget> SPW; //SPW = "shared_ptr
// to Widget"
vector<SPW> vwp;
for (int i = 0; i < SOME_MAGIC_NUMBER; ++i)
vwp.push_back(SPW new Widget); // create a SPW from a
// Widget*, then do a
//push_back on it
… // use vwp
} // no Widgets are leaked here, not
// even if an exception is thrown
//in the code above
千万不能被auto_ptr愚弄了,不要认为创建auto_ptr的容器,指针会被自动删除。这是可怕是想法,它是如此危险,我准备用整个条款8来说明你不应使用它。
应记住的是STL容器是智能的,但它不足以知道是否要删除它包含的指针。为了避免资源泄漏,使用指针容器时应删除指针。你需要使用智能指针或在容器销毁前手工删除每一个指针。
最后,一个类似于DeleteObject的结构可以方便地避免使用指针容器时的资源泄漏,这也许会使你联想起,也许可能创建一个类似的DeleteArray,避免使用数组指针容器时的资源泄漏。当然,这是可能的,但是是否明智就是另一个问题了。条款13解释了为什么动态申请数组总是不如vector和string对象。所以在你坐下来写DeleteArray之前,请先看一看条款13。如果幸运,DeleteArray的时代将永远不会到来。