标准模板库内存释放问题
和标准C++运行库中的绝大部分东西一样,标准容器类是用类型来参数化的:你能创建一个std::vector来容纳int类型的对象,创建一个std::vector来容纳string对象,创建一个std::vector来容纳用户自定义类型的对象。创建std::vector、std::vector或std::vector也是完全合理的。容纳指针的容器很重要也很常见。
·
和标准C++运行库中的绝大部分东西一样,标准容器类是用类型来参数化的:
你能创建一个std::vector<int>来容纳int类型的对象,
创建一个std::vector<std::string>来容纳string对象,
创建一个std::vector<my_type>来容纳用户自定义类型的对象。
创建std::vector<int *>、std::vector<std::string *>
或std::vector<my_type *>也是完全合理的。
容纳指针的容器很重要也很常见。
不幸地是,虽然是常见技术,容纳指针的容器对新手来说也是造成混乱的
最常见的根源之一。几乎没有哪个星期在C++新闻组中不出现这样的贴子的:
为什么这样的代码导致内存泄漏:
{
std::vector<my_type*> v;
for (int i = 0; i < N; ++i)
v.insert(new my_type(i));
...
} // v is destroyed here
这个内存泄漏是编译器的bug吗?std::vector的析构函数不是会销毁v的元素的吗?
如果你仔细想过std::vector<T>大体上是如何工作的,并且你了解其中对指
针并没有特别的规则的话--也就是说,对vector来说,my_type *只不过是另外
一个T--就不难明白为什么vector<my_type *>有这样的行为以及为什么这段代码
有内存泄漏了。然而,vector<my_type *>的行为可能会令对旧的容器库更熟悉的人感到
惊讶的。
这篇文章解释了容纳指针的容器的行为是怎么样的,什么时候容纳指针的容器会
有用,和在需要执行比标准容器在内存管理上的默认行更多的任务时,该做些什么。
容器和所有权
标准容器使用值语义。举例来说,当你向一个vector附加一个变量x时:
v.push_back(x)
你实际正在做的是附加x的一个拷贝。这个语句存储了x的值(的一个拷贝),而不是x的
地址。在你将x加入一个vector后,你能对x做如何想做的事--比如赋给它一个新值或
让它离开生存域而销毁--而不影响vector中的拷贝。一个容器中的元素不能是另外一
个容器的元素(两个容器的元素必须是不同的对象,即使这些元素碰巧相等),并且将
一个元素从容器中移除将会销毁这个元素(虽然具有相同的值的另外一个对象可能存在
于别处)。最后,容器“拥有”它的元素:当一个容器销毁时,其中的所以元素都随它
一起销毁了。
这些特性与平常的内建数组很相似,并且可能是太明显了而不值一提。我列出它们
以清楚显示容器和数组有多么相似。新手使用标准容器时发生的最常见的概念是认为容
器“在幕后”做了比实际上更多的事。
值语义不总是你所需要的:有时你需要在容器中存储对象的地址而不是拷贝对象的
值。你能以和数组相同的方式,用容器实现引用语义:藉由显式要求。你能将任何类型
的对象放入容器[注1],而指针自己就是非常好的对象。指针占用内存;能被赋值;自己
有地址;有能被拷贝的值。如果你需要存储对象的地址,就使用容纳指针的容器。不再
是写:
std::vector<my_type> v;
my_type x;
...
v.push_back(x);
你能写:
std::vector<my_type*> v;
my_type x;
...
v.push_back(&x);
感觉上,没有任何变化。你仍然正在创建一个std::vector<T>;只不过现在T碰巧是
一个指针类型,my_type *。vector仍然“拥有”它的元素,但你必须明白这些元素是什
么:它们是指针,而不是指针所指向的东西。
拥有指针和拥有指针所指的东西之间的区别就象是vector与数组或局部变量。假如你写:
{
my_type* p = new my_type;
}
当离开代码域时,指针p将会消失,但它所指向的对象,*p,不会消失。如果你想
销毁这对象并释放其内存,你需要自己来完成,显式地写delete p或用其它等价的方法。
同样,在std::vector<my_type *>中没有任何特殊代码以遍历整个vector并对每个元素
调用delete。元素在vector消失时消失。如果你想在那些元素销毁前发生另外一些事,
你必须自己做。
你可能奇怪为什么std::vector和其它标准容器没有设计得对指针做些特别的动作。
首先,当然,有一个简单的一致性因素:理解有一致语义的库比理解有许多特例的库容易
如果存在特例,很难划出分界线。你将iterator或用户自定义的handle类型等同于指针吗?
如果在通用规则上对vector<my_type *>有一个例外,应该对vector<const my_type *>
再有一个例外的例外吗?容器如何知道什么时候用delete p,什么时候用delete [] p?
第二,并且更重要的是:如果std::vector<my_type *>确实自动地拥有所指向的对象
std::vector的用处就大为减少了。毕竟,如果你期望一个vector拥有一系列my_type的对
象的话,你已经有vector<my_type>了。vector<my_type *>是供你需要另外一些不同的东
西时用的,在值语义和强所有权不合适时。当你拥有的对象被多个容器引用时,或对象能
在同一容器出现多次时,或指针开始时并不指向有效对象时,你可以用容纳指针的容器。
(它们可能是NULL指针,指向原生内存的指针,或指向子对象的指针。)
想像一个特别的例子:
你正在维护一个任务链表,某些任务当前是活动的,某些被挂起。你用一个
std::list<task>存放所有任务,用一个std::vector<task *>存放活动任务组成的任务子集。
你的程序有一个字符串表:std::vector<const char *>,每个元素p指向一个NULL结束的字
符数组。依赖于你如何设计你的字符串表,你可能使用字符串文字,或指向一个巨大的字符
数组内部--无论哪种方法,你都不能用一个循环遍历vector,并对每个元素调用delete p。
你正在做I/O multiplexing,并且将一个std::vector<std::istream *>传给一个函数。
input stream是在别处打开的,将于别处关闭,并且,也许其中之一就是&std::cin。
如果容纳指针的容器多手多脚地delete了所指向的对象,上面的用法没一个能成为可能。
拥有所指向的对象
如果你创建了一个容纳指针的容器,原因通常应该是所指向的对象由别处创建和销毁的。
有没有情况是有理由获得一个容器,它拥有指针本身,还拥有所指向的对象?有的。我知道
的唯一一个好的理由,但也是很重要的一个理由:多态。
C++中的多态是和指针/引用语义绑定在一起的。假如,举例来说,那个task
不只是一个类,而且它是一个继承体系的基类。如果p是一个task *,那么p可能指向一
个task对象或任何一个从task派生的类的对象。当你通过p调用task的一个虚函数,将
会在运行期根据p所指向的实际类型调用相应的函数。
不幸地是,将task作为多态的基类意味着你不能使用vector<task>。容器中的对象
是存储的值;vector<task>中的元素必须是一个task对象,而不能是派生类对象。(事实
上,如果你遵从关于继承体系的基类必须是抽象基类的忠告的话,那么编译器将不允许
你创建task对象和vector<task>对象。)
面向对象的设计通常意味着在对象被创建到对象被销毁之间,你通过指针或引用来
访问对象。如果你想拥有一组对象,除了容纳指针的容器外,你几乎没有选择[注2]。
管理这样的容器的最好的方法是什么?
如果你正使用容纳指针的容器来拥有一组对象,关键是确保所有的对象都被销毁。
最明显的解决方法,可能也是最常见的,是在销毁容器前,遍历它,并为每个元素调用
delete语句。如果手写这个循环太麻烦,很容易作一个包装:
template <class T>
class my_vector : private std::vector<T*>
{
typedef std::vector<T> Base;
public:
using Base::iterator;
using Base::begin;
using Base::end;
...
public:
~my_vector() {
for (iterator i = begin(); i != end(); ++i)
delete *i;
}
};
这个技巧能工作,但是它比看起来有更多的限制和要求。
问题是,只改析构函数是不够的。如果你有一个列出所有的正要被销毁的对象的
容器,那么你最好确保只要指针离开了容器那个对象就要被销毁,并且一个指针绝不
在容器中出现两次。当你用erase()或clear()移除指针时,必须要小心,但是你也需
要小心容器的赋值和通过iterator的赋值:象v1 = v2,和v[n] = p这样的操作是危险
的。标准泛型算法,有很多会执行通过iterator的赋值的,这是另外一个危险。你显然
不能使用std::copy()和std::replace()这样的泛型算法;稍微不太明显地,你也不能
使用std::remove()、std::remove_if(),和std::unique()[注3]。
象my_vector这样的包装类能够解决其中一些问题,但不是全部。很难看出如何阻止用户
以危险的方式使用赋值,除非你禁止所有的赋值--而那时,你所得到的就不怎么象容器了。
问题是每个元素都必须被单独追踪,所以,也许解决方法是包装指针而不是包装整个容器。
标准运行库定义了一个对指针包装的类std::auto_ptr<T>。一个auot_ptr对象保存着一个
T *类型的指针p,其构造函数delete由p所指的对象。看起来这正是我们所要找的:
一个包装类,其析构函数delete一个指针。自然会想到用vector<auto_ptr<T> >
取代vector<T *>。
这是很自然的主意,但它是错误的。原因呢,再一次,是因为值语义。容器类假设它们能
拷贝自己的元素。举例来说,如果你有一个vector<T>,那么T类型的对象必须表现得和
一个平常的数值一样。如果t1是一个T类型的值,你最好能够写:
T t2(t1)
并且得到一个t1的拷贝t2。
形式上,按C++标准中的说法,T要是Assignable的和CopyConstructible的。指针满足
这些要求--你能得到指针的一个拷贝--但auto_ptr不满足。auto_ptr的卖点是它
维护强所用权,所以不允许拷贝。有一个形式上是拷贝构造函数的东西,但auto_ptr
的“拷贝构造函数”实际上并不进行拷贝。如果t1是一个 std::auto_ptr<T>,并且你写:
std::auto_ptr<T> t2(t1)
然后t2将不是t1的一个拷贝。不是进行拷贝,而是发生了所有权转移--t2得到了t1曾
经有着的值,而t1被改成一个NULL指针。auto_ptr 的物件是脆弱的:你只不过看了它
一下就能改变它的值。
在某些实作上,当你试图创建一个vector<auto_ptr<T> >的时候,会得到编译期错
误。这还是算你幸运;如果不幸运的话,事情看起来很好,直到运行期得到不可预知的
行为。总之,标准容器类不能与拷贝构造函数不执行拷贝的类型合作。这也不是
auto_ptr的设计目的,并且,标准[注4]甚至指出“用auto_ptr实例化标准运行库中的
容器会得到未定义的行为。”当你需要异常安全机制时,你应该使用auto_ptr以在退出
代码空间时delete指针;auto_ptr是因模拟了自动变量而得名的。你不应该试图在容器
类中使用auto_ptr来管理指针;它不可行。
取代auto_ptr,你应该使用一个不同的“智能指针”,引用计数的指针类。带引用
计数的指针跟踪多少个指针指向相同的对象。当你构造了一个引用计数指针的拷贝时,
计数加1;当你销毁一个引用计数指针时,计数减1。当计数变成0时,指针所指的对象
被自动销毁。
写一个引用计数的指针类不是特别困难,但也不是几乎什么都不用做;达到线程安
全需要特别的技巧。幸运地是,使用引用计数并不意味着你需要写一个自己的引用计数
指针类;几种这样的类已经存在并可免费使用。比如,你能使用Boost的shared_ptr类
我期望 shared_ptr或其它类似的东西将成为C++标准的下个修订版本的组成部分。
当然,引用计数只是一种特别的垃圾回收。像所有形式的垃圾回收,它自动销毁你
不再需要的对象。引用计数指针的主要优势是它们易于加入现有系统:share_ptr这样
的机制只是一个小的单独的类,你能只在一个较大系统中某个部分中使用它。另一方面
引用计数是垃圾回收的一种最低效的形式(每个指针的赋值和拷贝都需要一些相关的复
杂处理),最没有柔性的形式(在两个数据结构拥有互指指针时,你必须小心)。其它
形式的垃圾回收在C++程序中工作得同样好。特别地,
Boehm conservative garbage collector[注6]是免费的、可移植的,并被很好测试过。
如果你使用一个保守的垃圾回收器,你只需要将它链接入程序就可以了。你不需要
使用任何特别的指针包装类;只需要分配内存而不用关心delete它。特别地,如果你创
建一个vector<T *>,你知道所指向的对象只要vector还存在就不会被delete掉(垃圾回
收器绝不会破坏还有指针指向它们的对象),并且你也知道它们将在vector被销毁后的
某个时候delete掉(除非,程序的其它部份仍然引用它们)。
垃圾回收的优势--无论是引用计数还是保守的垃圾回收器,或其它方法--是它
让你将对象的生存期处理得完全不确定:你不须要掌握在某个特定时间程序的哪个部份
引用了这个对象。另一方面,垃圾回收的缺点也正是这一点!有时你确实知道对象的生
存期,或至少知道在程序的某个特定状态结束后对象不应该继续存在。举例来说,你可
能创建了一个复杂的分析树;也许它填满了多态对象,也许它是太复杂而无法单独掌握
每个节点,但是你能确定在分析完后就将不再需要它们中的任何一个。
从手工管理的vetor<T *>到vector<shared_pointer<T> >到保守的垃圾回收器,我们逐
步放弃了vector拥有一组对象的观点;垃圾回收的前提是对象的“所有权”是无关紧要
的。在某种意义上,它通过扔掉“所有权”问题而解决了这个难题。
如果你的程序确实有明确定义的状态,那么你可能有理由期望在某个状态结束时销
毁一组对象。代替垃圾回收,另外一个可选方法是通过一个arena(WQ注:字典上为“竞
技场、舞台”之意,译不好,不译)来分配对象:维护一个对象列表,以便能一次就销
毁所有对象。
你可能想知道这个技术和前面提到的技术(遍历容器并为每个元素调用destory())有多
大差异。有实质性差异吗?如果没有,让我花了这么多时间的那些危险和限制,又怎么说?
差异很小,但很重要:arena存储了一个对象集,目的是为了在以后delete它们,并
且没有其它目的。它可以以标准容器的形式实现,但是它不暴露容器接口。你在
vector<T *>上遇到问题是因为你能移除元素,拷贝覆盖元素,和运用泛型算法。
Arena是一个强所有权协议。Arena容器为每个它所管理的对象容纳了一个并且只一个指针
并且它拥有所有这些对象;它不允许你移除、复制、覆盖或用选择子遍历它容纳的指针。
要使用arena中的对象,你需要在别处存储指向它们的指针--在容器中,在树中,或在
任何合适的数据结构中。使用权和所有权完全分离。
arena是个通用的主意。arena可能简单地是个容器,只要记住别使用不安全的成员函
数,或者它可以是个包装类以试图运行得更安全。很多这样的包装类已经被写出来了
Listing 1是一个arena类的简化例子,使用了一个实现上的技巧以使得不必为每个指针类
型使用一个不同的arena类。举例来说,你可能写:
arena a;
...
std::vector<int*> v;
v.push_back(a.create<int>(3));
...
a.destroy_all();
我们几乎回到了起点:使用一个容纳指针的vector,所指向的对象由别处拥有和管理。
总结
指针在C++程序中很常见,标准容器同样如此;不用惊奇,它们的组合物,容纳指针容器同
样很常见。
新手最大的困难是容纳指针的容器时的所有权问题:应该什么时候delete所指向的对象?
处理容纳指针的容器的绝大部分技术都可以归结到一个原则:如果你有一个容纳指针的容器,
所指向的对象应该由其它地方所拥有。
如果正在处理非多态对象集(类型为my_type),你应该将对象的值存入容器,比如
list<my_type>或deque<my_type>。如果需要,你也以使用第二个容器以存储指向那些对象
的指针。
不要试图将auto_ptr放入标准容器。
如果有一组多态对象,你需要用容纳指针的容器来管理它们。(但是,那些指针可以被包
装在某种handle类或智能指针类中。)当对象生存期不能预知时,或不重要时,最容易的
方法是使用垃圾回收。垃圾回收的两个最简单的选择是引用计数指针类和保守的垃圾回收
器。对你来说哪个是最佳选择取决于工具的可用性。
如果你有一组多态对象,并需要控制它们的生存期,最简单的方法是使用arena。一个简单
的arena类例子展示于Listing 1。
Listing 1: A simple arena class
#include <vector>
class arena {
private:
struct holder {
virtual ~holder() { }
};
template <class T>
struct obj_holder : public holder {
T obj;
template <class Arg1>
obj_holder(Arg1 arg1)
: obj(arg1) { }
};
std::vector<holder*> owned;
private:
arena(const arena&);
void operator=(const arena&);
public:
arena() { }
~arena() {
destroy_all();
}
template <class T, class Arg1>
T* create(Arg1 arg1) {
obj_holder<T>* p = new obj_holder<T>(arg1);
owned.push_back(p);
return &p->obj;
}
void destroy_all() {
std::vector<holder*>::size_type i = 0;
while (i < owned.size()) {
delete owned[i];
++i;
}
owned.clear();
}
};
你能创建一个std::vector<int>来容纳int类型的对象,
创建一个std::vector<std::string>来容纳string对象,
创建一个std::vector<my_type>来容纳用户自定义类型的对象。
创建std::vector<int *>、std::vector<std::string *>
或std::vector<my_type *>也是完全合理的。
容纳指针的容器很重要也很常见。
不幸地是,虽然是常见技术,容纳指针的容器对新手来说也是造成混乱的
最常见的根源之一。几乎没有哪个星期在C++新闻组中不出现这样的贴子的:
为什么这样的代码导致内存泄漏:
{
std::vector<my_type*> v;
for (int i = 0; i < N; ++i)
v.insert(new my_type(i));
...
} // v is destroyed here
这个内存泄漏是编译器的bug吗?std::vector的析构函数不是会销毁v的元素的吗?
如果你仔细想过std::vector<T>大体上是如何工作的,并且你了解其中对指
针并没有特别的规则的话--也就是说,对vector来说,my_type *只不过是另外
一个T--就不难明白为什么vector<my_type *>有这样的行为以及为什么这段代码
有内存泄漏了。然而,vector<my_type *>的行为可能会令对旧的容器库更熟悉的人感到
惊讶的。
这篇文章解释了容纳指针的容器的行为是怎么样的,什么时候容纳指针的容器会
有用,和在需要执行比标准容器在内存管理上的默认行更多的任务时,该做些什么。
容器和所有权
标准容器使用值语义。举例来说,当你向一个vector附加一个变量x时:
v.push_back(x)
你实际正在做的是附加x的一个拷贝。这个语句存储了x的值(的一个拷贝),而不是x的
地址。在你将x加入一个vector后,你能对x做如何想做的事--比如赋给它一个新值或
让它离开生存域而销毁--而不影响vector中的拷贝。一个容器中的元素不能是另外一
个容器的元素(两个容器的元素必须是不同的对象,即使这些元素碰巧相等),并且将
一个元素从容器中移除将会销毁这个元素(虽然具有相同的值的另外一个对象可能存在
于别处)。最后,容器“拥有”它的元素:当一个容器销毁时,其中的所以元素都随它
一起销毁了。
这些特性与平常的内建数组很相似,并且可能是太明显了而不值一提。我列出它们
以清楚显示容器和数组有多么相似。新手使用标准容器时发生的最常见的概念是认为容
器“在幕后”做了比实际上更多的事。
值语义不总是你所需要的:有时你需要在容器中存储对象的地址而不是拷贝对象的
值。你能以和数组相同的方式,用容器实现引用语义:藉由显式要求。你能将任何类型
的对象放入容器[注1],而指针自己就是非常好的对象。指针占用内存;能被赋值;自己
有地址;有能被拷贝的值。如果你需要存储对象的地址,就使用容纳指针的容器。不再
是写:
std::vector<my_type> v;
my_type x;
...
v.push_back(x);
你能写:
std::vector<my_type*> v;
my_type x;
...
v.push_back(&x);
感觉上,没有任何变化。你仍然正在创建一个std::vector<T>;只不过现在T碰巧是
一个指针类型,my_type *。vector仍然“拥有”它的元素,但你必须明白这些元素是什
么:它们是指针,而不是指针所指向的东西。
拥有指针和拥有指针所指的东西之间的区别就象是vector与数组或局部变量。假如你写:
{
my_type* p = new my_type;
}
当离开代码域时,指针p将会消失,但它所指向的对象,*p,不会消失。如果你想
销毁这对象并释放其内存,你需要自己来完成,显式地写delete p或用其它等价的方法。
同样,在std::vector<my_type *>中没有任何特殊代码以遍历整个vector并对每个元素
调用delete。元素在vector消失时消失。如果你想在那些元素销毁前发生另外一些事,
你必须自己做。
你可能奇怪为什么std::vector和其它标准容器没有设计得对指针做些特别的动作。
首先,当然,有一个简单的一致性因素:理解有一致语义的库比理解有许多特例的库容易
如果存在特例,很难划出分界线。你将iterator或用户自定义的handle类型等同于指针吗?
如果在通用规则上对vector<my_type *>有一个例外,应该对vector<const my_type *>
再有一个例外的例外吗?容器如何知道什么时候用delete p,什么时候用delete [] p?
第二,并且更重要的是:如果std::vector<my_type *>确实自动地拥有所指向的对象
std::vector的用处就大为减少了。毕竟,如果你期望一个vector拥有一系列my_type的对
象的话,你已经有vector<my_type>了。vector<my_type *>是供你需要另外一些不同的东
西时用的,在值语义和强所有权不合适时。当你拥有的对象被多个容器引用时,或对象能
在同一容器出现多次时,或指针开始时并不指向有效对象时,你可以用容纳指针的容器。
(它们可能是NULL指针,指向原生内存的指针,或指向子对象的指针。)
想像一个特别的例子:
你正在维护一个任务链表,某些任务当前是活动的,某些被挂起。你用一个
std::list<task>存放所有任务,用一个std::vector<task *>存放活动任务组成的任务子集。
你的程序有一个字符串表:std::vector<const char *>,每个元素p指向一个NULL结束的字
符数组。依赖于你如何设计你的字符串表,你可能使用字符串文字,或指向一个巨大的字符
数组内部--无论哪种方法,你都不能用一个循环遍历vector,并对每个元素调用delete p。
你正在做I/O multiplexing,并且将一个std::vector<std::istream *>传给一个函数。
input stream是在别处打开的,将于别处关闭,并且,也许其中之一就是&std::cin。
如果容纳指针的容器多手多脚地delete了所指向的对象,上面的用法没一个能成为可能。
拥有所指向的对象
如果你创建了一个容纳指针的容器,原因通常应该是所指向的对象由别处创建和销毁的。
有没有情况是有理由获得一个容器,它拥有指针本身,还拥有所指向的对象?有的。我知道
的唯一一个好的理由,但也是很重要的一个理由:多态。
C++中的多态是和指针/引用语义绑定在一起的。假如,举例来说,那个task
不只是一个类,而且它是一个继承体系的基类。如果p是一个task *,那么p可能指向一
个task对象或任何一个从task派生的类的对象。当你通过p调用task的一个虚函数,将
会在运行期根据p所指向的实际类型调用相应的函数。
不幸地是,将task作为多态的基类意味着你不能使用vector<task>。容器中的对象
是存储的值;vector<task>中的元素必须是一个task对象,而不能是派生类对象。(事实
上,如果你遵从关于继承体系的基类必须是抽象基类的忠告的话,那么编译器将不允许
你创建task对象和vector<task>对象。)
面向对象的设计通常意味着在对象被创建到对象被销毁之间,你通过指针或引用来
访问对象。如果你想拥有一组对象,除了容纳指针的容器外,你几乎没有选择[注2]。
管理这样的容器的最好的方法是什么?
如果你正使用容纳指针的容器来拥有一组对象,关键是确保所有的对象都被销毁。
最明显的解决方法,可能也是最常见的,是在销毁容器前,遍历它,并为每个元素调用
delete语句。如果手写这个循环太麻烦,很容易作一个包装:
template <class T>
class my_vector : private std::vector<T*>
{
typedef std::vector<T> Base;
public:
using Base::iterator;
using Base::begin;
using Base::end;
...
public:
~my_vector() {
for (iterator i = begin(); i != end(); ++i)
delete *i;
}
};
这个技巧能工作,但是它比看起来有更多的限制和要求。
问题是,只改析构函数是不够的。如果你有一个列出所有的正要被销毁的对象的
容器,那么你最好确保只要指针离开了容器那个对象就要被销毁,并且一个指针绝不
在容器中出现两次。当你用erase()或clear()移除指针时,必须要小心,但是你也需
要小心容器的赋值和通过iterator的赋值:象v1 = v2,和v[n] = p这样的操作是危险
的。标准泛型算法,有很多会执行通过iterator的赋值的,这是另外一个危险。你显然
不能使用std::copy()和std::replace()这样的泛型算法;稍微不太明显地,你也不能
使用std::remove()、std::remove_if(),和std::unique()[注3]。
象my_vector这样的包装类能够解决其中一些问题,但不是全部。很难看出如何阻止用户
以危险的方式使用赋值,除非你禁止所有的赋值--而那时,你所得到的就不怎么象容器了。
问题是每个元素都必须被单独追踪,所以,也许解决方法是包装指针而不是包装整个容器。
标准运行库定义了一个对指针包装的类std::auto_ptr<T>。一个auot_ptr对象保存着一个
T *类型的指针p,其构造函数delete由p所指的对象。看起来这正是我们所要找的:
一个包装类,其析构函数delete一个指针。自然会想到用vector<auto_ptr<T> >
取代vector<T *>。
这是很自然的主意,但它是错误的。原因呢,再一次,是因为值语义。容器类假设它们能
拷贝自己的元素。举例来说,如果你有一个vector<T>,那么T类型的对象必须表现得和
一个平常的数值一样。如果t1是一个T类型的值,你最好能够写:
T t2(t1)
并且得到一个t1的拷贝t2。
形式上,按C++标准中的说法,T要是Assignable的和CopyConstructible的。指针满足
这些要求--你能得到指针的一个拷贝--但auto_ptr不满足。auto_ptr的卖点是它
维护强所用权,所以不允许拷贝。有一个形式上是拷贝构造函数的东西,但auto_ptr
的“拷贝构造函数”实际上并不进行拷贝。如果t1是一个 std::auto_ptr<T>,并且你写:
std::auto_ptr<T> t2(t1)
然后t2将不是t1的一个拷贝。不是进行拷贝,而是发生了所有权转移--t2得到了t1曾
经有着的值,而t1被改成一个NULL指针。auto_ptr 的物件是脆弱的:你只不过看了它
一下就能改变它的值。
在某些实作上,当你试图创建一个vector<auto_ptr<T> >的时候,会得到编译期错
误。这还是算你幸运;如果不幸运的话,事情看起来很好,直到运行期得到不可预知的
行为。总之,标准容器类不能与拷贝构造函数不执行拷贝的类型合作。这也不是
auto_ptr的设计目的,并且,标准[注4]甚至指出“用auto_ptr实例化标准运行库中的
容器会得到未定义的行为。”当你需要异常安全机制时,你应该使用auto_ptr以在退出
代码空间时delete指针;auto_ptr是因模拟了自动变量而得名的。你不应该试图在容器
类中使用auto_ptr来管理指针;它不可行。
取代auto_ptr,你应该使用一个不同的“智能指针”,引用计数的指针类。带引用
计数的指针跟踪多少个指针指向相同的对象。当你构造了一个引用计数指针的拷贝时,
计数加1;当你销毁一个引用计数指针时,计数减1。当计数变成0时,指针所指的对象
被自动销毁。
写一个引用计数的指针类不是特别困难,但也不是几乎什么都不用做;达到线程安
全需要特别的技巧。幸运地是,使用引用计数并不意味着你需要写一个自己的引用计数
指针类;几种这样的类已经存在并可免费使用。比如,你能使用Boost的shared_ptr类
我期望 shared_ptr或其它类似的东西将成为C++标准的下个修订版本的组成部分。
当然,引用计数只是一种特别的垃圾回收。像所有形式的垃圾回收,它自动销毁你
不再需要的对象。引用计数指针的主要优势是它们易于加入现有系统:share_ptr这样
的机制只是一个小的单独的类,你能只在一个较大系统中某个部分中使用它。另一方面
引用计数是垃圾回收的一种最低效的形式(每个指针的赋值和拷贝都需要一些相关的复
杂处理),最没有柔性的形式(在两个数据结构拥有互指指针时,你必须小心)。其它
形式的垃圾回收在C++程序中工作得同样好。特别地,
Boehm conservative garbage collector[注6]是免费的、可移植的,并被很好测试过。
如果你使用一个保守的垃圾回收器,你只需要将它链接入程序就可以了。你不需要
使用任何特别的指针包装类;只需要分配内存而不用关心delete它。特别地,如果你创
建一个vector<T *>,你知道所指向的对象只要vector还存在就不会被delete掉(垃圾回
收器绝不会破坏还有指针指向它们的对象),并且你也知道它们将在vector被销毁后的
某个时候delete掉(除非,程序的其它部份仍然引用它们)。
垃圾回收的优势--无论是引用计数还是保守的垃圾回收器,或其它方法--是它
让你将对象的生存期处理得完全不确定:你不须要掌握在某个特定时间程序的哪个部份
引用了这个对象。另一方面,垃圾回收的缺点也正是这一点!有时你确实知道对象的生
存期,或至少知道在程序的某个特定状态结束后对象不应该继续存在。举例来说,你可
能创建了一个复杂的分析树;也许它填满了多态对象,也许它是太复杂而无法单独掌握
每个节点,但是你能确定在分析完后就将不再需要它们中的任何一个。
从手工管理的vetor<T *>到vector<shared_pointer<T> >到保守的垃圾回收器,我们逐
步放弃了vector拥有一组对象的观点;垃圾回收的前提是对象的“所有权”是无关紧要
的。在某种意义上,它通过扔掉“所有权”问题而解决了这个难题。
如果你的程序确实有明确定义的状态,那么你可能有理由期望在某个状态结束时销
毁一组对象。代替垃圾回收,另外一个可选方法是通过一个arena(WQ注:字典上为“竞
技场、舞台”之意,译不好,不译)来分配对象:维护一个对象列表,以便能一次就销
毁所有对象。
你可能想知道这个技术和前面提到的技术(遍历容器并为每个元素调用destory())有多
大差异。有实质性差异吗?如果没有,让我花了这么多时间的那些危险和限制,又怎么说?
差异很小,但很重要:arena存储了一个对象集,目的是为了在以后delete它们,并
且没有其它目的。它可以以标准容器的形式实现,但是它不暴露容器接口。你在
vector<T *>上遇到问题是因为你能移除元素,拷贝覆盖元素,和运用泛型算法。
Arena是一个强所有权协议。Arena容器为每个它所管理的对象容纳了一个并且只一个指针
并且它拥有所有这些对象;它不允许你移除、复制、覆盖或用选择子遍历它容纳的指针。
要使用arena中的对象,你需要在别处存储指向它们的指针--在容器中,在树中,或在
任何合适的数据结构中。使用权和所有权完全分离。
arena是个通用的主意。arena可能简单地是个容器,只要记住别使用不安全的成员函
数,或者它可以是个包装类以试图运行得更安全。很多这样的包装类已经被写出来了
Listing 1是一个arena类的简化例子,使用了一个实现上的技巧以使得不必为每个指针类
型使用一个不同的arena类。举例来说,你可能写:
arena a;
...
std::vector<int*> v;
v.push_back(a.create<int>(3));
...
a.destroy_all();
我们几乎回到了起点:使用一个容纳指针的vector,所指向的对象由别处拥有和管理。
总结
指针在C++程序中很常见,标准容器同样如此;不用惊奇,它们的组合物,容纳指针容器同
样很常见。
新手最大的困难是容纳指针的容器时的所有权问题:应该什么时候delete所指向的对象?
处理容纳指针的容器的绝大部分技术都可以归结到一个原则:如果你有一个容纳指针的容器,
所指向的对象应该由其它地方所拥有。
如果正在处理非多态对象集(类型为my_type),你应该将对象的值存入容器,比如
list<my_type>或deque<my_type>。如果需要,你也以使用第二个容器以存储指向那些对象
的指针。
不要试图将auto_ptr放入标准容器。
如果有一组多态对象,你需要用容纳指针的容器来管理它们。(但是,那些指针可以被包
装在某种handle类或智能指针类中。)当对象生存期不能预知时,或不重要时,最容易的
方法是使用垃圾回收。垃圾回收的两个最简单的选择是引用计数指针类和保守的垃圾回收
器。对你来说哪个是最佳选择取决于工具的可用性。
如果你有一组多态对象,并需要控制它们的生存期,最简单的方法是使用arena。一个简单
的arena类例子展示于Listing 1。
Listing 1: A simple arena class
#include <vector>
class arena {
private:
struct holder {
virtual ~holder() { }
};
template <class T>
struct obj_holder : public holder {
T obj;
template <class Arg1>
obj_holder(Arg1 arg1)
: obj(arg1) { }
};
std::vector<holder*> owned;
private:
arena(const arena&);
void operator=(const arena&);
public:
arena() { }
~arena() {
destroy_all();
}
template <class T, class Arg1>
T* create(Arg1 arg1) {
obj_holder<T>* p = new obj_holder<T>(arg1);
owned.push_back(p);
return &p->obj;
}
void destroy_all() {
std::vector<holder*>::size_type i = 0;
while (i < owned.size()) {
delete owned[i];
++i;
}
owned.clear();
}
};
更多推荐
已为社区贡献2条内容
所有评论(0)