为什么对象通过引用传递?


31

一个正在研究面向对象的年轻同事问我为什么每个对象都通过引用传递,这与原始类型或结构相反。它是Java和C#等语言的共同特征。

我找不到适合他的答案。

此设计决定的动机是什么?这些语言的开发人员是否厌倦了每次都必须创建指针和typedef的麻烦?


2
您是否在问Java和C#为什么要通过引用而不是通过值或通过引用而不是通过指针传递参数?
罗伯特

@Robert,“引用而不是通过指针”之间在高层有什么区别?您认为我应该将标题更改为“为什么总是引用对象吗?”?
Gustavo Cardoso

引用是指针。
compman 2011年

2
@Anto:Java引用在所有方面都与正确使用的C指针完全相同(正确使用:未进行类型转换,未设置为无效内存,未由文字设置)。
Zan Lynx

5
同样要真正学究的是,标题也不正确(至少就.net而言)。对象不按引用传递,引用按值传递。将对象传递给方法时,参考值将复制到方法主体中的新参考。我觉得很遗憾,“对象通过引用传递”在不正确的情况下进入了程序员常用引号的列表,并导致初学者对参考文献的理解较差。
SecretDeveloper

Answers:


15

基本原因可以归结为:

  • 指针是正确的技术
  • 您需要指针来实现某些数据结构
  • 您需要指针以提高内存使用效率
  • 如果您不直接使用硬件,不需要手动进行内存索引。

因此,参考。


32

简单答案:

在重新创建并对传递到某处的每个对象进行深层复制时,最大程度地减少内存消耗

CPU时间。


我同意你的观点,但我认为,在这些方面还存在一些美学或OO设计动机。
Gustavo Cardoso

9
@Gustavo Cardoso:“一些美学或OO设计动机”。不,这只是一种优化。
S.Lott

6
@ S.Lott:不,用OO术语来引用是有意义的,因为从语义上讲,您不想复制对象。您想传递对象而不是它的副本。如果您按值传递,它会在某种程度上打破OO隐喻,因为您已经在所有地方生成了所有这些对象克隆,这些克隆在更高层次上没有任何意义。
直觉

@古斯塔沃:我认为我们在争论同一点。您提到了OOP的语义,并提到OOP的隐喻是我自己的其他原因。在我看来,OOP的创建者采用了“最大限度地减少内存消耗”和“节省CPU时间”的方式
蒂姆

14

在C ++中,有两个主要选项:按值返回或按指针返回。让我们看第一个:

MyClass getNewObject() {
    MyClass newObj;
    return newObj;
}

假设您的编译器不够聪明,无法使用返回值优化,那么这是怎么回事:

  • newObj被构造为一个临时对象,并放置在本地堆栈上。
  • 创建并返回newObj的副本。

我们毫无意义地制作了对象的副本。这浪费了处理时间。

让我们看一下按指针返回:

MyClass* getNewObject() {
    MyClass newObj = new MyClass();
    return newObj;
}

我们已经消除了冗余副本,但是现在又引入了另一个问题:我们在堆上创建了一个不会自动销毁的对象。我们必须自己处理:

MyClass someObj = getNewObject();
delete someObj;

知道谁负责删除以这种方式分配的对象是只能通过注释或约定来传达的信息。它很容易导致内存泄漏。

建议了许多解决方法来解决这两个问题-返回值优化(在这种情况下,编译器足够聪明,不能在按值返回的情况下创建冗余副本),将引用传递给方法(因此函数会注入到现有对象,而不是创建一个新对象),智能指针(这样,所有权问题就没有意义了)。

Java / C#创建者意识到,始终通过引用返回对象是一个更好的解决方案,特别是如果该语言本身支持该对象。它与语言所具有的许多其他功能相关联,例如垃圾收集等。


按值返回已经足够糟糕,但是当涉及对象时,按值传递甚至更糟,我认为这是他们试图避免的真正问题。
梅森惠勒

确保您有一个正确的观点。但是@Mason指出的OO设计问题是更改的最终动机。当您只想使用引用时,保持引用和值之间的差异没有任何意义。
Gustavo Cardoso

10

许多其他答案都有很好的信息。我想补充一点关于克隆的要点的但这仅部分得到解决。

使用引用很聪明。复制东西很危险。

正如其他人所说,在Java中,没有自然的“克隆”。 这不仅仅是缺少的功能。永远都不想只随意地复制对象中的每个属性(无论是浅层还是深层)。如果该属性是数据库连接怎么办?您不仅可以克隆一个数据库连接,还可以克隆一个人。 存在初始化是有原因的。

深拷贝是他们自己的问题-您真正走了多深?您绝对不能复制任何静态的内容(包括任何Class对象)。

因此,出于同样的原因,也就没有自然的克隆,作为副本传递的对象会造成精神错乱。即使您可以“克隆”数据库连接-现在如何确保它已关闭?


*请参阅注释-通过此“从不”语句,我的意思是自动克隆每个属性。Java没有提供Java语言,对于您作为使用该语言的用户来说,创建Java语言可能不是一个好主意。仅克隆非瞬态字段将是一个开始,但是即使那样,您也需要认真地定义transient适当的位置。


我很难理解在某些情况下从良好反对到克隆的转变,再到不再需要这种说法的问题。而且我遇到了需要精确复制的情况,其中没有涉及静态功能,没有IO或开放连接的问题……我了解克隆的风险,但是我永远也看不到。
印加

2
@Inca-您可能会误解我。故意实施clone是可以的。“故意”意味着复制所有属性而不考虑它-没有故意的意图。Java语言设计人员通过要求用户创建的实现来强制实现这一意图clone
妮可(Nicole)

使用对不可变对象的引用很聪明。使诸如Date之类的简单值可变,然后创建对其的多个引用不是。
凯文·克莱恩

@NickC:“克隆事物的意愿”很危险的主要原因是Java / .net之类的语言/框架没有任何方式来声明性地指出引用是否封装了可变状态和身份,或者两者都封装。如果field包含封装可变状态但不包含身份的对象引用,则克隆该对象需要复制持有该状态的对象,并将对该副本的引用存储在字段中。如果引用封装身份但不封装可变状态,则副本中的字段必须引用与原始对象相同的对象。
2012年

深度复制方面很重要。当对象包含对其他对象的引用时,复制对象是有问题的,尤其是在对象图包含可变对象的情况下。
卡雷布(Caleb)

4

对象始终以Java引用。他们永远不会绕过自己。

一个优点是,这简化了语言。C ++对象可以表示为值或引用,这就需要使用两个不同的运算符来访问成员: .->。(有一些原因不能将其合并;例如,智能指针是作为引用的值,并且必须使其与众不同。)Java仅需要.

另一个原因是多态性必须通过引用而不是值来完成;按值处理的对象就在那里,并且具有固定的类型。可以用C ++搞定。

而且,Java可以切换默认的分配/复制/任何内容。在C ++中,它是一个或多或少的深层副本,而在Java中,它是一个简单的指针赋值/复制/任何操作,并带有.clone()诸如此类,以防您需要复制。


有时候,当您使用'(* object)->'时,它会变得非常丑陋
Gustavo Cardoso

1
值得注意的是,C ++区分指针,引用和值。SomeClass *是指向对象的指针。SomeClass&是对对象的引用。SomeClass是一个值类型。
蚂蚁

我已经在最初的问题上问过@Rober,但是我也会在这里做:*和&在C ++上的区别只是一个低级的技术问题,不是吗?从高层次上讲,它们在语义上是否相同?
古斯塔沃·卡多佐

3
@Gustavo Cardoso:区别在于语义;在较低的技术水平上,它们通常是相同的。指针指向一个对象,或者为NULL(已定义的错误值)。除非const,否则可以将其值更改为指向其他对象。引用是对象的另一个名称,不能为NULL,也不能重新放置。它通常是通过简单使用指针来实现的,但这是实现的细节。
David Thornley

+1表示“多态性必须通过引用来完成”。这是一个非常关键的细节,大多数其他答案都忽略了。
2013年

4

关于通过引用传递的C#对象的初始声明不正确。在C#中,对象是引用类型,但是默认情况下,它们像值类型一样通过值传递。对于引用类型,作为值传递方法参数复制的“值”就是引用本身,因此对方法内部属性的更改将反映在方法范围之外。

但是,如果要在方法内部重新分配参数变量本身,则会看到此更改未反映在方法范围之外。相反,如果实际上使用ref关键字通过引用传递参数,则此行为可以按预期进行。


3

快速回答

Java和类似语言的设计人员希望应用“一切都是对象”的概念。而且,将数据作为参考传递非常快,并且不会占用太多内存。

额外的无聊评论

通俗地说,这些语言使用对象引用(Java,Delphi,C#,VB.NET,Vala,Scala,PHP),事实是对象引用是伪装成对象的指针。空值,内存分配,引用的副本而不复制对象的全部数据,所有这些都是对象指针,而不是普通对象!

在对象Pascal(不是Delphi)和c C ++(不是Java,不是C#)中,可以通过使用指针将对象声明为静态分配的变量,也可以将其声明为动态分配的变量(“糖语法”)。每个案例都使用特定的语法,并且没有像Java“和朋友”中那样令人困惑的方法。在这些语言中,对象既可以作为值也可以作为引用传递。

程序员知道什么时候需要指针语法,什么时候不需要指针语法,但是在Java和类似语言中,这很令人困惑。

在Java出现或成为主流之前,许多程序员在C ++中没有指针地学习了面向对象,而是在需要时通过值或引用进行传递。从学习切换到业务应用程序时,它们通常使用对象指针。QT库就是一个很好的例子。

当我学习Java时,我试图遵循一切都是对象的概念,但是对编码感到困惑。最终,我说:“好,这是一个使用静态分配的对象的语法动态分配了指针的对象”,并且再次编写代码没有麻烦。


3

Java和C#确实可以控制您的低级内存。您创建的对象所在的“堆”拥有自己的生活;例如,垃圾收集器会根据需要收获对象。

由于您的程序和该“堆”之间存在单独的间接层,因此通过值和指针(如C ++)引用对象的两种方式变得难以区分。:您始终通过“指针”引用对象。在堆的某个地方。这就是为什么这种设计方法将按引用传递作为赋值的默认语义的原因。Java,C#,Ruby等。

以上仅涉及命令式语言。在对内存的控制上面提到的语言传递给运行,但语言的设计也说:“哎,但实际上,还有就是内存,并且有的对象,他们占用内存”。通过从其定义中排除“内存”的概念,功能语言甚至可以进一步抽象。这就是为什么按引用传递不一定适用于您不控制低级内存的所有语言的原因。


2

我可以想到一些原因:

  • 复制原始类型很简单,它通常转换为一条机器指令。

  • 复制对象并非易事,对象可以包含对象本身。复制对象在CPU时间和内存上都很昂贵。根据上下文,甚至有多种复制对象的方法。

  • 通过引用传递对象很便宜,并且当您要在对象的多个客户端之间共享/更新对象信息时,也很方便。

  • 复杂的数据结构(尤其是递归的数据结构)需要指针。通过引用传递对象只是传递指针的一种更安全的方法。


1

因为否则,该函数应该能够自动创建传递给它的任何类型的对象的(显然很深的)副本。通常情况下,它无法猜测出来。因此,您将必须为所有对象/类定义复制/克隆方法的实现。


它可以做一个浅表复制并保留实际值和指向其他对象的指针吗?
Gustavo Cardoso

#Gustavo Cardoso,然后您可以通过此对象修改其他对象,这是您希望没有通过引用传递给对象的对象吗?
大卫

0

因为Java被设计为更好的C ++,而C#被设计为更好的Java,并且这些语言的开发人员对根本破坏了C ++对象模型(对象是值类型)感到厌倦。

面向对象编程的三个基本原理中的两个是继承和多态性,将对象视为值类型而不是引用类型会给这两者造成严重破坏。当您将对象作为参数传递给函数时,编译器需要知道要传递多少字节。当您的对象是引用类型时,答案很简单:指针的大小,所有对象都相同。但是,当您的对象是值类型时,它必须传递值的实际大小。由于派生类可以添加新字段,因此这意味着sizeof(派生)!= sizeof(基数),多态性超出了人们的视野。

这是一个演示问题的普通C ++程序:

#include <iostream> 
class Parent 
{ 
public: 
   int a;
   int b;
   int c;
   Parent(int ia, int ib, int ic) { 
      a = ia; b = ib; c = ic;
   };
   virtual void doSomething(void) { 
      std::cout << "Parent doSomething" << std::endl;
   }
};

class Child : public Parent {
public:
   int d;
   int e;
   Child(int id, int ie) : Parent(1,2,3) { 
      d = id; e = ie;
   };
   virtual void doSomething(void) {
      std::cout << "Child doSomething : D = " << d << std::endl;
   }
};

void foo(Parent a) {
   a.doSomething();
}

int main(void)
{
   Child c(4, 5);
   foo(c);
   return 0;
}

该程序的输出不同于任何理智的OO语言中的等效程序的输出,因为您无法将按值派生的对象传递给需要基础对象的函数,因此编译器将创建一个隐藏的副本构造函数并传递Child对象的Parent部分的副本,而不是像您告诉的那样传递Child对象。像这样的隐藏语义陷阱就是为什么在C ++中应避免按值传递对象,而在几乎所有其他OO语言中根本不可能实现。


很好的一点。但是,我专注于退货问题,因为要解决这些问题需要花费很多精力。可以通过添加一个&符号来修复该程序:void foo(Parent&a)
蚂蚁

OO真的没有指针就无法正常工作
Gustavo Cardoso

-1.000000000000
P Shved

5
重要的是要记住,Java是按值传递的(它按值传递对象引用,而基元仅按值传递)。
妮可(Nicole)

3
@Pavel Shved-“两个胜过一个!” 或者,换句话说,更多的绳索可以挂在自己身上。
妮可(Nicole)

0

因为否则就不会有多态性。

在OO编程中,您可以Derived从一个类中创建一个更大的类Base,然后将其传递给期望一个类的函数Base。太琐碎了吧?

函数的参数大小是固定的,并且在编译时确定。您可以争论所有您想要的,可执行代码是这样,并且语言需要在某一点或另一点执行(纯解释语言不受此限制...)

现在,计算机上已经定义了一个数据:存储单元的地址,通常表示为一个或两个“字”。它可以作为指针或引用在编程语言中看到。

因此,为了传递任意长度的对象,最简单的方法是将指针/引用传递给该对象。

这是OO编程的技术限制。

但是由于对于大型类型,您通常还是喜欢传递引用以避免复制,因此通常不认为这是一个重大打击:)

但是,在Java或C#中,有一个重要的后果,就是将对象传递给方法时,您不知道对象是否会被该方法修改。这使得调试/并行化变得更加困难,这就是功能语言和透明引用正在尝试解决的问题->复制并不是那么糟糕(在有意义的时候)。


-1

答案是名字(几乎总是如此)。引用(如地址)只是引用其他内容,值是其他内容的另一个副本。我确定有人可能提到了以下内容,但在某些情况下,其中一种是不适用的(内存安全性与内存效率)。这一切都是关于管理内存,内存,内存……内存!:D


-2

好吧,所以我并不是要说这就是为什么对象是引用类型或通过引用传递的原因,但是我可以举一个例子说明为什么从长远来看这是一个非常好的主意。

如果我没记错的话,当您在C ++中继承一个类时,该类的所有方法和属性实际上都会复制到子类中。就像在子类中再次编写该类的内容一样。

因此,这意味着子类中数据的总大小是父类和派生类中内容的组合。

EG:#include

class Top 
{   
    int arrTop[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Middle : Top 
{   
    int arrMiddle[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Bottom : Middle
{   
    int arrBottom[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

int main()
{   
    using namespace std;

    int arr[20];
    cout << "Size of array of 20 ints: " << sizeof(arr) << endl;

    Top top;
    Middle middle;
    Bottom bottom;

    cout << "Size of Top Class: " << sizeof(top) << endl;
    cout << "Size of middle Class: " << sizeof(middle) << endl;
    cout << "Size of bottom Class: " << sizeof(bottom) << endl;

}   

这将向您显示:

Size of array of 20 ints: 80
Size of Top Class: 80
Size of middle Class: 160
Size of bottom Class: 240

这意味着,如果您具有多个类的大型层次结构,则此处声明的对象的总大小将是所有这些类的组合。显然,这些对象在很多情况下会很大。

我认为,解决方案是在堆上创建它并使用指针。从某种意义上讲,这意味着具有几个父类的类的对象大小是可以控制的。

这就是为什么使用引用将是更可取的方法。


1
对于13个先前的答案所提出和解释的观点,这似乎没有提供任何实质性的内容
gnat 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.