当前位置: 首页 > 科技观察

为什么C++不加入垃圾回收机制

时间:2023-03-19 09:49:21 科技观察

Java爱好者经常批评C++没有提供类似于Java的垃圾回收(GabageCollector)机制(这很正常,就像C++爱好者有时攻击Java没有这个或那,或者这行不通,那还不够好),导致C++中动态存储的官僚作风被称为程序员的噩梦,不是吗?你经常听到的是内存泄漏和非法指针访问,这一定让你很头疼,也不能放弃指针带来的灵活性。在本文中,我不想揭露Java提供的垃圾回收机制的内在缺陷,而是指出在C++中引入垃圾回收的可行性。请读者注意,此处介绍的方法更多地基于当前标准和库设计观点,而不是要求更改语言定义或扩展编译器。什么是垃圾收集?C++作为一种支持指针的编程语言,将动态管理内存资源的便利交给了程序员。当以指针形式使用对象时(请注意,由于语言机制的限制,引用在初始化后不能改变引用目标,多态应用在大多数情况下依赖于指针),程序员必须完成内存分配,use和release,语言本身在这个过程中无法提供任何帮助,除了可能根据你的要求正确地与操作系统紧密配合来做实际的内存管理。在标准文本中多次提到“未定义(undefined)”,其中大部分都与指针有关。有些语言提供了垃圾回收机制,也就是说程序员只负责分配内存并使用,语言自己负责释放不再使用的内存,让程序员从烦人的事情中解脱出来内存管理的工作。但是,C++没有提供类似的机制。C++的设计者BjarneStroustrup花时间在我所知道的唯一一本介绍语言设计的思想和哲学的书中《The Design and Evolution of C++》(中文译名:DesignandEvolutionofC++Language)中有一个小节讨论了这个特性。简而言之,Bjarne自己认为,“我有意设计C++,使其不依赖于自动垃圾收集(通常只是说垃圾收集)。这是基于我自己对垃圾收集系统的经验。空间和时间开销,以及实现和移植垃圾收集系统的复杂性。此外,垃圾收集会使C++不适合许多低级作业,这是其设计目标之一。但我喜欢垃圾收集作为一种简化设计的机制的想法并消除了许多错误来源。垃圾收集的基本原因很容易理解:用户友好,比用户提供的存储管理模型更高效、可靠。反对垃圾回收的理由有很多,但都不是最根本的,而是关于实现和效率的。有很多反对意见:每个应用程序在垃圾收集方面做得更好。同样,也有很好的理由反对它:没有应用程序可能在垃圾收集方面做得更好。并非每个程序都需要永远无休止地运行;并非所有代码都是基础库代码;对于许多应用程序,少量的存储变动是可以接受的;许多应用程序可以管理自己的存储,不需要垃圾收集或其他相关技术,例如引用计数。我的结论是,无论是在原则上还是在实践中,都需要进行垃圾回收。但对于今天的用户和普通用法和硬件来说,我们无法承受在垃圾收集系统之上定义C++及其基础库的语义。”在我看来,一个统一的自动垃圾收集系统不可能适用于各种应用环境而不对实现造成负担。后面我会针对特定类型设计一个可选的垃圾收集器,这显然总有一些效率或多或少的开销,强制C++用户必须接受这一点可能是不可取的关于为什么C++没有垃圾收集以及C++中可能为它做些什么上面提到的工作是最全面的说明这期我看过的,虽然只有一小节,但是涵盖了很多内容,这是Bjarne作品一贯的特点,言简意赅,内韵十足。下面我一步步介绍自己自制wine的垃圾回收系统,可以根据需要自由选择,不影响其他代码C++提供的构造函数和析构函数很好的解决了对autom的需求资源的自动释放。Bjarne有一句名言,“资源查询就是初始化”。因此,我们可以在构造函数中申请需要分配的资源,而在析构函数中只要对象的生命期结束,对象申请的资源就会自动释放。那么就只剩下一个问题了,如果对象本身在空闲存储区(FreeStore,也就是所谓的“Heap”),并且由指针管理(相信你已经知道为什么了),你还是要显式调用析构函数通过编码,当然是借助于指针的删除表达式。幸运的是,智能指针,出于某种原因,C++标准库中至少引入了一种智能指针。虽然在使用上有局限性,但正好可以解决我们的问题。这是标准库中唯一的智能指针::std::auto_ptr。它将指针包装到类中,并重载取消引用运算符operator*和成员选择运算符operator->以模仿指针的行为。关于auto_ptr的详细介绍,请参考《The C++ Standard Library》(中文译名:C++标准库)。例如下面的代码,#include#include#includeclassstring{public:string(constchar*cstr){_data=newchar[strlen(cstr)+1];strcpy(_data,cstr);}~string(){删除[]_data;}constchar*c_str()const{return_data;}private:char*_data;};voidfoo(){::std::auto_ptrstr(newstring("hello"));::std::cout<c_str()<<::std::endl;}由于str是函数的局部对象,生命周期结束于函数退出点,此时的析构函数调用auto_ptr自动销毁内部指针维护的string对象(由构造函数中的new表达式分配),然后执行string的析构函数将其释放为实际的string动态申请内存。也可以在string中管理其他类型的资源,比如多线程环境下的同步资源。下图说明了上述过程。现在我们有了最简单的垃圾回收机制(我隐藏一点,在string中,你仍然需要自己编写控制对象的动态创建和销毁的代码,但这种情况下的原理非常简单,就是在构造函数中分配资源,在析构函数中释放资源,就像飞机驾驶员在起飞后降落前必须检查起落架。),即使foo函数发生异常,str的生命周期也会结束,C++保证当它自然退出发生的一切在异常发生时也有效。auto_ptr只是一种智能指针。它的复制行为提供了所有权转移的语义,即智能指针在复制时会转移内部维护的实际指针的所有权,例如:auto_ptrstr1(newstring());cout<c_str();auto_ptrstr2(str1);//str1内部指针不再指向原始对象cout<c_str();cout<c_str();//未定义,str1的内部指针不再有效。有时,需要共享同一个对象。这时候auto_ptr就不行了。由于一些历史原因,C++标准库没有提供其他Form智能指针,走投无路?另一种智能指针但是我们可以自己做另一种形式的智能指针,即具有值复制语义和共享值的智能指针。当同一个类的多个对象需要同时拥有一个对象的副本时,我们可以使用引用计数(ReferenceCounting/UsingCounting)来实现。它曾经是C++为了提高效率和COW(copyonwrite,copywhenrewriting))技术一起广泛使用的,后来证明在多线程应用中,COW为了保证正确的行为导致了reducedefficiency(HerbShutter'sGurucolumninC++Reportmagazineand《More Exceptional C++》完成后发表讨论这个问题)。不过对于我们现在的问题来说,引用计数本身并不是什么大问题,因为不涉及复制问题。为了保证多线程环境下的正确性,没有必要牺牲太多的效率,但是为了简化问题,这里忽略了对多线程安全的考虑。首先,我们模仿auto_ptr设计了一个类模板(来自HerbShutter的《More Execptional C++》),templateclassshared_ptr{private:classimplement//实现类,引用计数{public:implement(T*pp):p(pp),refs(1){}~implement(){deletep;}T*p;//实际指针size_trefs;//引用计数};实施*_impl;public:显式shared_ptr(T*p):_impl(newimplement(p)){}~shared_ptr(){decrease();//倒计时}shared_ptr(constshared_ptr&rhs):_impl(rhs._impl){increase();//向上计数}shared_ptr&operator=(constshared_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:voiddecrease(){if(--(_impl->refs)==0){//不再共享,销毁对象delete_impl;}}voidincrease(){++(_impl->refs);}};这个类模板非常简单,不需要过多解释代码。这里只是给出了一个简单的使用示例,这足以说明shared_ptr是一个简单的垃圾收集器替代品。voidfoo1(shared_ptr&val){shared_ptrtemp(val);*temp=300;}voidfoo2(shared_ptr&val){val=shared_ptr(newint(200));}intmain(){shared_ptrval(newint(100));cout<<"val="<<*val;foo1(val);cout<<"val="<<*val;foo2(val);cout<<"val="<<*val;}在main()函数中,先调用foo1(val),函数中使用了一个局部对象temp,它与val共享相同的数据,修改后实际value改变了,函数返回后,val拥有的value也改变了,但实际上val本身并没有被修改。然后调用foo2(val),函数使用一个未命名的临时对象创建一个新值,使用赋值表达式修饰val,同时val和临时对象有相同的值,当函数返回时,val仍然有这个正确的值。最后,在整个过程中,除了使用shared_ptr构造函数时使用new表达式创建一个新的,没有删除指针的动作,但是所有的内存管理都是正确的,这要归功于shared_ptr设计的巧妙的。有了auto_ptr和shared_ptr这两个强大的工具,应对大部分情况下的垃圾回收应该就足够了。如果需要语义比较复杂的智能指针(主要是指复制的语义),可以参考boost的源码,里面设计了几种智能指针。标准容器对于程序中需要具有相同类型的多个对象,利用好标准库提供的各种容器类可以最大程度地消除显式内存管理。然而,标准容器不适合存储指针,因此对于多个State-of-the-art的支持仍然面临困难。使用智能指针作为容器的元素类型。但是,大多数标准容器和算法都需要值复制语义。前面介绍的用于转移所有权的auto_ptr和用于自制共享对象的shared_ptr无法提供正确的值复制语义。中的HerbSutter在中设计了一个具有全复制语义的智能指针ValuePtr,解决了指针在标准容器中使用的问题。但是,多态性还是没有解决,我会专门另写一篇文章来解决使用容器管理多态对象的问题。语言支持为什么不在C++语言中添加对垃圾收集的支持?根据前面的讨论,我们可以看出,不同的应用环境可能需要不同的垃圾收集器。无论使用垃圾收集器,这些不同类型的垃圾收集器都需要集成在一起,即使它能够成功(对此我表示怀疑),而且还会导致效率成本增加。这违背了C++“不要为不必要的功能买单”的设计理念,强行让用户接受垃圾回收的代价是不可取的。相反,按需选择你需要的垃圾收集器,你需要掌握的规则比显式内存管理要简单得多,也更不容易出错。最重要的一点是C++不是一种“傻瓜式”的编程语言。他偏爱喜欢并善于思考的程序员。设计一个适合自己需求的垃圾收集器对于热爱C++的程序员来说只是一个挑战。