代码之家  ›  专栏  ›  技术社区  ›  BefittingTheorem

C++中只使用堆栈的后果

  •  16
  • BefittingTheorem  · 技术社区  · 14 年前

    假设我认识一个对C++新手的人。他不传指针(正确地说),但他拒绝通过引用。他总是使用传递值。原因是,他认为“通过参照传递物体是一个坏设计的标志”。

    这个程序是一个小的图形程序,大多数传递的问题是数学向量(三元组)对象。有一些大的控制器对象,但没有比这更复杂的了。

    我发现很难找到一个只反对使用堆栈的杀手论点。

    我认为对于向量这样的小对象来说,传递值是可以的,但是即使这样,代码中也会出现大量不必要的复制。按值传递大型对象显然是浪费的,而且很可能不是您在功能上想要的。

    在pro方面,我相信栈在分配/释放内存方面更快,并且具有恒定的分配时间。

    我唯一能想到的主要论点是堆栈可能会溢出,但我猜想这不可能发生?是否有其他反对只使用堆栈/传递值而不使用传递引用的参数?

    13 回复  |  直到 14 年前
        1
  •  14
  •   missingfaktor Kevin Wright    14 年前

    子类型多态性是这样一种情况:传递值不起作用,因为您将派生类切片为其基类。也许对某些人来说,使用子类型多态性是不好的设计?

        2
  •  6
  •   Jonathan Leffler    14 年前

    在没有使用引用的情况下,有各种各样的事情是无法完成的——从一个复制构造函数开始。引用(或指针)是基本的,不管他喜欢与否,他都在使用引用。(引用的一个优点,或者可能是缺点是,一般来说,您不必更改代码来传递(const)引用。)而且大多数时候没有理由不使用引用。

    是的,对于没有动态分配要求的小对象来说,传递值是可以的,但是通过说“没有引用”而不进行具体的测量,使自己陷入困境仍然是愚蠢的,所谓的开销(a)是可感知的,(b)是显著的。过早的优化是万恶之源” .

    各种属性,包括 C A Hoare (尽管他显然否认了这一点)。

        3
  •  6
  •   wilhelmtell    14 年前

    你朋友的问题不在于他的想法,而在于他的宗教信仰。对于任何函数,总是考虑按值、引用、常量引用、指针或智能指针传递的优缺点。然后决定。

    我在这里看到的唯一破碎设计的迹象就是你朋友的盲目信仰。

    也就是说,有一些签名并没有带来什么好处。按值取常量可能是愚蠢的,因为如果你保证不改变对象,那么你最好不要自己复制它。当然,除非它是一个原语,在这种情况下,编译器可以足够聪明地获取引用。或者,有时把指向指针的指针作为参数是很笨拙的。这增加了复杂性;相反,通过引用一个指针,您可能能够摆脱它,并获得相同的效果。

    但不要把这些指导方针视为一成不变的;要始终考虑你的选择,因为没有正式的证据可以消除任何选择的有用性。

    1. 如果您需要根据自己的需要更改参数,但不想影响客户机,则按值接受该参数。
    2. 如果您希望向客户机提供服务,而客户机与服务不密切相关,那么考虑引用一个参数。
    3. 如果客户机与服务密切相关,那么考虑不采用参数,而是编写成员函数。
    4. 如果您希望为与服务密切相关但彼此非常不同的客户系列编写一个服务函数,那么请考虑引用参数,并可能使该函数成为需要这种友谊的客户的朋友。
    5. 如果您根本不需要更改客户机,那么考虑使用常量引用。
        4
  •  5
  •   Konrad Rudolph    14 年前

    原因是,他认为“通过参照传递物体是一个坏设计的标志”。

    尽管C++在纯技术方面是错误的,但是总是使用传递值对初学者来说是一个很好的近似。 许多的 比通过指针传递所有东西(或者甚至比通过引用传递所有东西)更好。这会使一些代码效率低下,但是,嘿!只要这不打扰你的朋友,不要被这种做法过度打扰。提醒他,总有一天他可能会想重新考虑。

    另一方面,这:

    有一些大的控制器对象,但没有比这更复杂的了。

    一个问题。你的朋友说的是坏的设计,然后所有的代码使用的是一些三维向量和大型控制结构? 是一个破碎的设计。好的代码通过使用数据结构实现模块化。似乎情况并非如此。

    _浼浼浼浼浼浼浼浼浼浼浼

        5
  •  5
  •   Matthieu M.    14 年前

    我认为这个问题本身存在一个巨大的误解。

    一方面,堆栈或堆分配对象与另一方面的传递值、引用或指针之间没有关系。

    堆栈与堆分配

    如果可能的话,总是更喜欢堆栈,对象的生命周期随后会为您管理,这更容易处理。

    但是,在以下几种情况下可能无法实现:

    • 虚拟建筑(想想工厂)
    • 共享所有权(尽管你应该尽量避免)

    我可能会错过一些,但在本例中,您应该使用SBRM(范围绑定资源管理)来利用堆栈的生存期管理功能,例如使用智能指针。

    传递:值、引用、指针

    首先,不同的是 语义 :

    • 值,常量引用:传递的对象将不被该方法修改
    • 引用:传递的对象可能被该方法修改
    • 指针/常量指针:与引用(对于行为)相同,但可能为空

    请注意,默认情况下,某些语言(像haskell这样的函数类型)不提供引用/指针。一旦创建,这些值就不可变。除了一些处理外部环境的工作区之外,它们并没有受到这种使用的限制,而且它以某种方式使调试更容易。

    你的朋友应该知道传递引用或传递指针绝对没有任何错误:例如 swap ,它不能用pass-by值实现。

    最后,多态性不允许传递值语义。

    现在,让我们谈谈表演。

    通常很好地接受内建应该按值传递(以避免间接传递)和用户定义 大的 类应该通过引用/指针传递(以避免复制)。 大的 实际上,通常意味着复制构造函数并不简单。

    然而,关于 小的 用户定义的类。最近发表的一些文章建议,在某些情况下,传递值可能允许从编译器进行更好的优化,例如,在这种情况下:

    Object foo(Object d) { d.bar(); return d; }
    
    int main(int argc, char* argv[])
    {
      Object o;
      o = foo(o);
      return 0;
    }
    

    在这里,智能编译器可以确定 o 可以在不复制的情况下就地修改!(函数定义必须是可见的,我想,我不知道链接时间优化是否能解决这个问题)

    因此,性能问题只有一种可能,就像往常一样: 测量 .

        6
  •  3
  •   tia    14 年前

    首先,除了递归情况外,堆栈很少溢出此网站。

    关于他的推理,我认为他可能是错的,因为他太笼统了,但是 他所做的 可能是正确的…或不是?

    例如,Windows窗体库使用 Rectangle 结构有4个成员,苹果的Quartzcore也有 CGRect 结构,这些结构总是 按值传递 . 我想我们可以把它与向量和3个浮点变量进行比较。

    但是,由于我看不到代码,我觉得我不应该评判他做了什么,尽管我觉得他可能做了正确的事情,尽管他的想法过于笼统。

        7
  •  3
  •   Stack Overflow is garbage    14 年前

    我认为对于向量这样的小对象来说,传递值是可以的,但是即使这样,代码中也会出现大量不必要的复制。按值传递大型对象显然是浪费的,而且很可能不是您在功能上想要的。

    这并不像你想象的那么明显。C++编译器非常积极地执行复制删除操作,因此您通常可以通过值而不会导致复制操作的成本。在某些情况下,通过价值传递 might even be faster .

    在出于性能原因谴责这个问题之前,您至少应该生成支持它的基准。它们可能很难被创造出来 因为 编译器通常消除性能差异。

    所以真正的问题应该是语义学。您希望代码的行为如何?有时,引用语义是您想要的,然后您应该通过引用传递。如果您特别想要/需要值语义,那么就通过值传递。

    有一点有利于传递值。它有助于实现更具功能性的代码风格,副作用更少,并且在默认情况下不可变。这使得许多代码更容易解释,而且也可能使代码并行化更容易。

    但事实上,两者都有自己的位置。和 从未 使用旁路参考绝对是一个大的警告标志。

    在过去6个月左右的时间里,我一直在尝试将旁路值作为默认值。如果我不显式地需要引用语义,那么我尝试假定编译器将为我执行复制省略,这样我就可以通过值传递而不损失任何效率。

    到目前为止,编译器还没有让我失望。我肯定我会遇到一些情况,我必须回去把一些电话改为通过引用,但我知道后会这样做的

    • 性能是个问题,而且
    • 编译器未能应用副本删除
        8
  •  3
  •   karlphillip    14 年前

    我会这么说的 在C语言中不使用指针是新手程序员的标志。

    听起来你的朋友害怕指点。

    记住,C++指针实际上是从C语言继承的,而C是在计算机不那么强大的时候开发出来的。然而,直到今天,速度和效率仍然至关重要。

    那么,为什么要使用指针呢? 它们允许开发人员优化一个程序,使其运行速度更快,或者使用更少的内存,否则的话!引用数据的内存位置比复制周围的所有数据更有效。

    指针通常是一个概念,对于那些开始编程的人来说很难理解,因为所有的实验都涉及到小的数组,也许是一些结构,但基本上,当你有1GB的内存在家里的时候,它们包括使用几兆字节(如果你幸运的话)。在这个场景中,几个MB什么都不是,通常对程序的性能影响很小。

    让我们稍微夸大一下。想到一个 字符数组 对于2147483648个元素(2GB的数据),您需要传递给函数,函数将所有数据写入磁盘。现在,你认为什么技术更有效/更快?

    • 传递值,在程序将数据写入磁盘之前,必须将这些2GB数据重新复制到内存中的另一个位置,或者
    • 传递引用,它将只引用该内存位置。

    当你没有4GB内存时会发生什么?你会不会仅仅因为害怕使用指针而花美元购买RAM芯片?

    在不必复制的情况下,重新复制内存中的数据听起来有点多余,这浪费了计算机资源。

    不管怎样,对你的朋友要有耐心。如果他想在一生中的某个时刻成为一个严肃的/专业的程序员,他最终将不得不花时间真正理解指针。

    祝你好运。

        9
  •  2
  •   skimobear    14 年前

    正如前面提到的,引用和指针之间的最大区别在于指针可以为空。如果类需要数据,引用声明将使其成为必需的。如果调用方希望这样做,则添加const将使其“只读”。

    提到的传递值“缺陷”只是不正确。按值传递所有内容将完全改变应用程序的性能。当基元类型(如int、double等)通过值传递时,情况也不错,但当类实例通过值传递时,会创建临时对象,这要求对类和类中的所有成员变量调用构造函数和析构函数。当使用大型类层次结构时,这会很恼火,因为必须同时调用父类构造函数/析构函数。

    而且,仅仅因为向量是通过值传递的,并不意味着它只使用堆栈内存。堆可以用于每个元素,因为它是在传递给方法/函数的临时向量中创建的。如果向量达到其容量,它本身也可能需要通过堆重新分配。

    如果传递值的作用是不修改调用方的值,那么只需使用常量引用。

        10
  •  2
  •   Andrew    14 年前

    到目前为止,我看到的答案都集中在性能上:引用传递比值传递更快的情况。如果你专注于那些不可能通过价值传递的案例,你的论点可能会更成功。

    小元组或向量是一种非常简单的数据结构类型。更复杂的数据结构共享信息,而共享不能直接表示为值。您要么需要使用引用/指针,要么使用模拟它们的东西,如数组和索引。

    许多问题归结为形成一个图或有向图的数据。在这两种情况下,都有需要存储在数据结构中的边和节点的混合。现在您有一个问题,相同的数据需要在多个地方。如果避免引用,那么首先需要复制数据,然后在其他副本中仔细复制每个更改。

    你朋友的论点归结为:解决任何复杂到可以用图表表示的问题都是一个糟糕的设计……

        11
  •  2
  •   Bob Murphy    14 年前

    我唯一能想到的主要论点 那堆可能 溢出,但我猜是 这不可能发生?是 还有其他反对意见 仅使用堆栈/传递值作为 反对通过引用?

    哦,天哪,从哪里开始……

    1. 正如您所提到的,“代码中发生了许多不必要的复制”。假设您有一个循环,您在其中调用这些对象的函数。使用指针而不是复制对象可以将执行速度提高一个或多个数量级。

    2. 不能在堆栈上传递可变大小的数据结构、数组等。您必须动态地分配它,并向开头传递指针或引用。如果你的朋友没有遇到这个问题,那么,他是“C++新手”。

    3. 正如您所提到的,所讨论的程序很简单,并且主要使用非常小的对象,如图形3-元组,如果元素是双精度的,那么每个元素将是24字节。但在图形技术中,处理4x4阵列是很常见的,它处理旋转和平移。这将是每个128字节,因此,如果一个必须处理这些字节的程序在每个函数调用时由于复制量的增加而具有传递值,则速度会慢5倍。通过pass-by引用,在32位可执行文件中传递3元组或4x4数组只需要复制一个4字节指针。

    4. 在寄存器上,诸如arm、powerpc、64位x86、680x0-但不是32位x86-指针(和引用,这些引用秘密地是穿着花哨语法服装的指针)通常在寄存器中传递或返回,与堆栈操作中涉及的内存访问相比,这真的非常快。

    5. 您提到了堆栈空间不足的可能性。是的,在一个小程序上,你可以为一个课堂作业而写。但几个月前,我调试的商业代码可能是main()下面的80个函数调用。如果使用pass-by值而不是pass-by引用,那么堆栈将是巨大的。为了避免你的朋友认为这是一个“坏设计”,这实际上是一个基于webkit的浏览器,使用gtk+在Linux上实现,所有这些都是最先进的,并且对于专业代码来说函数调用深度是正常的。

    6. 一些可执行的体系结构限制了单个堆栈帧的大小,因此,即使您本身没有耗尽堆栈空间,您也可以超过它,并以完全不存在于这样一个平台上的C++代码结束。

    我可以继续说下去。

    如果你的朋友对图形感兴趣,他应该看看图形中常用的一些API:Linux上的OpenGL和XWindows,Mac OS X上的Quartz,Windows上的Direct X。他应该看看大型C/C++系统的内部结构,比如WebKIT或GECKO HTML渲染引擎,或者任何Mozilla浏览器,或者GTK+或QT-GUI工具包。它们都通过任何比单个整数或引用浮点大得多的值传递,并且通常通过引用而不是作为函数返回值来填充结果。

    没有任何严重的现实世界C/C++CHOPS -我的意思是 没有人 -按值传递数据结构。这是有原因的:它只是翻转效率低下和容易出现问题。

        12
  •  1
  •   Potatoswatter    14 年前

    哇,已经有13个答案了我没有详细阅读,但我认为这和其他答案有很大的不同。

    他说得有道理。按规则传递值的优点是子例程不能细微地修改其参数。经过 非常量 参考文献表明,每个函数都有难看的副作用,表明设计不好。

    简单地向他解释 vector3 & vector3 const& ,并演示如何用常量初始化后者,如 vec_function( vector3(1,2,3) ); 但不是前者。传递常量引用是传递值的简单优化。

        13
  •  0
  •   Paul Michalik    14 年前

    给你的朋友买一本好的C++书籍。通过引用传递非平凡的对象是一个很好的实践,可以节省大量不必要的构造函数/析构函数调用。这也与在自由存储上分配和使用堆栈无关。您可以(或者应该)通过引用传递程序堆栈上分配的对象,而不需要使用任何自由存储。你也可以完全忽略免费商店,但这会让你回到你的朋友可能没有想到的旧Fortran时代-否则他会为你的项目选择一个古老的F77编译器,不是吗…?