`

C++之父谈关于C++的五个需要被重新认识的观点(中)

阅读更多
概述:学习和使用过C++的人几乎都曾经听说过下面的五个关于C++的描述,并且对这些话笃信不已,那么现在的情况是怎么样的呢?本文的作者——C++之父Bjarne Stroustrup将会对这些观点作逐一回击。本篇为中篇,探讨其中的第三个观点。

学习和使用过C++的人几乎都曾经听说过下面的五个关于C++的描述,并且对这些话笃信不已,那么现在的情况是怎么样的呢?本文的作者——C++之父Bjarne Stroustrup将会对这些观点作逐一回击。

以下的这五个观点盛行于C++多年:

  1. “要了解C++,你必须先学习C语言。”
  2. “C++是一门面向对象的语言。”
  3. “对于可靠的软件,垃圾回收机制必不可少。”
  4. “为了提高效率,你必须编写底层代码。”
  5. “C++只对大型复杂的项目有用。”

如果你还对这些观点深信不已,那么这篇文章可以给你一些重新认识。这些观点在特定的时间对于某些人、某些工作来说是正确的。但是对于今天的C++,随着ISO C++11标准的编译器和工具的广泛使用,这些观点都需要被重新认识。 

上一篇,这一篇里我们将围绕“对于可靠的软件,垃圾回收机制必不可少。”的观点进行探讨。

观点三:“对于可靠的软件,垃圾回收机制必不可少。”

对于回收未使用的内存这份工作,垃圾回收做得不错但却不够完美。它并非灵丹妙药。内存可以被间接引用并且许多资源并非单纯的内存。来看这个例子:

C++内存回收

这里Filter的构造函数会开启两个用于数据存储的文件(file)。完成这项工作以后,Filter从输入文件执行输入任务并将产生的输出结果保存到输出文件里。 这些任务包括硬连接到Filter,作为匿名(lambda)函数,提供一个可能具有覆盖虚函数派生类的函数。在谈及资源管理时这些细节并不重要。我们可以这样创建Filter:

C++内存回收

从资源管理的角度来看,这里的问题是如何关闭文件以及对与输入输出流相关联的对象资源进行回收重用。

在许多种依托于垃圾回收的语言和系统里,常见解决方案是放弃使用delete(它很容易在编程过程中被人遗忘,从而导致内存泄漏)和析构函数(被垃圾回收后的语言中尽量少用析构函数和不用finalizer,因为它们在逻辑上令人捉摸不透并经常破坏性能)。垃圾回收器可以回收所有的内存资源,但是我们还需要使用手动操作(通过编写代码的方式)来关闭文件并释放任何与数据流相关的非内存资源(比如锁)。因此虽然内存被自动完全回收了,但是由于其它资源是手动管理的,内存的错误和泄漏仍有可能发生。

被C++推荐和使用的方法是依靠析构函数来处理资源回收的问题。值得一提的是,这些被构造函数获取的资源是通过RAII(“资源获取即初始化”)这一简单而通用的技术来处理的。在user()中,用于flt的析构函数隐式调用了用于输入输出流(IS及OS)的析构函数。这些析构函数依次关闭文件并释放与数据流相关的资源。而delete对*p会做同样的操作。

拥有丰富的现代C++开发经验的程序员会注意到user()非常笨拙且容易产生错误,而采用下面的编写方式会更好:

C++内存回收

现在当user()退出后*p需要被隐式释放。程序员不能忘记这项操作。与内置的“裸”指针不同的是,智能指针unique_ptr是一个用于确保资源释放掉后就不再需要运行时间和内存空间等系统开销的标准库类。

然而,我们仍然能够看到new。这个解决方案有点冗长(Filter类型重复了),并且由于结构被普通指针(使用的new)和智能指针(在这里是unique_ptr)分拆开而使某些重要的优化丢失。我们可以使用一个C++14的帮助函数make_unique来进行改善,它能够构造一个指定类型的对象并返回一个指向它的unique_ptr指针:

C++内存回收

除非出现需要第二个具有指针语义的Filter的情况(不太可能),否则这段代码将会更好:

C++内存回收

最后的一个版本比原来的更加简短、清晰和快速。

Filter的析构函数做了什么呢?它释放了属于Filter的资源。也就是说,它关闭了文件(通过调用它们的析构函数)。事实上,这项工作是通过隐式的方式完成的,所以除了Filter需要的一些东西,我们可以去掉Filter析构函数的显式声明并让编译器来处理这一切。因此,我只需要这样编写:

C++内存回收

这样比大多数拥有垃圾回收机制的语言(如Java或者C#)的编写都要简单,而且也不会因为程序员的健忘而导致内存泄漏。它比其它的替代方案也要快速的多(无需模拟自由/动态内存的使用且不需要运行垃圾回收器)。值得一提的是,相对于手动操作的方法RAII还降低了资源的滞留时间。

这是理想的资源管理方法。它处理的不仅是内存,还包括一般(非内存)资源,比如文件句柄、线程句柄以及锁等。但这样就够了么?对于那些需要从一个函数传递到另外一个函数的对象又该怎么办呢?对于那些没有明显的单一所有者的对象又该怎么办呢?

转移所有权:move

让我们首先来考虑将对象(所包含的信息)从一个作用域转移到另一个的问题。这个问题的关键在于在不使用copy或易错指针等需要影响系统性能的情况下如何从作用域之外获得大量关于所需对象的信息。传统的方法是使用一个指针:

C++内存回收

现在负责删除对象是谁?在这个简单的例子中,很明显是make_X()的调用者,但在通常情况下这个答案是不明确的。假如make_X()为了将系统开销降低最小而保留了对象缓存呢?假如user()将指针传递给了一些other_user()呢?这种方法产生混乱的可能性很大并且也容易产生内存泄漏。

我可以使用shared_ptr或者unique_ptr来明确所创建对象的所有权。例如:

C++内存回收

但是为什么非要使用一个指针(智能指针或者一般指针)呢?我通常都不希望使用指针,因为指针的使用与常规的对象引用不合拍。例如,一个Matrix加法函数创建了一个包含2个参数的新对象(求和),但如果返回一个指针则会导致代码变得非常奇怪: 

C++内存回收

那个*的位置应该是需要的求和结果,而不是一个指向这个结果的指针。在很多时候,我真正想获取的是一个对象,而不是指向对象的指针。而多数情况下,获取对象都会很简单,特别是对于那些小型对象,只需要简单的copy就可以了,根本不需要考虑使用指针:

C++内存回收

另一方面,一个包含大量数据信息的对象通常会处理大部分那样的数据。比如istream,string,vector,list和thread。它们只是使用了几句关于数据的简单命令就可以确保潜在的大量数据的合理访问。让我们再来看看Matrix加法,我们希望的是

C++内存回收

我们可以很容易用这种实现(创建临时对象函数):

C++内存回收

在默认的情况下,程序会把res(临时对象)的元素copy到r,但随后res会被销毁,持有这些元素所占用的内存也会被释放,我们考虑到了一种无需copy(C++的设计目标就是尽量少分配内存)的方法:直接“窃取”这些元素。从第一天学习C++的初学者到老手,每一个人都想过要这么做,但这种方法很难实现且技术还没有得到广泛理解。C++11的出现使这种构想成为了现实。它支持“窃取对象信息(steal the representation)”的理念——通过move句柄的形式转移对象所有权(即转移对象所包含信息)。来看看下面这个简单的2维双重Matrix函数:

C++内存回收

copy操作可通过引用(&)参数来识别的,同样的,move操作可通过右值引用(&&) 参数来识别。move操作可以用来“窃取”对象的信息并遗留下一个“空对象”。对于Matrix来说,这就意味着是这样的:

C++内存回收

它的机制是这样的:当编译器看到了return res,它就明白可以把res销毁了。也就是说,res在返回之后就不会再使用了。因此,编译器会立刻应用一个move构造函数而不是copy构造函数来转移返回的值。通过以下的形式:

C++内存回收

在operator+()中的res会成为空对象,然后交由析构函数来善后,而res中的元素现在已经归r所有。将对象包含的信息从函数operator+()提取出来放进调用的变量中,我们已经达成了获取元素(可能是上百万字节的内存)的结果,并且我们只使用了最小的成本(也就是差不多四行用于分配的代码)。

老道的C++用户会指出,在某些情况下,好的编译器能够完全清除掉return上所copy的信息(在本例中会保存关于move的四行代码和调用的析构函数)。然而,这是对实现的依赖,我不希望基础编程技术的性能还要由每个独立编译器的聪明程度来决定。此外,能够清除掉copy信息的编译器也能够很轻松的把move给抹掉。我们这里的就有一个用于减小把大量信息从一个作用域copy到另外一个的复杂性和所产生花费的简单、可靠、通用的方法。

通常情况下,我们甚至不需要定义所有的这些copy和move操作。如果一个类中缺乏所需的成员,我们可以依靠编译器所生成的默认操作,比如:

C++内存回收

这个版本的Matrix运行起来与上个版本很相似,除了稍微提升了对错误的处理和有一个更多一些的陈述(vector通常只有3行代码)

对于那些不是句柄的对象呢?假如它们很小,就象一个int或者一个双double类型complex那样,则无须担心。否则,需要使用nique_ptr或shared_ptr这样的智能指针来处理它们并进行返回操作。注意,不要加入“裸”指针new和delete。

不幸的是,就象我举例的Matrix类一样,某些类并不是ISO C++标准库的一部分,但是它的其中一部分还是可用的(开源和面向商业的)。例如,在网上搜索“Origin Matrix Sutton”,你可以看见在我的书The C++ Programming Language (Fourth Edition)的第29章在讨论如何设计这样的一个矩阵。

共享所有权:shared_ptr

在关于垃圾回收的讨论中,经常会看到并不是每一个对象都对应唯一的所有者。这意味着我们必须确保当对象的最后一个引用消失后,该对象是否已经被销毁/释放。在这个模型里,我们必须使用一个机制来确保当最后一个所有者被销毁后这个对象也会随之被销毁。也就是说,我们需要一个共享所有权的形式。例如,我们有一个同步队列sync_queue,用于任务之间的通信。提供者(producer)和使用者(consumer)都被赋予了一个指向sync_queue的指针:

C++内存回收

我假定task1、task2、iqueue和oqueue已经在其它地方被定义了,在这里我使用了detatch()来让线程的生存周期比创建线程的作用域更长。你可能会想到多任务管道和sync_queues。然而,在这里我感兴趣的只有一个问题:“是谁删除了startup()中所创建的sync_queue?”以书面文字来说,这问题这么提会更好:“最后使用sync_queue的是谁?”这是经典的垃圾回收调用案例。垃圾回收的原型就是计算指针:持续对使用对象计数,当计数归零则删除该对象。(当有一个指针指向自己时计数值加1;当删除一个指向自己的指针时,计数值减1,如果计数值减为0,说明已经不存在指向该对象的指针了,则可以安全销毁)。现在许多语言的垃圾回收机制都是以此为蓝本发展的而在C++11里shared_ptr就是使用的这种机制。上面的例子可变成:

C++内存回收

用于task1和task2的析构函数可以销毁它们的shared_ptrs(在大多数优秀的设计当中都会非常隐蔽的干这项工作),两者中较晚完成的会同时对sync_queue进行销毁。

这个方法简单且合理高效。它意味着一个运行复杂的系统并一定需要垃圾回收器。重要的是,它不仅可以回收与sync_queue相关的内存资源,还能够回收sync_queue中用于管理不同任务的多线程同步性的同步对象(互斥对象、锁等)。这种方法不仅适用于内存管理,还适合一般的资源管理。“隐藏”的同步对象准确处理前面例子中文件句柄和数据流缓冲器所处理的工作。

我们可以尝试通过在某些封装任务的作用域中引入一个唯一所有者来替代使用shared_ptr,当这样做起来并不一定简单,因此C++11提供了unique_ptr(用于唯一所有权)和shared_ptr(用于共享所有权)。

类型安全

前面,我只谈论了垃圾回收与资源管理的关系。在类型安全方面,垃圾回收也影响重大。只要我们有一个明确的delete操作,它就有可能被误用。例如:

C++内存回收

不要这样做,在一般的用户代码上使用“裸指针”delete是危险且多余的。让delete远离字符串、输出流、线程、unique_ptr和shared_ptr这样的资源管理类。在这些地方,delete需要与new谨慎配用来以确保无害。

摘要:资源管理理念

对于资源管理,我认为垃圾回收应该作为最后的选择,而不是作为“解决方案”或者理念:

  • 使用递归和隐式的占用抽象来处理自己的资源,对于这种作用域变量的对象来说是更好的选择。
  • 当你需要指针/引用语义时,使用如unique_ptr或者shared_ptr这样的智能指针来表示所有权。
  • 如果所有都失败了(比如,因为你的代码是一段包含缺乏内存管理和错误处理的语言特性支持的混乱指针的程序),请尝试“手动”处理非内存资源并嵌入一个保守的垃圾回收器来处理几乎不可能避免的内存泄漏。

这样的策略很完美么?不,但是至少它是简单适用的。基于传统垃圾回收的策略并不完美,它并不能直接解决非内存资源的问题。

前一篇我们探讨了“要了解C++,你必须先学习C语言。”和“C++是一门面向对象的语言。”的观点,在下一篇我们将探讨最后两个观点“为了提高效率,你必须编写底层代码。”和“C++只对大型复杂的项目有用。”

本文翻译自Five Popular Myths about C++, Part 2,作者为:C++之父Bjarne Stroustrup 

 

1
0
分享到:
评论

相关推荐

    C++之父Bjarne谈C++的未来发展

    《C++之父Bjarne谈C++的未来发展》一文深入探讨了C++语言及其标准库的发展方向,尤其关注于解决现有标准中的一些缺陷,以及如何更好地支持泛型编程和初学者。以下是对该文章核心观点的详细解析: ### 泛型编程的...

    Linux之父炮轰C++是糟糕程序员的垃圾语言

    Linux 之父 Linus Torvalds 在最近的一篇文章中炮轰 C++,称其为糟糕的语言,认为它会导致非常糟糕的设计选择和低效的抽象编程模型。Torvalds 认为,C++ 的所谓优点只是巨大的错误,并且会导致项目中的混乱和垃圾...

    给C++初学者的忠告

    - **解释**:这两本书分别由C++之父Bjarne Stroustrup和Scott Meyers撰写,是学习C++不可多得的资源。它们不仅涵盖了语言的基础部分,还深入讲解了高级主题和内部实现机制。 - **建议**:虽然这些书籍可能比较晦涩...

    二十三种设计模式【PDF版】

    之道 》,其中很多观点我看了很受启发,以前我也将"设计模式" 看成一个简单的解决方案,没有从一种高度来看待"设计模式"在软 件中地位,下面是我自己的一些想法: 建筑和软件某些地方是可以来比喻的 特别是中国传统建筑...

    cmd-bat-批处理-脚本-Screenshot.zip

    cmd-bat-批处理-脚本-Screenshot.zip

    升\降压电路的自计算表格 及 公式表达

    公式主要来自于德州仪器的datasheet 以及 一些电路公式表达式

    2025年自动检测生产线项目大数据研究报告.docx

    2025年自动检测生产线项目大数据研究报告.docx

    cmd-bat-批处理-脚本-deactivate.zip

    cmd-bat-批处理-脚本-deactivate.zip

    cmd-bat-批处理-脚本-happy05 1.zip

    cmd-bat-批处理-脚本-happy05 1.zip

    基于MATLAB的单相光伏并网逆变器仿真研究

    在单相光伏逆变器相关领域,涉及诸多关键环节。首先,光伏系统建模是基础,其中光伏板作为能量来源,其特性建模至关重要。最大功率点跟踪(MPPT)技术用于确保光伏板输出功率最大化,而Boost升压电路则负责将光伏板输出的较低电压提升至适合逆变器处理的水平。在控制策略方面,电压电流双闭环控制是实现稳定输出的关键,通过精确控制电压和电流,保证逆变器的性能。最终目标是使并网电流波形达到标准正弦波形,以满足电网接入要求。希望与大家深入交流这些内容,共同探讨技术细节与优化方案。

    cmd-bat-批处理-脚本-JoinDomain.zip

    cmd-bat-批处理-脚本-JoinDomain.zip

    cmd-bat-批处理-脚本-ppcp.zip

    cmd-bat-批处理-脚本-ppcp.zip

    最新修复版走路赚钱乐步2.0任务平台系统源码

    内附详细安装教程,亲测搭建无问题。 一、乐步交易流程----购买乐步糖果 方法一:在卖方市场选择合适的卖家或者用手机号定向查询特定卖家 步骤一:点击首页下方【交易中心】。 步骤二:点击【卖单列表】,选择合适的卖家或者用手机号搜索特定卖家,确定卖家之后点击该卖家后方的【购买】。 步骤三:点击之后,系统会显示该卖家的收款信息。按照系统显示的收款信息付款,(付款备注交易订单号)付款完成之后上传凭证,等待卖家确认收款并且支付糖果。 方法二:挂单买入糖果 步骤一:点击首页下方【交易中心】。 步骤二:点击【买单列表】--【发布买单】,填写购买糖果单价、数量、交易密码,点击【确定】,买单发布,等待匹配成交。 二、乐步交易流程----出售乐步糖果 方法一:在买方市场选择合适的买家或者用手机号定向查询特定买家 步骤一:点击首页下方【交易中心】。 步骤二:点击【买单列表】,选择合适的买家或者用手机号搜索特定买家,确定买家之后点击该买家后方的【出售】。 步骤三:点击之后,系统会提示买家付款,买家按照系统提示的账号给卖家付款(付款备注交易订单号),付款完成之后上传凭证,等待卖家确定并且支付糖果。 方法二:挂单卖出糖果 步骤一:点击首页下方【交易中心】。 步骤二:点击【卖单列表】--【发布卖单】,填写出售糖果单价、数量、验证码、交易密码,点击【确定】,卖单发布,等待匹配成交。

    多媒体技术及应用实验三(音视频编码转换软件开发)

    包括一个python源程序和一个.exe文件

    永磁同步电机速度环控制中的多种PID自整定技术及其应用 RBF神经网络

    内容概要:本文探讨了永磁同步电机(PMSM)速度环控制中多种PID自整定技术的应用,包括RBF神经网络PID、基于分解合并机制的RBF神经网络PID、基于小波神经网络的PID、粒子群算法优化PID、天牛须算法优化PID以及模糊PID自整定。每种技术都通过具体的数学模型和代码片段进行了详细的解释,旨在提升PMSM速度环控制的精度和效率。 适合人群:从事电机控制系统研究和开发的技术人员,尤其是对PID自整定技术和智能算法感兴趣的工程师。 使用场景及目标:适用于需要改进现有PMSM速度环控制系统的场合,目标是通过引入先进的PID自整定技术,提高系统的响应速度、稳定性和鲁棒性。 其他说明:文中不仅介绍了各种技术的基本原理,还提供了部分Python代码示例,帮助读者更好地理解和实践这些方法。同时,强调了不同技术之间的对比和优势,便于读者根据实际情况选择最合适的技术路径。

    桔子云测评小程序V1.1.1+前端.zip

    桔子云测评小程序,做专业测评系统小程序平台,支持微信小程序和抖音小程序,为网友提供心理测试,帮助你更好地了解自己的兴趣、性格、能力等特点,找到适合自己的成长之路。 盈利模式 流量主、激励视频解锁、单独付费测评、VIP会员付费等 功能特色 1、支持定义3种题型:单题型、多题型、 有因子多题型 2、 因子题型支持算法自定义分析 3、答案支持单独自定义分享海报 4、IOS端可设置联系客服索取激活码付费方式 5、支持量表导入 6、支持跳转其他小程序 7、支持分销推广 版本号:1.1.1 – 多开商业版 【修复】添加项目出现分类串联问题 【修复】快速测试出现结果错误问题 【优化】重新测试体验流程 toutiao前端、微信前端都需要提交审核

    2025年职称计算机考试题型及大纲.doc

    2025年职称计算机考试题型及大纲.doc

    cmd-bat-批处理-脚本-TV no signal color bars.zip

    cmd-bat-批处理-脚本-TV no signal color bars.zip

    实证数据-2009-2023上市公司-绿色治理绩效数据-社科经管.rar

    该数据集为2009-2023年中国上市公司绿色治理绩效(GGP)面板数据,覆盖1557家上市公司,数据来源于华证ESG评级、上市公司年报及社会责任报告等公开披露信息。核心指标包括污染物排放达标/未达标得分、突发环境事故、环境违法事件、ISO14001认证情况等12项环境治理指标,采用Janis-Fadner系数法计算综合绩效值(GGP),反映企业在环境合规、绿色运营及社会责任履行等方面的表现。数据经学术团队整理校验,参考《管理世界》等期刊的测度方法,可直接用于ESG表现、绿色创新等领域的实证研究。部分样本包含财务指标匹配数据,便于多维度分析。

    基于GJO-TCN-BiGRU-Attention的Matlab多变量时间序列预测算法及应用 BiGRU Matlab源码与数据集:GJO-TCN-BiGRU-Attention金豹算法优化多变量时间

    内容概要:本文介绍了利用Matlab实现的基于GJO-TCN-BiGRU-Attention算法的时间序列预测方法。该方法结合了时间卷积网络(TCN)、双向门控循环单元(BiGRU)以及注意力机制,用于多变量时间序列预测。文中详细描述了模型架构的设计思路及其各部分的功能,如TCN层用于捕捉长期依赖关系,BiGRU处理双向时序特征,而注意力层则赋予不同特征不同的权重。此外,还探讨了参数优化的方法——采用金豹优化(GJO)算法调整学习率、神经元数目、注意力机制的关键参数等超参数,并提供了完整的源代码和数据集。实验结果显示,该模型在电力负荷预测任务中表现出色,相比单一模型提升了大约8个百分点。 适用人群:对时间序列预测感兴趣的科研工作者、研究生及以上水平的数据科学家和技术爱好者。 使用场景及目标:适用于需要进行高精度多变量时间序列预测的应用场合,比如能源管理系统的负荷预测、金融市场趋势分析等领域。目标是提高预测准确性,降低误差。 其他说明:文中提到一些实践经验,例如避免TCN层数过多导致梯度爆炸的问题,推荐使用RobustScaler进行数据标准化处理,以及选择合适的序列滑窗长度等技巧。

Global site tag (gtag.js) - Google Analytics