1.前言

Java的爱好者们经常批评C++中没有提供与Java类似的垃圾回收(Gabage Collector)机制(这很正常,正如C++的爱好者有时也攻击Java没有这个没有那个,或者这个不行那个不够好)。
垃圾回收导致C++中对动态存储的管理称为程序员的噩梦,不是吗?经常听到的是内存遗失(memory leak)和非法指针存取,这一定令你很头疼,而且你又不能抛弃指针带来的灵活性。
此文中,我并不想揭露Java提供的垃圾回收机制的天生缺陷,而是指出了C++中引入垃圾回收的可行性。请注意,这里介绍的方法更多的是基于当前标准和库设计的角度,而不是要求修改语言定义或者扩展编译器。

2.什么是垃圾回收?

作为支持指针的编程语言,C++将动态管理存储器资源的便利性交给了程序员。在使用指针形式的对象时(请注意,由于引用在初始化后不能更改引用目标的语言机制的限制,多态性应用大多数情况下依赖于指针进行),程序员必须自己完成存储器的分配、使用和释放,语言本身在此过程中不能提供任何帮助,也许除了按照你的要求正确的和操作系统亲密合作,完成实际的存储器管理。标准文本中,多次提到了“未定义(undefined)”,而这大多数情况下和指针相关。
某些语言提供了垃圾回收机制,也就是说程序员仅负责分配存储器和使用,而由语言本身负责释放不再使用的存储器,这样程序员就从讨厌的存储器管理的工作中脱身了。
然而C++并没有提供类似的机制,C++的设计者Bjarne Stroustrup在我所知的唯一一本介绍语言设计的思想和哲学的著作《The Design and Evolution of C++》(中译本:C++语言的设计和演化)中花了一个小节讨论这个特性。简而言之,Bjarne本人认为,“我有意这样设计C++,使它不依赖于自动垃圾回收(通常就直接说垃圾回收)。这是基于自己对垃圾回收系统的经验,我很害怕那种严重的空间和时间开销,也害怕由于实现和移植垃圾回收系统而带来的复杂性。
还有,垃圾回收将使C++不适合做许多底层的工作,而这却正是它的一个设计目标。但我喜欢垃圾回收的思想,它是一种机制,能够简化设计、排除掉许多产生错误的根源。
需要垃圾回收的基本理由是很容易理解的:用户的使用方便以及比用户提供的存储管理模式更可靠。而反对垃圾回收的理由也有很多,但都不是最根本的,而是关于实现和效率方面的。
已经有充分多的论据可以反驳:每个应用在有了垃圾回收之后会做的更好些。类似的,也有充分的论据可以反对:没有应用可能因为有了垃圾回收而做得更好。
并不是每个程序都需要永远无休止的运行下去;并不是所有的代码都是基础性的库代码;对于许多应用而言,出现一点存储流失是可以接受的;许多应用可以管理自己的存储,而不需要垃圾回收或者其他与之相关的技术,如引用计数等。
我的结论是,从原则上和可行性上说,垃圾回收都是需要的。但是对今天的用户以及普遍的使用和硬件而言,我们还无法承受将C++的语义和它的基本库定义在垃圾回收系统之上的负担。”
以我之见,统一的自动垃圾回收系统无法适用于各种不同的应用环境,而又不至于导致实现上的负担。稍后我将设计一个针对特定类型的可选的垃圾回收器,可以很明显地看到,或多或少总是存在一些效率上的开销,如果强迫C++用户必须接受这一点,也许是不可取的。
关于为什么C++没有垃圾回收以及可能的在C++中为此做出的努力,上面提到的著作是我所看过的对这个问题叙述的最全面的,尽管只有短短的一个小节的内容,但是已经涵盖了很多内容,这正是Bjarne著作的一贯特点,言简意赅而内韵十足。
下面一步一步地向大家介绍我自己土制佳酿的垃圾回收系统,可以按照需要自由选用,而不影响其他代码。

3.构造函数和析构函数

C++中提供的构造函数和析构函数很好的解决了自动释放资源的需求。Bjarne有一句名言,“资源需求就是初始化(Resource Inquirment Is Initialization)”。
因此,我们可以将需要分配的资源在构造函数中申请完成,而在析构函数中释放已经分配的资源,只要对象的生存期结束,对象请求分配的资源即被自动释放。
那么就仅剩下一个问题了,如果对象本身是在自由存储区(Free Store,也就是所谓的“堆”)中动态创建的,并由指针管理(相信你已经知道为什么了),则还是必须通过编码显式的调用析构函数,当然是借助指针的delete表达式。

3.1 智能指针

幸运的是,出于某些原因,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++的标准库中并没有提供其他形式的智能指针,走投无路了吗?

3.2 另一种智能指针

但是我们可以自己制作另一种形式的智能指针,也就是具有值复制语义的,并且共享值的智能指针。
需要同一个类的多个对象同时拥有一个对象的拷贝时,我们可以使用引用计数(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++》),

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的源代码,其中设计了多种类型的智能指针。

3.3 标准容器

对于需要在程序中拥有相同类型的多个对象,善用标准库提供的各种容器类,可以最大限度的杜绝显式的内存管理,然而标准容器并不适用于储存指针,这样对于多态性的支持仍然面临困境。
使用智能指针作为容器的元素类型,然而标准容器和算法大多数需要值复制语义的元素,前面介绍的转移所有权的auto_ptr和自制的共享对象的shared_ptr都不能提供正确的值复制语义,Herb Sutter在《More Execptional C++》中设计了一个具有完全复制语义的智能指针ValuePtr,解决了指针用于标准容器的问题。

4.语言支持

为什么不在C++语言中增加对垃圾回收的支持?
根据前面的讨论,我们可以看见,不同的应用环境,也许需要不同的垃圾回收器,不管三七二十一使用垃圾回收,需要将这些不同类型的垃圾回收器整合在一起,即使可以成功,也会导致效率成本的增加。
这违反了C++的设计哲学,“不为不必要的功能支付代价”,强迫用户接受垃圾回收的代价并不可取。相反,按需选择你自己需要的垃圾回收器,需要掌握的规则与显式的管理内存相比,简单的多,也不容易出错。
最关键的一点, C++并不是“傻瓜型”的编程语言,他青睐喜欢和善于思考的编程者,设计一个合适自己需要的垃圾回收器,正是对喜爱C++的程序员的一种挑战。

文章引用:C++开发者公众号,编辑:沈春旭,转载请注明来源。

为什么我们批评C++?又爱又恨的垃圾回收机制相关推荐

  1. Java面试官最爱问的垃圾回收机制,Java编程配置思路详解

    Java编程配置优点:相对于xml配置而言,其结构更清晰,可读性更高,同时也节省了解析xml耗时. Java编程配置缺点:修改应用配置参数需要重新编译.其实并不是一个大的问题,实际生成环境中,应用配置 ...

  2. Java面试官最爱问的垃圾回收机制,mysql密码忘记

    一.硬核! 30张图解HTTP常见面试题 在面试过程中.HTTP被提问的概率还是比较高的. 小编我授集了5大类HTTP面试常问的题目,同时这5大类题跟HTTP的发展和演变关联性是比较大的,通过问答+图 ...

  3. Java面试官最爱问的垃圾回收机制,这位阿里P7大佬分析总结的属实到位

    可达性分析算法:判断对象的引用链是否可达 可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收. 可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的 ...

  4. Java面试官最爱问的垃圾回收机制,mysqlssl连接

    说在前面 已经到了月中旬了,程序员们即将迎来面试季,今天刚好有一位粉丝找到我,他上周刚面完奇虎360,经过了几轮的面试,最后薪资也谈了,今天主要是问我想这样的大型互联网公司一般多久会正式下offer. ...

  5. 2020最全JVM垃圾回收机制面试题整理,阿里面试官最爱问的都在这里了(附答案)

    前言 为什么需要垃圾回收 首先我们来聊聊为什么会需要垃圾回收,假设我们不进行垃圾回收会造成什么后果,我们举一个简单的例子 我们住在一个房子里面,我们每天都在里面生活,然后垃圾都丢在房子里面,又不打扫, ...

  6. 《花落红尘》:对两性社会非爱即恨的文学消解

    李素红是个我比较陌生的女作家,读了她的<花落红尘>(作家出版社2011年版),我非常惊讶.据说作者是搞书法的,且成绩斐然,还有自己的公司,她的写作几乎是在业余时间.由此我想到了" ...

  7. 让你又爱又恨的推荐系统--程序猿篇

    正文共8957个字,13张图,预计阅读时间23分钟. 又爱又恨的推荐系统 作为一名程序猿,一直对推荐系统比较感兴趣,最近看到一个用户的吐槽: 又爱又恨 推荐系统的应用场景,我相信在日常生活中大家基本都 ...

  8. 360 一个让人又爱又恨的公司

    今天中午吃完饭,看到了一篇博客,说51cto要做11年的博客评选,本博客我也转贴10年的博客排行,有点兴趣,就点进去看了,没想到排行第一的网路游侠,之前对此人有些了解,也看了他的一些博客.由于好奇有顺 ...

  9. 专访傲游CEO陈明杰:为何微软对IE8“又爱又恨”

    随着IE 8 Beta 2和Google Chrome的推出,浏览器战场注定会变得更加硝烟弥漫,就连世界著名前端大师.JSON的创立者Douglas Crockford也忍不住在本月的一次技术论坛上高 ...

最新文章

  1. JavaWeb--数据库添加
  2. 一篇文章教你学会Java基础I/O流
  3. 最高的奖励 51Nod - 1163(贪心+并查集)
  4. 【codevs1033】蚯蚓的游戏问题,费用流
  5. php openssl des ecb,php7.2 des-ede3-ecb加密报错:openssl_encrypt():Unknown cipher algorithm 落叶随风博客...
  6. 关于spring MVC机制,示例解读
  7. BZOJ 2337 XOR和路径(概率DP)
  8. Julia : “;”和[] 引发的差别
  9. sql server需要存储1000万条数据该怎么办?
  10. CSS 巧妙实现文字二次加粗再加边框
  11. tomcat闪退没有报错_越狱后直接换sileo商店附Sileo的部分报错解决办法
  12. pytorch动态调整学习率之Poly策略
  13. 如何将BMP文件转换为JPG文件
  14. HR 问你为什么离职时是什么意思
  15. 【C语言趣味编程100题】
  16. 网上书店(基于JavaWeb和Mysql)项目
  17. 共享单车靠什么赚钱?
  18. COUNT计算机公式,countif函数的使用方法(统计考勤函数计算公式)
  19. 洛谷 P2947 [USACO09MAR]向右看齐Look Up (队列)
  20. Window10 应用商店闪退问题

热门文章

  1. 开展project 正常的生活之路
  2. MSN即将退役,即时通讯开放平台成趋势
  3. RIP(Routing Information Protocol)精析04
  4. HD 1159 Common Subsequence (最长公共子序列)
  5. SQL Server 创建用户及权限管理
  6. NYOJ 28 大数阶乘
  7. NYOJ 17 单调递增最长公共子序列
  8. NYOJ 252 01串 dp
  9. React Native获取手机的各种高度
  10. SpringBoot学习笔记(一)整合Mybatis