“通过引用传递”究竟是什么意思?

时间:2011-11-13 18:46:03

标签: java parameters terminology pass-by-reference

谁有权决定?

编辑:显然我没有成功地完善我的问题 我询问Java的参数传递是如何工作的。我知道看起来像持有对象的变量实际上是一个包含对象引用的变量,并且该引用是按值传递的。 这里有很多很好的解释(在链接的线程和其他线程中)和其他地方。

问题是关于术语传递引用的技术含义。 (结束编辑)

我不确定这是否是SO的正确问题,如果没有道歉,但我不知道更好的地方。在这里的其他问题中已经说了很多,例如Is Java "pass-by-reference" or "pass-by-value"?pass by reference or pass by value?,但我没有找到一个权威的答案来回答这个术语的含义。

我认为“通过引用传递”意味着“将引用(通常是指针)传递给对象”,因此被调用者可以修改调用者看到的对象,而“按值传递”意味着复制对象,让被叫者对副本有乐趣(明显的问题:如果对象包含引用,深层复制或浅层,该怎么办) 唱FW出现lots of places saying“通过引用传递”意味着,here有一些论点,它意味着更多,但定义仍在阅读

  

一种ParameterPassing模式,其中将实际参数的引用(或者如果你想在政治上不正确,指针)传递给形式参数;当被调用者需要形式参数时,它会取消引用指针以获取它。

我没有找到很多地方给出更强的术语定义,在this页面上,我发现“形式参数的左值设置为实际参数的左值。”并且,如果我理解正确,则使用相同的定义here(“形式参数仅作为实际参数的别名。”

事实上,我发现使用更强定义的唯一地方是反对这样一种观点,即在Java中,对象是通过引用传递的(可能是由于我缺乏google-fu)。

所以,如果我把事情弄清楚,那就是传递参考

class Thing { ... }
void byReference(Thing object){ ... }
Thing something;
byReference(something);
根据第一个定义,

大致对应于(在C中)

struct RawThing { ... };
typedef RawThing *Thing;
void byReference(Thing object){
    // do something
}
// ...
struct RawThing whatever = blah();
Thing something = &whatever;
byReference(something); // pass whatever by reference
// we can change the value of what something (the reference to whatever) points to, but not
// where something points to

从这个意义上说,Java通过引用传递对象就足够了。但根据第二个定义,传递参考意味着或多或少

struct RawThing { ... };
typedef RawThing *RawThingPtr;
typedef RawThingPtr *Thing;
void byReference(Thing object){
    // do something
}
// ...
RawThing whatever = blah();
RawThingPtr thing_pointer = &whatever;
byReference(&thing_pointer); // pass whatever by reference
// now we can not only change the pointed-to (referred) value,
// but also where thing_pointer points to

由于Java只允许你有指针到对象(限制你可以用它们做什么),但没有指针指针,从这个意义上说,Java通过引用传递对象是完全错误的。

所以,

  1. 我是否已充分理解上述传递参考的定义?
  2. 是否有其他定义?
  3. 是否有共识,哪个定义是“正确的”,如果是,哪个?

7 个答案:

答案 0 :(得分:8)

当然,不同的人目前对“传递参考”的含义有不同的定义。这就是为什么他们不同意某些东西是否通过参考传递。

但是,无论您使用何种定义,都必须在语言中使用始终。你不能说一种语言具有传值,并且在另一种语言中具有完全相同的语义,并且说它是传递参考。指出语言之间的类比是解决这一争议的最佳方式,因为虽然人们可能对特定语言中的传递模式有强烈的意见,但当你将相同的语义与其他语言进行对比时,它有时会带来反直觉的结果,迫使他们重新思考他们的定义。

  • 一个主要观点是Java只是按值传递。 (在Internet上随处搜索,您将找到这种观点。)此视图是对象不是值,但始终通过引用进行操作,因此它是按值分配或传递的引用。这个视图认为传递引用的测试是否可以分配给调用范围中的变量。

如果有人同意这个观点,那么还必须考虑大多数语言,包括 Python,Ruby,OCaml,Scheme,Smalltalk,SML,Go,JavaScript,Objective-C 等多种语言,等等仅按值传递。如果其中任何一个让你感到奇怪或违反直觉,我挑战你指出为什么你认为在Java中的任何语言中任何语言中的对象的语义不同。 (我知道其中一些语言可能明确声称它们是通过引用传递;但它与它们所说的无关;必须根据实际行为对所有语言应用一致的定义。)

  • 如果您认为Java中的对象是传递引用,那么您还必须将C视为传递引用。

拿你的Java例子:

class Thing { int x; }
void func(Thing object){ object.x = 42; object = null; }
Thing something = null;
something = new Thing();
func(something);
在C中,它等同于:

typedef struct { int x; } Thing;
void func(Thing *object){ object->x = 42; object = NULL; }
Thing *something = NULL;
something = malloc(sizeof Thing);
memset(something, 0, sizeof(something));
func(something);
// later:
free(something);

我声称上述语义相同;只有语法不同。唯一的语法差异是:

  1. C需要明确的*来表示指针类型; Java的引用(指向对象的指针)类型不需要显式的*
  2. C使用->通过指针访问字段; Java只使用.
  3. Java使用new为堆上的新对象动态分配内存; C使用malloc来分配它,然后我们需要初始化内存。
  4. Java有垃圾收集
  5. 请注意,重要的是,

    1. 在这两种情况下,使用对象调用函数的语法都是相同的:func(something),无需执行任何操作,例如获取地址或任何内容。
    2. 在这两种情况下,对象都是动态分配的(它可能超出了函数的范围)。和
    3. 在这两种情况下,函数内的object = null;都不会影响调用范围。
    4. 因此在两种情况下语义都是相同的,所以如果你调用Java pass-by-reference,你也必须调用C pass-by-reference。

答案 1 :(得分:5)

两个C示例实际上都演示了pass-by-value,因为C没有pass-by-reference。只是你传递的值是一个指针。传递引用发生在诸如Perl之类的语言中:

sub set_to_one($)
{
    $_[0] = 1; # set argument = 1
}
my $a = 0;
set_to_one($a); # equivalent to: $a = 1

这里,变量$a实际上是通过引用传递的,因此子例程可以修改它。它不会修改$a通过间接指向的某个对象;相反,它会修改$a本身。

在这方面,Java就像C一样,只是在Java对象中是“引用类型”,所以你所拥有的(以及你所能传递的所有东西)都是指向它们的指针。像这样:

void setToOne(Integer i)
{
    i = 1; // set argument = 1
}

void foo()
{
    Integer a = 0;
    setToOne(a); // has no effect
}

实际上不会改变a;它只会重新分配i

答案 2 :(得分:5)

谁有权决定?没有人,也没有人。你自己决定;作家决定他或她的书;读者决定是否同意作者。

要理解这个术语,需要深入了解语言(并根据C代码解释它们而不是忽视这一点)。参数传递样式是指编译器通常用于创建特定行为的机制。通常定义以下内容:

  • 按值传递:在输入子例程时将参数复制到参数中
  • 传递结果:输入子程序时参数未定义,子程序返回时复制到参数
  • 传递value-result:将参数复制到entry处的参数中,并将参数复制到返回的参数中
  • 通过引用传递:对参数变量的引用被复制到参数中;对参数变量的任何访问都透明地转换为对参数变量
  • 的访问

(术语说明:参数是子程序中定义的变量,参数是调用中使用的表达式。)

教科书通常也会按名称定义传递,但这种情况很少见,也不容易解释。通过需要也存在。

参数传递样式的重要性在于它的作用:在传递值时,对参数所做的任何更改都不会传递给参数;在通过结果传递时,对参数所做的任何更改都会传递给最后的参数;在通过引用传递时,对参数所做的任何更改都会在创建时传递给参数。

某些语言定义了多种传递样式,允许程序员分别为每个参数选择首选样式。例如,在Pascal中,默认样式是按值传递,但程序员可以使用var关键字来指定按引用传递。其他一些语言指定一种传递方式。还有一些语言为不同类型指定不同的样式(例如,在C中,传递值是默认值,但数组是通过引用传递的。)

现在,在Java中,从技术上讲,我们有一种带有pass-by-value的语言,对象变量的值是对象的引用。是否使得Java通过引用传递对象变量是一个品味问题。

答案 3 :(得分:2)

通过引用传递实际上是将引用传递给值 - 而不是它的副本 - 作为参数。


  

我想在继续之前,应该定义某些事情。我可能会使用它们,而不是习惯于使用它们。

     
      
  • 对象是一个数据分子。它占用存储空间,可能包含其他对象,但有自己的标识,可以作为一个单元引用和使用。

  •   
  • 引用是对象的别名或句柄。在语言层面,参考文献的作用大多就像它所指的那样;根据语言的不同,编译器/解释器/运行时/ gnomes会在需要实际对象时自动取消引用

  •   
  • 是评估表达式的结果。它是一个具体的对象,可以存储,传递给函数等。(OOP不好,请注意我使用"对象"这里是通用"数据分子"感觉,而不是OOP"类的实例" sense。)

  •   
  • 变量是预先分配的的命名引用

         

    特别注意:变量不是值。尽管如此,变量通常不会改变。他们的正在发生变化。他们如此容易混淆,部分证明了参考< - >指示错觉通常有多好。

  •   
  • 引用类型变量(Java,C#,...)是一个变量,其值为引用

  •   

当您将变量作为参数传递时,大多数语言默认情况下会创建变量值的副本并传递副本。被调用者将其参数的名称绑定到该副本。这被称为"传递价值" (或者,更清楚地说,"通过副本")。调用两侧的两个变量最终会有不同的存储位置,因此是完全不同的变量(只是因为它们通常以相等的值开始相关)。

另一方面,

通过引用传递不会复制。相反,它传递变量本身(减去名称)。也就是说,它将引用传递给变量别名的相同值。 (这通常通过隐式地将指针传递给变量的存储来完成,但这只是一个实现细节;调用者和被调用者不必知道或关心它是如何发生的。)被调用者将其参数的名称绑定到该位置。最终结果是双方使用相同的存储位置(仅可能使用不同的名称)。因此,被调用者对其变量所做的任何更改也都是对调用者的变量进行的。例如,在面向对象语言的情况下,可以为变量赋予完全不同的值。

大多数语言(包括Java)本身不支持此功能。哦,他们喜欢他们这样做......但那是因为那些从来没有真正通过引用传递的人,往往不会这样做了解这样做与按值传递引用之间的细微差别。如果混淆了这些语言,则使用引用类型变量。 Java本身永远不会直接使用引用类型对象,但使用引用来处理这些对象。区别在于变量"包含"说对象。 reference-type变量的值是这样的引用(或者,有时候是一个特殊的引用值,意思是"没有")。当Java传递这样的引用时,虽然它不复制对象,但它仍然复制该值(即:函数获得的引用是变量引用的值的副本)。也就是说,它是传递引用,但是通过值传递 。这允许通过引用传递的大多数事物允许但不是所有


我能想到的最明显的测试是真正的传递参考支持,将是"交换测试"。本机支持通过引用传递的语言必须提供足够的支持来编写交换其参数值的函数swap。代码相当于:

swap (x, y):       <-- these should be declared as "reference to T"
  temp = x
  x = y
  y = temp

--

value1 = (any valid T)
value2 = (any other valid T)

a = value1
b = value2
swap(a, b)
assert (a == value2 and b == value1)
  1. 必须是可能的并且成功运行 - 对于任何允许复制和重新分配的类型T - 使用语言的赋值和严格相等运算符(包括T指定的任何重载);和
  2. 不得要求来电转换或&#34;包装&#34; args(例如:通过显式传递指针)。要求将args标记为通过引用传递是正常的。
  3. (显然,不具备可变变量的语言无法通过这种方式进行测试 - 但这很好,因为它们并不重要两者之间的巨大语义差异在于被调用者的调用者的变量是如何可修改的。当变量的值在任何情况下都不可修改时,差异只会变成实现细节或优化。)< / p>

    请注意,本答案中的大部分内容都是关于&#34;变量&#34;。许多语言(如C ++)也允许通过引用传递匿名值。机制是一样的;值占用存储空间,引用是它的别名。它不一定在呼叫者中有一个名字。

答案 4 :(得分:1)

Java不通过引用传递。您总是传递副本/按值。但是,如果您传递一个对象,那么您将获得该引用的副本。因此,您可以直接编辑对象,但是如果覆盖本地引用,则不会覆盖原始对象引用。

答案 5 :(得分:0)

通过引用传递参数意味着参数的指针嵌套比局部变量的指针嵌套更深。如果您有一个具有类类型的变量,则该变量是指向实际值的指针。基本类型的变量包含值本身。

现在,如果按值传递这些变量,则保持指针嵌套:对象引用保留指向对象的指针,原始变量保留值本身。

将变量作为引用传递意味着指针嵌套变得更深:您将指针传递给对象引用,以便您可以更改对象引用;或者您传递指向基元的指针,以便您可以更改其值。

这些定义在C#和Object Pascal中使用,它们都有关键字来通过引用传递变量。

回答你的问题:因为第一个例子中的最后一个变量 - whatever和第二个变量中的thing_pointer - 通过指针(&)传递给函数,两者都通过引用传递。

答案 6 :(得分:0)

如果您熟悉C,可能以下类比解释了Java的工作原理。这仅适用于类类型(而非基本类型)的对象。

在Java中,我们可以有一个变量并将其传递给函数:

void f(Object x)
{
  x.bar = 5;    // #1j
  x = new Foo;  // #2j
}

void main()
{
  Foo a;
  a.bar = 4;
  f(a);
  // now a.bar == 5
}

在C中,这看起来如下:

void f(struct Foo * p)
{
  p->bar = 5;                      // #1c
  p = malloc(sizeof(struct Foo));  // #2c
}

int main()
{
  struct Foo * w = malloc(sizeof(struct Foo));
  w->bar = 4;
  f(w);
  /* now w->bar = 5; */
}

在Java中,类类型的变量总是引用,它们在C中最忠实地映射到指针。但是在函数调用中,指针本身是通过复制传递的。 在#1j中访问指针,#1c修改原始变量,因此从这个意义上讲,你传递的是对变量的引用。但是,变量本身只是一个指针,它本身是通过复制传递的。所以当你为它分配别的东西时。与在#2j和#2c中一样,您只是在f的本地范围内重新绑定引用/指针的副本

表示相应示例中的原始变量aw

简而言之:一切都是引用,引用按值传递。

另一方面,在C中,我可以通过声明void v(struct Foo ** r);并调用f(&w)来实现真正的“通过引用传递”;这样我就可以在w内更改f

注1:对于像int这样的基本类型,情况并非如此,它们完全按值传递。

注2:C ++示例有点整洁,因为我可以通过引用传递指针(我不必说struct):void f(Foo * & r) { r = new Foo; }f(w);