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

C语言与CPP编程 2022-06-24 08:40

来源:http://www.codeceo.com/article/why-cpp-not-use-gc.html

作者:M-先生


Java的爱好者们经常批评C++中没有提供与Java类似的垃圾回收(Gabage Collector)机制(这很正常,正如C++的爱好者有时也攻击Java没有这个没有那个,或者这个不行那个不够好),导致C++中对动态存储的官吏称为程序员的噩梦,不是吗?你经常听到的是内存遗失(memory leak)和非法指针存取,这一定令你很头疼,而且你又不能抛弃指针带来的灵活性。


在本文中,我并不想揭露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++》),

template 
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 的构造函数时使用了new表达式创建新之外,并没有任何删除指针的动作,但是所有的内存管理均正确无误,这就是得益于shared_ptr的精巧的设计。

拥有了auto_ptr和shared_ptr两大利器以后,应该足以应付大多数情况下的垃圾回收了,如果你需要更复杂语义(主要是指复制时的语义)的智能指针,可以参考boost的源代码,其中设计了多种类型的智能指针。

标准容器

对于需要在程序中拥有相同类型的多个对象,善用标准库提供的各种容器类,可以最大限度的杜绝显式的内存管理,然而标准容器并不适用于储存指针,这样对于多态性的支持仍然面临困境。

使用智能指针作为容器的元素类型,然而标准容器和算法大多数需要值复制语义的元素,前面介绍的转移所有权的auto_ptr和自制的共享对象的shared_ptr都不能提供正确的值复制语义,Herb Sutter在《More Execptional C++》中设计了一个具有完全复制语义的智能指针ValuePtr,解决了指针用于标准容器的问题。

然而,多态性仍然没有解决,我将在另一篇文章专门介绍使用容器管理多态对象的问题。

语言支持

为什么不在C++语言中增加对垃圾回收的支持?

根据前面的讨论,我们可以看见,不同的应用环境,也许需要不同的垃圾回收器,不管三七二十一使用垃圾回收,需要将这些不同类型的垃圾回收器整合在一起,即使可以成功(对此我感到怀疑),也会导致效率成本的增加。

这违反了C++的设计哲学,“不为不必要的功能支付代价”,强迫用户接受垃圾回收的代价并不可取。

相反,按需选择你自己需要的垃圾回收器,需要掌握的规则与显式的管理内存相比,简单的多,也不容易出错。

最关键的一点, C++并不是“傻瓜型”的编程语言,他青睐喜欢和善于思考的编程者,设计一个合适自己需要的垃圾回收器,正是对喜爱C++的程序员的一种挑战。

- EOF -

C语言与CPP编程 C语言/C++开发,C语言/C++基础知识,C语言/C++学习路线,C语言/C++进阶,数据结构;算法;python;计算机基础等
评论
  • 在现代生活中,我们经常会遇到需要检测电线是否带电的情况。这时,一款好用的数显测电笔就显得尤为重要了。今天,我想跟大家分享一下DELIXI数显测电笔的使用方法,通过一个故事来讲述它如何帮助我们解决生活中的小麻烦。 在一个阳光明媚的周末,小明决定对他家的电路进行一次全面的检查。他知道,虽然自己不是专业的电工,但有了DELIXI数显测电笔的帮助,他也能轻松应对。 小明拿出了DELIXI数显测电笔,这款测电笔设计得非常人性化,操作起来也很简单。他首先注意到了测电笔上的两个按键:DIRECT(A键)和
    丙丁先生 2024-12-16 12:58 44浏览
  • 一、引言在数字化时代,芯片作为现代科技的核心,其制造过程却常被视作神秘的黑箱。菊地正典的《大话芯片制造》为我们揭开了这层神秘的面纱,以通俗易懂的方式,全面系统地介绍了芯片制造的各个环节。作为一名电子信息技术专业的教育工作者,我深感这本书不仅为学生提供了宝贵的知识资源,也让我对芯片制造及其在现代社会中的作用有了更深刻的理解。二、生活中的芯片印记芯片的影响渗透到我们日常生活的每一个角落。从智能手机的闹钟唤醒,到交通卡的便捷支付,再到智能家居的智能化功能,芯片以其强大的运算和处理能力,为我们的现代生活
    月光 2024-12-16 11:52 50浏览
  • 光耦合器是现代电子系统中的关键组件,可在实现电路间信号传输的同时提供电气隔离。然而,人们经常对其功能、选择和应用感到困惑。本文旨在澄清常见的误解,并为工程师和业余爱好者提供必要的见解。什么是光耦合器?光耦合器或光隔离器由封装在一个封装中的发光二极管(LED)和光电探测器(如光电晶体管或光电二极管)组成。当电流通过LED时,LED会发光。光电探测器检测到该光,并产生相应的输出信号。这种机制允许在电气隔离输入和输出的同时传输信号,保护敏感元件免受高压和噪声的影响。关于光耦合器的常见困惑1.了解功能许
    腾恩科技-彭工 2024-12-13 16:17 48浏览
  • 在现代软件开发领域,效率和可靠性是企业在竞争中取胜的关键。本文将深入探讨 ANA Systems 如何通过引入业界领先的 CI/CD 平台——CircleCI,克服传统开发流程的瓶颈,实现开发运营效率的全面提升。同时,本文还将详细解析 CircleCI 的核心优势,包括其强大的自动化功能、广泛的工具整合能力,以及为企业量身定制的支持服务,揭示其如何助力 ANA Systems 在「新一代国内旅客项目」中脱颖而出。这一案例将为企业优化开发流程、提升竞争力提供重要的实践参考。ANA Systems
    艾体宝IT 2024-12-16 16:44 55浏览
  • 家用国产固态继电器(SSR)已成为各行各业的基石,性能可靠、设计紧凑、效率高。这些先进的开关设备取代了传统的机电继电器,具有静音运行、使用寿命更长、可靠性更高等诸多优点。家用SSR专为从工业自动化到家用电器等各种应用而设计,展示了本地制造商的独创性和竞争力。国产固态继电器特点和优势家用SSR采用半导体技术制造,与传统继电器相比,具有很强的耐磨性。主要特点包括:静音无振动运行:SSR使用半导体元件进行开关,消除了机械噪音。响应时间快:是工业控制系统中高速开关的理想选择。耐用性:没有移动部件,即使在
    克里雅半导体科技 2024-12-13 16:49 45浏览
  • 串口调试助手软件:XCOM 也是一款专为嵌入式开发和硬件调试设计的强大工具,如正点原子串口调试助手 XCOM V2.6。这款软件支持多种串口参数配置,满足不同开发需求,广泛应用于嵌入式系统开发、硬件调试以及电子爱好者的项目开发中。XCOM在嵌入式开发和硬件调试中的作用主要体现在以下几个方面: 1. 串口通信测试:XCOM作为一款强大的串口调试工具,允许用户通过计算机的串口进行数据的发送与接收,从而实现对串口通信的测试。这对于验证硬件设备的通信协议、确保数据传输的正确性至关重要。 2. 数据发
    丙丁先生 2024-12-15 11:56 63浏览
  • 提高团队响应速度,优化维护运营在工业或商业建筑中,集中告警管理已成为确保安全性或检测故障的必备工具。通过将所有安全系统集中管理,企业能够将所有告警统一在一个HMI界面中,大幅提升响应速度。关键要点✔ 集成所有安全系统,获取全面的告警视图。✔ 集中管理告警,可自动触发维护请求,提升团队响应效率。一、安全系统的统一管理通过集中管理,监控摄像头、对讲系统、入侵检测、火灾报警和门禁控制等安全设备可以整合到一个统一的界面中。这不仅提供了全局的告警视图,还能更好地检测潜在威胁。1.实时全局视图通过全面掌握设
    宏集科技 2024-12-13 15:58 35浏览
  • 全球领先的光学解决方案供应商艾迈斯欧司朗(瑞士证券交易所股票代码:AMS)于10月23日在深圳益田威斯汀酒店举办了艾迈斯欧司朗中国发展中心(以下简称,CDC)圆桌论坛。本次论坛以“智能化时代,以多元应用场景和技术 助力中国市场加速发展”为核心议题,探讨在人工智能驱动的市场趋势下,CDC如何助力中国伙伴把握时代机遇,推动大中华地区业务稳健增长,展示了艾迈斯欧司朗对中国市场的信心。立足中国 贴近本土客户需求在全球经济增速放缓和国际形势复杂多变的背景下,中国市场依然保持了稳定的增长势态,并释放出强大的
    艾迈斯欧司朗 2024-12-16 18:00 50浏览
  •        霍尔传感器是一种基于霍尔效应的传感器。霍尔效应指的是当通过一个导体的电流受到外部磁场的影响时,导体内部将会产生一种电场,使得在导体两端的电势差发生变化,这种电势差变化称为霍尔电势差。利用这种现象,可以设计出一种可以测量磁场强度和方向的传感器,即霍尔传感器。  霍尔传感器分为线型霍尔传感器和开关型霍尔传感器两种。  (一)开关型霍尔传感器由稳压器、霍尔元件、差分放大器,斯密特触发器和输出级组成,它输出数字量。开关型霍尔传感器还有一种特
    锦正茂科技 2024-12-14 10:58 61浏览
  • 光耦合器是一种重要的电子元件,其在电子信号隔离和传输中的作用不可替代。自20世纪60年代首次被研发以来,光耦合器经历了从基础隔离器件到高性能元件的不断演化,在现代电子设备中占据了重要地位。本文将深入探讨光耦合器的发展历程、技术特点以及在当今科技领域中的广泛应用。光耦合器的诞生背景光耦合器的诞生源于20世纪60年代,为了解决电子信号在不同电路之间传输时的隔离问题,科学家们设计了一种基于光信号传递的全新器件。光耦合器通过发光二极管(LED)将电信号转化为光信号,再由光敏器件接收并重新转换为电信号,从
    腾恩科技-彭工 2024-12-13 16:18 42浏览
  • 擎天柱,这个名字听起来就像是从科幻电影里走出来的英雄。但今天,我们要聊的不是那个变形金刚,而是一款同样令人兴奋的实验板——Ai8051U-LQFP48 转 89C52-DIP40 核心功能实验板。这款实验板就像是电子世界的“擎天柱”,它拥有强大的力量和无限的潜力,等待着我们去发掘和探索。 想象一下,你手中握着的不是一块普通的电路板,而是一张通往未来科技世界的门票。Ai8051U芯片,这颗强大的心脏,内置了硬件浮点运算单元(TFPU@120MHz),让你的计算速度飞起来,就像给机器人装上了翅膀。
    丙丁先生 2024-12-16 13:02 43浏览
  • 概述 Cyclone 10 GX器件的ALM结构与Cyclone V类似,所以在Cyclone 10 GX器件上实现TDC功能理论上是可以完全参考甚至移植自Cyclone V系列的成功案例。但是,现实却是更多的问题出现当在Cyclone 10 GX使用和Cyclone V同样策略实现TDC的时候。 本文主要记录在Cyclone 10 GX器件上实现TDC时的探索,并为后续TDC设计、测试等展开前期研究。Cyclone 10 GX ALM结构 如图1所示,Cyclone 10 GX器件的ALM结构
    coyoo 2024-12-14 17:15 61浏览
  • 习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-13 23:20 58浏览
  • 霍尔传感器的原理        霍尔传感器是一种固体的传感器,其输出电压与磁场强度成比例。顾名思 义,这种器件是依赖于霍尔效应原理工作的。霍尔效应原理是在导体通电 和加有磁场的情况下,在导体的横向 上会产生电压。电子(在实践中多数载流子最常被使 用)在外部电场的驱动下会产生“漂移”,当暴露于磁场中时,这些运动 的带电粒子会受到一个垂直于电场和 磁场的力的作用。这个力会让导体的边缘充电,一边为正,一边为负。边
    锦正茂科技 2024-12-14 11:41 50浏览
  • 习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-14 20:56 62浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦