17

假设我认识一个 C++ 新手。他不传递指针(正确地如此),但他拒绝通过引用传递。他总是使用传递值。原因是他认为“通过引用传递对象是设计失败的标志”。

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

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

我认为按值传递对于向量等小对象来说很好,但即便如此,代码中也会发生很多不必要的复制。按值传递大对象显然是浪费的,而且很可能不是您想要的功能。

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

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

4

13 回答 13

14

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

于 2010-09-12T15:02:05.603 回答
7

你朋友的问题与其说是他的想法不如说是他的宗教。给定任何函数,始终考虑按值、引用、常量引用、指针或智能指针传递的优缺点。然后决定。

我在这里看到的唯一破坏设计的迹象是你朋友的盲目宗教。

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

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

  1. 如果您需要根据自己的需要更改参数,但又不想影响客户端,则按值取参数。
  2. 如果您想向客户端提供服务,并且客户端与服务没有密切关系,则考虑通过引用进行参数。
  3. 如果客户端与服务密切相关,则考虑不带参数但编写成员函数。
  4. 如果您希望为与服务密切相关但彼此非常不同的客户家族编写服务函数,请考虑采用参考参数,并可能使该函数成为需要这种友谊的客户的朋友。
  5. 如果您根本不需要更改客户端,请考虑使用 const-reference。
于 2010-09-12T15:07:23.570 回答
6

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

是的,对于没有动态分配要求的小型对象,按值传递是可以的,但是在没有具体测量所谓的开销是(a)可感知和(b)显着的情况下说“无引用”仍然是愚蠢的. “过早的优化是万恶之源” 1

1 各种归因,包括CA Hoare(尽管他显然否认了这一点)。

于 2010-09-12T14:53:55.307 回答
5

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

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

堆栈与堆分配

尽可能选择堆栈,然后为您管理对象的生命周期,这更容易处理。

但在以下几种情况下可能是不可能的:

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

我可能会错过一些,但在这种情况下,您应该使用 SBRM(范围绑定资源管理)来利用堆栈生命周期管理功能,例如通过使用智能指针。

传递:值、引用、指针

首先,语义上有区别:

  • value, const reference:传递的对象不会被方法修改
  • 参考:传递的对象可能被方法修改
  • 指针/常量指针:与引用相同(对于行为),但可能为空

请注意,某些语言(像 Haskell 之类的函数式语言)默认不提供引用/指针。一旦创建,这些值就是不可变的。除了一些处理外部环境的变通方法之外,它们不受这种使用的限制,并且它以某种方式使调试更容易。

你的朋友应该知道,pass-by-reference 或 pass-by-pointer 绝对没有错:例如 thing of swap,它不能用 pass-by-value 来实现。

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

现在,让我们谈谈表演。

通常人们普遍认为内置函数应该通过值传递(以避免间接),而用户定义的大类应该通过引用/指针传递(以避免复制)。big实际上通常意味着复制构造函数不是微不足道的。

然而,关于小型用户定义的类有一个悬而未决的问题。最近发表的一些文章表明,在某些情况下,按值传递可能允许编译器进行更好的优化,例如,在这种情况下:

Object foo(Object d) { d.bar(); return d; }

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

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

因此,与往常一样,性能问题只有一种可能性:measure

于 2010-09-12T16:45:08.977 回答
4

原因是他认为“通过引用传递对象是设计失败的标志”。

尽管出于纯技术原因这在 C++ 中是错误的,但始终使用按值传递对于初学者来说是一个足够好的近似值——它肯定通过指针传递所有内容(甚至可能比通过引用传递所有内容)要好得多。它会使一些代码效率低下,但是,嘿!只要这不打扰你的朋友,就不要被这种做法过度打扰。只是提醒他,有一天他可能会重新考虑。

另一方面,这是:

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

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

……一旦你使用了这样的数据结构,没有引用传递的代码可能确实会变得非常低效。

于 2010-09-12T16:30:35.657 回答
3

第一件事是,堆栈很少溢出这个网站,除非在递归情况下。

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

例如,Windows Forms 库使用Rectanglestruct 有 4 个成员,Apple 的 QuartzCore 也有CGRectstruct,并且这些 struct 总是通过 value 传递。我认为我们可以将其与具有 3 个浮点变量的 Vector 进行比较。

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

于 2010-09-12T16:01:58.983 回答
3

我想说在 C 中不使用指针是新手程序员的标志。

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

请记住,C++ 指针实际上是从 C 语言继承而来的,而 C 是在计算机功能还不够强大的时候开发的。尽管如此,直到今天,速度和效率仍然至关重要。

那么,为什么要使用指针呢?它们允许开发人员优化程序以更快地运行或使用更少的内存!引用数据的内存位置比复制所有数据要高效得多。

对于刚开始编程的人来说,指针通常是一个难以掌握的概念,因为所做的所有实验都涉及小数组,可能是一些结构,但基本上它们由几兆字节(如果你幸运的话)组成,当你房子周围有 1GB 的内存。在这个场景中,几个 MB 什么都不是,而且它通常太少而不会对程序的性能产生重大影响。

所以让我们夸大一点。考虑一个包含 2147483648 个元素(2GB 数据)的char 数组,您需要将其传递给将所有数据写入磁盘的函数。现在,您认为哪种技术会更有效/更快?

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

当您没有 4GB 的 RAM 时会发生什么?你会因为害怕使用指针而花钱购买 RAM 芯片吗?

不必要时重新复制内存中的数据听起来有点多余,而且浪费了计算机资源。

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

祝你好运。

于 2010-09-12T14:52:19.840 回答
3

我认为按值传递对于向量等小对象来说很好,但即便如此,代码中也会发生很多不必要的复制。按值传递大对象显然是浪费的,而且很可能不是您想要的功能。

它并不像你想象的那么明显。C++ 编译器非常积极地执行复制省略,因此您通常可以按值传递而不会产生复制操作的成本。在某些情况下,按值传递甚至可能更快

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

所以真正的问题应该是语义之一。你希望你的代码表现如何?有时,引用语义是你想要的,然后你应该通过引用传递。如果您特别想要/需要值语义,那么您可以按值传递。

有一点赞成按价值传递。它有助于实现更具功能性的代码风格,副作用更少,并且默认情况下不变性。这使得很多代码更容易推理,也可以更容易并行化代码。

但事实上,两者都有自己的位置。并且从不使用传递引用绝对是一个很大的警告信号。

在过去 6 个月左右的时间里,我一直在尝试将 pass-by-value 作为默认值。如果我不明确需要引用语义,那么我会尝试假设编译器将为我执行复制省略,因此我可以按值传递而不会损失任何效率。

到目前为止,编译器并没有让我失望。我确信我会遇到必须返回并更改一些调用以通过引用传递的情况,但是当我知道时我会这样做

  • 性能是一个问题,并且
  • 编译器未能应用复制省略
于 2010-09-12T16:45:54.890 回答
2

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

提到的按值传递“缺陷”根本不是真的。按值传递所有内容将彻底改变应用程序的性能。当原始类型(即 int、double 等)按值传递时并没有那么糟糕,但是当类实例按值传递时,会创建临时对象,这需要构造函数,然后在类和所有对象上调用析构函数类中的成员变量。当使用大型类层次结构时,这很令人恼火,因为也必须调用父类的构造函数/析构函数。

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

如果按值传递是为了不修改调用者的值,那么只需使用 const 引用。

于 2010-09-12T14:59:13.677 回答
2

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

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

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

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

于 2010-09-12T16:34:43.553 回答
2

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

好吧,天哪,从哪里开始......

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

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

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

  4. 在诸如 ARM、PowerPC、64 位 x86、680x0(但不是 32 位 x86)之类的寄存器丰富的 CPU 架构上,指针(和引用,它们是穿着花哨的语法服装的秘密指针)通常在寄存器中传递或返回,这与堆栈操作中涉及的内存访问相比,这真是太快了。

  5. 您提到了堆栈空间用完的可能性。是的,对于一个可能为课堂作业编写的小程序就是如此。但是几个月前,我正在调试可能是 main() 之下的 80 个函数调用的商业代码。如果他们使用按值传递而不是按引用传递,堆栈将是巨大的。以免你的朋友认为这是一个“坏设计”,这实际上是一个使用 GTK+ 在 Linux 上实现的基于 WebKit 的浏览器,所有这些都是非常先进的,函数调用深度对于专业代码来说是正常的.

  6. 一些可执行体系结构限制了单个堆栈帧的大小,因此即使您本身可能不会用完堆栈空间,您也可能会超出这个范围,并最终得到不会在这样的平台上构建的完全有效的 C++ 代码。

我可以继续下去。

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

没有任何认真的现实世界 C/C++ 印章 - 我的意思是没有人- 按值传递数据结构。这是有原因的:它只是效率低下且容易出现问题。

于 2010-09-12T16:41:02.043 回答
1

哇,已经有13个答案了……我没有详细阅读,但我认为这与其他答案大不相同……</p>

他有一点。作为一个规则,按值传递的优点是子程序不能巧妙地修改它们的参数。传递非常量引用将表明每个函数都有丑陋的副作用,表明设计不佳。

简单地向他解释 和 之间的区别vector3 &vector3 const&并演示后者如何被初始化为常量vec_function( vector3(1,2,3) );,而不是前者。Pass by const reference 是对 pass by value 的简单优化。

于 2010-09-12T18:36:42.803 回答
0

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

于 2010-09-12T17:04:57.570 回答