5.2.4 C++与垃圾回收
如我们提到的,在C++11中,智能指针等可以支持引用计数。不过由于引用计数并不能有效解决形如“环形引用”等问题,其使用会受到一些限制。而且基于一些其他的原因,比如因多线程程序等而引入的内存管理上的困难,程序员可能也会需要垃圾回收。
一些第三方的C/C++库已经支持标记-清除方法的垃圾回收,比如一个比较著名的C/C++垃圾回收库——Boehm[1]。该垃圾回收器需要程序员使用库中的堆内存分配函数显式地替代malloc,继而将堆内存的管理交给垃圾回收器来完成垃圾回收。不过由于C/C++中指针类型的使用非常灵活,这样的库在实际使用中会有一些限制,可移植性也不好。
为了解决垃圾回收中的安全性和可移植性问题,在2007年,惠普的Hans-J.Boehm(Boehm的作者)和赛门铁克的Mike Spertus共同向C++委员会递交了一个关于C++中垃圾回收的提案。该提案通过添加gc_forbidden、gc_relaxed、gc_required、gc_safe、gc_strict等关键字来支持C++语言中的垃圾回收。该提案甚至可以让程序员显式地要求垃圾回收。刚开始这得到了大多数委员的支持,后来却在标准的初稿中删除了,原因是该特性过于复杂,并且还存在一些问题(比如与显式调用析构函数的现有的库的兼容问题等)。所以,Boehm和Spertus对初稿进行了简化,仅仅保留了支持垃圾回收的最基本的部分,即通过对语言的约束,来保证安全的垃圾回收。这也是我们现在看到的C++11标准中的“最小垃圾回收支持”的历史来由。
而要保证安全的垃圾回收,首先必须知道C/C++语言中什么样的行为可能导致垃圾回收中出现“不安全”的状况。简单地说,不安全源自于C/C++语言对指针的“放纵”,即允许过分灵活的使用。我们可以看代码清单5-10所示的例子。
代码清单5-10
int main(){
int*p=new int;
p+=10;//移动指针,可能导致垃圾回收器
p-=10;//回收原来指向的内存
*p=10;//再次使用原本相同的指针则可能无效
}
//编译选项:g++5-2-3.cpp
在代码清单5-10中,我们对指针p做了自加和自减操作。这在C/C++中被认为是合理的,因为指针有所指向的类型,自加或者自减能够使程序员轻松地找到“下一个”同样的对象(实际是一个迭代器的概念)。不过对于垃圾回收来说,一旦p指向了别的地址,则可认为p曾指向的内存不再使用。垃圾回收器可以据此对其进行回收。这对之后p的使用(*p=10)带来的后果将是灾难性的。
我们再来看一个例子,如代码清单5-11所示。
代码清单5-11
int main(){
int*p=new int;
intq=(int)(reinterpret_cast<long long>(p)^2012);//q隐藏了p
//做一些其他工作,垃圾回收器可能已经回收了p指向对象
q=(int*)(reinterpret_cast<long long>(q)^2012);//这里的q==p
*q=10;
}
//编译选项:g++5-2-4.cpp
在代码清单5-11中,我们用指针q隐藏了指针p。而之后又用可逆的异或运算将p“恢复”了出来。在main函数中,p实际所指向的内存都是有效的,但由于该指针被隐藏了,垃圾回收器可以早早地将p指向的对象回收掉。同样,语句*q=10的后果也是灾难性的。
指针的灵活使用可能是C/C++中的一大优势,而对于垃圾回收来说,却会带来很大的困扰。被隐藏的指针会导致编译器在分析指针的可达性(生命周期)时出错。而即使编译器开发出了隐藏指针分析的手段,其带来的编译开销也不会让程序员对编译时间的显著增长视而不见。历史上,解决这种问题的方法通常是新接口。C++11和垃圾回收的解决方案也不例外,就是让程序员利用这样的接口来提示编译器代码中存在指针不安全的区域。