C 为什么不加入垃圾回收机制
时间:2021-08-19 16:29:22
手机看文章
扫描二维码
随时随地手机看文章
[导读]来源:http://www.codeceo.com/article/why-cpp-not-use-gc.html作者:M-先生Java的爱好者们经常批评C中没有提供与Java类似的垃圾回收(GabageCollector)机制(这很正常,正如C的爱好者有时也攻击Java没有这个...
来源:http://www.codeceo.com/article/why-cpp-not-use-gc.html作者:M-先生
在本文中,我并不想揭露Java提供的垃圾回收机制的天生缺陷,而是指出了C 中引入垃圾回收的可行性。请读者注意,这里介绍的方法更多的是基于当前标准和库设计的角度,而不是要求修改语言定义或者扩展编译器。
什么是垃圾回收?
作为支持指针的编程语言,C 将动态管理存储器资源的便利性交给了程序员。在使用指针形式的对象时(请注意,由于引用在初始化后不能更改引用目标的语言机制的限制,多态性应用大多数情况下依赖于指针进行),程序员必须自己完成存储器的分配、使用和释放,语言本身在此过程中不能提供任何帮助,也许除了按照你的要求正确的和操作系统亲密合作,完成实际的存储器管理。标准文本中,多次提到了“未定义(undefined)”,而这大多数情况下和指针相关。某些语言提供了垃圾回收机制,也就是说程序员仅负责分配存储器和使用,而由语言本身负责释放不再使用的存储器,这样程序员就从讨厌的存储器管理的工作中脱身了。然而C 并没有提供类似的机制,C 的设计者Bjarne Stroustrup在我所知的唯一一本介绍语言设计的思想和哲学的著作《The Design and Evolution of C 》(中译本:C 语言的设计和演化)中花了一个小节讨论这个特性。简而言之,Bjarne本人认为,“我有意这样设计C ,使它不依赖于自动垃圾回收(通常就直接说垃圾回收)。这是基于自己对垃圾回收系统的经验,我很害怕那种严重的空间和时间开销,也害怕由于实现和移植垃圾回收系统而带来的复杂性。还有,垃圾回收将使C 不适合做许多底层的工作,而这却正是它的一个设计目标。但我喜欢垃圾回收的思想,它是一种机制,能够简化设计、排除掉许多产生错误的根源。需要垃圾回收的基本理由是很容易理解的:用户的使用方便以及比用户提供的存储管理模式更可靠。而反对垃圾回收的理由也有很多,但都不是最根本的,而是关于实现和效率方面的。已经有充分多的论据可以反驳:每个应用在有了垃圾回收之后会做的更好些。类似的,也有充分的论据可以反对:没有应用可能因为有了垃圾回收而做得更好。并不是每个程序都需要永远无休止的运行下去;并不是所有的代码都是基础性的库代码;对于许多应用而言,出现一点存储流失是可以接受的;许多应用可以管理自己的存储,而不需要垃圾回收或者其他与之相关的技术,如引用计数等。我的结论是,从原则上和可行性上说,垃圾回收都是需要的。但是对今天的用户以及普遍的使用和硬件而言,我们还无法承受将C 的语义和它的基本库定义在垃圾回收系统之上的负担。”以我之见,统一的自动垃圾回收系统无法适用于各种不同的应用环境,而又不至于导致实现上的负担。稍后我将设计一个针对特定类型的可选的垃圾回收器,可以很明显地看到,或多或少总是存在一些效率上的开销,如果强迫C 用户必须接受这一点,也许是不可取的。关于为什么C 没有垃圾回收以及可能的在C 中为此做出的努力,上面提到的著作是我所看过的对这个问题叙述的最全面的,尽管只有短短的一个小节的内容,但是已经涵盖了很多内容,这正是Bjarne著作的一贯特点,言简意赅而内韵十足。下面一步一步地向大家介绍我自己土制佳酿的垃圾回收系统,可以按照需要自由选用,而不影响其他代码。构造函数和析构函数
C 中提供的构造函数和析构函数很好的解决了自动释放资源的需求。Bjarne有一句名言,“资源需求就是初始化(Resource Inquirment Is Initialization)”。因此,我们可以将需要分配的资源在构造函数中申请完成,而在析构函数中释放已经分配的资源,只要对象的生存期结束,对象请求分配的资源即被自动释放。那么就仅剩下一个问题了,如果对象本身是在自由存储区(Free Store,也就是所谓的“堆”)中动态创建的,并由指针管理(相信你已经知道为什么了),则还是必须通过编码显式的调用析构函数,当然是借助指针的delete表达式。智能指针
幸运的是,出于某些原因,C 的标准库中至少引入了一种类型的智能指针,虽然在使用上有局限性,但是它刚好可以解决我们的这个难题,这就是标准库中唯一的一个智能指针::std::auto_ptr。它将指针包装成了类,并且重载了反引用(dereference)运算符operator *和成员选择运算符operator ->,以模仿指针的行为。关于auto_ptr的具体细节,参阅《The C Standard Library》(中译本:C 标准库)。例如以下代码,#include#include
#include
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对象(先前在构造函数中通过new表达式分配而来的),并进而执行string的析构函数,释放为实际的字符串动态申请的内存。在string中也可能管理其他类型的资源,如用于多线程环境下的同步资源。下图说明了上面的过程。现在我们拥有了最简单的垃圾回收机制(我隐瞒了一点,在string中,你仍然需要自己编码控制对象的动态创建和销毁,但是这种情况下的准则极其简单,就是在构造函数中分配资源,在析构函数中释放资源,就好像飞机驾驶员必须在起飞后和降落前检查起落架一样。),即使在foo函数中发生了异常,str的生存期也会结束,C 保证自然退出时发生的一切在异常发生时一样会有效。auto_ptr只是智能指针的一种,它的复制行为提供了所有权转移的语义,即智能指针在复制时将对内部维护的实际指针的所有权进行了转移,例如auto_ptr <string> str1( new string(
cout << str1->c_str();
auto_ptr <string> str2(str1); // str1内部指针不再指向原来的对象
cout << str2->c_str();
cout << str1->c_str(); // 未定义,str1内部指针不再有效某些时候,需要共享同一个对象,此时auto_ptr就不敷使用,由于某些历史的原因,C 的标准库中并没有提供其他形式的智能指针,走投无路了吗?
另一种智能指针
但是我们可以自己制作另一种形式的智能指针,也就是具有值复制语义的,并且共享值的智能指针。需要同一个类的多个对象同时拥有一个对象的拷贝时,我们可以使用引用计数(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 》),templateclass 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