C++中的废物收集
副标题#e#
Java的喜好者们常常品评C++中没有提供与Java雷同的废物收集(Gabage Collector)机制(这很正常,正如C++的喜好者有时也进攻Java没有这个没有谁人,可能这个不可谁人不足好),导致C++中对动态存储的仕宦称为措施员的恶梦,不是吗?你常常听到的是内存遗失(memory leak)和犯科指针存取,这必然令你很头疼,并且你又不能丢弃指针带来的机动性。
在本文中,我并不想揭破Java提供的废物收集机制的天生缺陷,而是指出了C++中引入废物收集的可行性。请读者留意,这里先容的要领更多的是基于当前尺度和库设计的角度,而不是要求修改语言界说可能扩展编译器。
1 什么是废物收集?
作为支持指针的编程语言,C++将动态打点存储器资源的便利性交给了措施员。在利用指针形式的工具时(请留意,由于引用在初始化后不能变动引用方针的语言机制的限制,多态性应用大大都环境下依赖于指针举办),措施员必需本身完成存储器的分派、利用和释放,语言自己在此进程中不能提供任何辅佐,也许除了凭据你的要求正确的和操纵系统亲密相助,完成实际的存储器打点。尺度文本中,多次提到了“未界说(undefined)”,而这大大都环境下和指针相关。
某些语言提供了废物收集机制,也就是说措施员仅认真分派存储器和利用,而由语言自己认真释放不再利用的存储器,这样措施员就从讨厌的存储器打点的事情中脱身了。然而C++并没有提供雷同的机制,C++的设计者Bjarne Stroustrup在我所知的独一一本先容语言设计的思想和哲学的著作《The Design and Evolution of C++》(中译本:C++语言的设计和演化)中花了一个小节接头这个特性。简而言之,Bjarne本人认为,
“我有意这样设计C++,使它不依赖于自动废物收集(凡是就直接说废物收集)。这是基于本身对废物收集系统的履历,我很畏惧那种严重的空间和时间开销,也畏惧由于实现和移植废物收集系统而带来的巨大性。尚有,废物收集将使C++不适合做很多底层的事情,而这却正是它的一个设计方针。但我喜欢废物收集的思想,它是一种机制,可以或许简化设计、解除去很多发生错误的来源。
需要废物收集的根基来由是很容易领略的:用户的利用利便以及比用户提供的存储打点模式更靠得住。而阻挡废物收集的来由也有许多,但都不是最基础的,而是关于实现和效率方面的。
已经有充实多的论据可以辩驳:每个应用在有了废物收集之后会做的更好些。雷同的,也有充实的论据可以阻挡:没有应用大概因为有了废物收集而做得更好。
并不是每个措施都需要永远无休止的运行下去;并不是所有的代码都是基本性的库代码;对付很多应用而言,呈现一点存储流失是可以接管的;很多应用可以打点本身的存储,而不需要废物收集可能其他与之相关的技能,如引用计数等。
我的结论是,从原则上和可行性上说,废物收集都是需要的。可是对本日的用户以及普遍的利用和硬件而言,我们还无法遭受将C++的语义和它的根基库界说在废物收集系统之上的承担。”
以我之见,统一的自动废物收集系统无法合用于各类差异的应用情况,而又不至于导致实现上的承担。稍后我将设计一个针对特定范例的可选的废物收集器,可以很明明地看到,或多或少老是存在一些效率上的开销,假如强迫C++用户必需接管这一点,也许是不行取的。
关于为什么C++没有废物收集以及大概的在C++中为此做出的尽力,上面提到的著作是我所看过的对这个问题论述的最全面的,尽量只有短短的一个小节的内容,可是已经涵盖了许多内容,这正是Bjarne著作的一贯特点,言简意赅而内韵十足。
下面一步一步地向各人先容我本身土制佳酿的废物收集系统,可以凭据需要自由选用,而不影响其他代码。
2 结构函数和析构函数
C++中提供的结构函数和析构函数很好的办理了自动释放资源的需求。Bjarne有一句名言,“资源需求就是初始化(Resource Inquirment Is Initialization)”。
因此,我们可以将需要分派的资源在结构函数中申请完成,而在析构函数中释放已经分派的资源,只要工具的保留期竣事,工具请求分派的资源即被自动释放。
那么就仅剩下一个问题了,假如工具自己是在自由存储区(Free Store,也就是所谓的“堆”)中动态建设的,并由指针打点(相信你已经知道为什么了),则照旧必需通过编码显式的挪用析构函数,虽然是借助指针的delete表达式。
#p#副标题#e#
3 智能指针
#p#分页标题#e#
幸运的是,出于某些原因,C++的尺度库中至少引入了一种范例的智能指针,固然在利用上有范围性,可是它恰好可以办理我们的这个困难,这就是尺度库中独一的一个智能指针::std::auto_ptr<>。
它将指针包装成了类,而且重载了反引用(dereference)运算符operator *和成员选择运算符operator ->,以仿照指针的行为。关于auto_ptr<>的详细细节,参阅《The C++ Standard Library》(中译本:C++尺度库)。
譬喻以下代码,
#include < cstring >
#include < memory >
#include < iostream >
class string
{
public:
string(const char* cstr) { _data=new char [ strlen(cstr)+1 ]; strcpy(_data, cstr); }
~string() { delete [] _data; }
const char* c_str() const { return _data; }
private:
char* _data;
};
void foo()
{
::std::auto_ptr < string > str ( new string( " hello " ) );
::std::cout << str->c_str() << ::std::endl;
}
由于str是函数的局部工具,因此在函数退出点保留期竣事,此时auto_ptr<string>的析构函数挪用,自动销毁内部指针维护的string工具(先前在结构函数中通过new表达式分派而来的),并进而执行string的析构函数,释放为实际的字符串动态申请的内存。在string中也大概打点其他范例的资源,如用于多线程情况下的同步资源。下图说明白上面的进程。
进入函数foo 退出函数
| A
V |
auto_ptr<string>::auto<string>() auto_ptr<string>::~auto_ptr<string>()
| A
V |
string::string() string::~string()
| A
V |
_data=new char[] delete [] _data
| A
V |
利用资源 -----------------------------------> 释放资源
此刻我们拥有了最简朴的废物收集机制(我隐瞒了一点,在string中,你仍然需要本身编码节制工具的动态建设和销毁,可是这种环境下的准则极其简朴,就是在结构函数中分派资源,在析构函数中释放资源,就仿佛飞机驾驶员必需在起飞后和降落前查抄起落架一样。),纵然在foo函数中产生了异常,str的保留期也会竣事,C++担保自然退出时产生的一切在异常产生时一样会有效。
auto_ptr<>只是智能指针的一种,它的复制行为提供了所有权转移的语义,即智能指针在复制时将对内部维护的实际指针的所有权举办了转移,譬喻
auto_ptr < string > str1( new string( < str1 > ) );
cout << str1->c_str();
auto_ptr < string > str2(str1); // str1内部指针不再指向本来的工具
cout << str2->c_str();
cout << str1->c_str(); // 未界说,str1内部指针不再有效
某些时候,需要共享同一个工具,此时auto_ptr就不够利用,由于某些汗青的原因,C++的尺度库中并没有提供其他形式的智能指针,走投无路了吗?
4 另一种智能指针
可是我们可以本身建造另一种形式的智能指针,也就是具有值复制语义的,而且共享值的智能指针。
需要同一个类的多个工具同时拥有一个工具的拷贝时,我们可以利用引用计数(Reference Counting/Using Counting)来实现,曾经这是一个C++中为了提高效率与COW(copy on write,改写时复制)技能一起被遍及利用的技能,厥后证明在多线程应用中,COW为了担保行为的正确反而导致了效率低落(Herb Shutter的在C++ Report杂志中的Guru专栏以及整理后出书的《More Exceptional C++》中专门接头了这个问题)。
然而对付我们今朝的问题,引用计数自己并不会有太大的问题,因为没有牵涉到复制问题,为了担保多线程情况下的正确,并不需要过多的效率牺牲,可是为了简化问题,这里忽略了对付多线程安详的思量。
首先我们仿造auto_ptr设计了一个类模板(出自Herb Shutter的《More Execptional C++》),
#p#分页标题#e#
template < typename T >
class shared_ptr
{
private:
class implement // 实现类,引用计数
{
public:
implement(T* pp):p(pp),refs(1){}
~implement(){delete p;}
T* p; // 实际指针
size_t refs; // 引用计数
};
implement* _impl;
public:
explicit shared_ptr(T* p)
: _impl(new implement(p)){}
~shared_ptr()
{
decrease(); // 计数递减
}
shared_ptr(const shared_ptr& rhs)
: _impl(rhs._impl)
{
increase(); // 计数递增
}
shared_ptr& operator=(const shared_ptr& rhs)
{
if (_impl != rhs._impl) // 制止自赋值
{
decrease(); // 计数递减,不再共享原工具
_impl=rhs._impl; // 共享新的工具
increase(); // 计数递增,维护正确的引用计数值
}
return *this;
}
T* operator->() const
{
return _impl->p;
}
T& operator*() const
{
return *(_impl->p);
}
private:
void decrease()
{
if (--(_impl->refs)==0)
{ // 不再被共享,销毁工具
delete _impl;
}
}
void increase()
{
++(_impl->refs);
}
};
这个类模板是如此的简朴,所以都不需要对代码举办太多地说明。这里仅仅给出一个简朴的利用实例,足以说明shared_ptr<>作为简朴的废物收集器的替代品。
void foo1(shared_ptr < int >& val)
{
shared_ptr < int > temp(val);
*temp=300;
}
void foo2(shared_ptr < int >& val)
{
val=shared_ptr < int > ( new int(200) );
}
int main()
{
shared_ptr < int > val(new int(100));
cout<<"val="<<*val;
foo1(val);
cout<<"val="<<*val;
foo2(val);
cout<<"val="<<*val;
}
在main()函数中,先挪用foo1(val),函数中利用了一个局部工具temp,它和val共享同一份数据,并修改了实际值,函数返回后,val拥有的值同样也产生了变革,而实际上val自己并没有修悔改。
然后挪用了foo2(val),函数中利用了一个无名的姑且工具建设了一个新值,利用赋值表达式修改了val,同时val和姑且工具拥有同一个值,函数返回时,val仍然拥有这正确的值。
最后,在整个进程中,除了在利用shared_ptr < int >的结构函数时利用了new表达式建设新之外,并没有任何删除指针的行动,可是所有的内存打点均正确无误,这就是得益于shared_ptr<>的精良的设计。
拥有了auto_ptr<>和shared_ptr<>两大利器今后,应该足以应付大大都环境下的废物收集了,假如你需要更巨大语义(主要是指复制时的语义)的智能指针,可以参考boost的源代码,个中设计了多种范例的智能指针。
5 尺度容器
对付需要在措施中拥有沟通范例的多个工具,善用尺度库提供的各类容器类,可以最大限度的杜绝显式的内存打点,然而尺度容器并不合用于储存指针,这样对付多态性的支持仍然面对逆境。
利用智能指针作为容器的元素范例,然而尺度容器和算法大大都需要值复制语义的元素,前面先容的转移所有权的auto_ptr和廉价的共享工具的shared_ptr都不能提供正确的值复制语义,Herb Sutter在《More Execptional C++》中设计了一个具有完全复制语义的智能指针ValuePtr,办理了指针用于尺度容器的问题。
然而,多态性仍然没有办理,我将在另一篇文章专门先容利用容器打点多态工具的问题。
6 语言支持
为什么不在C++语言中增加对废物收集的支持?
按照前面的接头,我们可以瞥见,差异的应用情况,也许需要差异的废物收集器,不管三七二十一利用废物收集,需要将这些差异范例的废物收集器整合在一起,纵然可以乐成(对此我感想猜疑),也会导致效率本钱的增加。
这违反了C++的设计哲学,“不为不须要的成果付出价钱”,强迫用户接管废物收集的价钱并不行取。
相反,按需选择你本身需要的废物收集器,需要把握的法则与显式的打点内存对比,简朴的多,也不容易堕落。
#p#分页标题#e#
最要害的一点, C++并不是“傻瓜型”的编程语言,他青睐喜欢和蔼于思考的编程者,设计一个符合本身需要的废物收集器,正是对喜爱C++的措施员的一种挑战。