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

Posted

技术标签:

【中文标题】“通过引用传递”究竟是啥意思?【英文标题】:What exactly does "pass by reference" mean?“通过引用传递”究竟是什么意思? 【发布时间】:2011-12-28 03:58:11 【问题描述】:

谁有权决定?

编辑:显然我没有成功地提出我的问题。 我不是询问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 通过引用传递对象是完全错误的。

所以,

    我是否充分理解了上述传递引用的定义? 还有其他定义吗? 是否已达成共识,哪个定义是“正确的”,如果是,是哪个?

【问题讨论】:

我主要不使用 Java,但我认为它不能通过引用传递任何东西,例如 VB.NET 的 ByRef 或 C# 的 ref。我想人们所说的“通过引用传递”的实际意思是“通过值传递引用类型”。 我认为您误解了第二个示例 - 实际上您通过引用传递的不是whatever,而是thing_pointer 据我所知:Java 使用按值传递。这意味着对于原始类型,值被复制到形式参数中。对于对象引用,这意味着:引用被复制到形式参数中,因此您将获得按引用传递的行为 也许最简洁的表述方式是“所有(类类型)变量都是引用,所有引用都按值传递”。你知道 C++ 吗?这将允许一个明确的答案。 看到这个答案:***.com/questions/7893492/… 【参考方案1】:

通过引用传递实际上是将对值的引用(而不是其副本)作为参数传递。


我想在我们继续之前,应该定义某些事情。我使用它们的方式可能与您习惯使用它们的方式不同。

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

reference 是对象的别名或句柄。在语言级别上,引用的行为大多类似于它所指的事物。根据语言的不同,编译器/解释器/运行时/gnomes 会在需要实际对象时自动取消引用它。

是对表达式求值的结果。它是一个具体的对象,可以存储,传递给函数等(OOP 专家,注意我在这里使用“对象”是指通用的“数据分子”,而不是 OOP“类的实例”。 )

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

特别注意:变量不是值。尽管有名称,但变量通常不会改变。他们的价值是变化的。它们如此容易混淆,部分证明了参考参考错觉通常有多好。

引用类型变量(如 Java、C#、...)是 变量,其值为 引用.


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

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

大多数语言(包括 Java)本身不支持此功能。哦,他们喜欢他们确实...但那是因为那些从未真正通过引用传递的人,通常不会理解做与做之间的细微差别所以按值传递引用。这些语言的混淆之处在于引用类型变量。 Java 本身从不直接使用引用类型的对象,而是使用对这些对象的引用。不同之处在于“包含”所述对象的变量。引用类型变量的值就是这样的引用(或者,有时是一个特殊的引用值,表示“无”)。当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)
    必须能够成功运行——对于允许复制和重新分配的任何类型 T——使用语言的赋值和严格相等运算符(包括 T 指定的任何重载);和 不得要求调用者转换或“包装”参数(例如:通过显式传递指针)。要求将参数标记为通过引用传递是可以的。

(显然,不能以这种方式测试没有可变变量的语言——但这很好,因为它们无关紧要。两者之间最大的语义差异是如何调用者的变量是被调用者可以修改的。当变量的值在任何情况下都不可修改时,差异就只是一个实现细节或优化。)

注意,这个答案中的大部分内容都是关于“变量”的。许多语言,如 C++,也允许通过引用传递匿名值。机制是一样的;该值占用存储空间,引用是它的别名。它只是在调用者中不一定有名字。

【讨论】:

对象引用和传递引用变量之间的一个主要区别是支持传递引用语义的语言可以将引用传递给临时变量,因为这些引用的接收者将被禁止持久化它们。因此,如果foo 传递barfoo 变量之一的引用,则该引用将在bar 退出其范围后立即消失,并且由于bar 保证在@987654328 之前退出其范围@ 确实,这确保了引用不能超过变量。相比之下…… ...Java 没有提供任何好的机制,通过该机制Foo 方法可以传递对Bar 的引用,而不必担心Bar 在某处存储引用的副本。 Foo 唯一能做的就是构造一个包装器对象,将Bar 传递给该包装器的引用,然后在Bar 退出后使包装器无效。这有点可行,但非常尴尬。 事情是,尽管如此鲁莽地这样做会触发 UB,但在 C++ 中,您实际上可以通过将引用存储在 std::reference_wrapper...或 squirrel 中来半轻松地持久化引用一个指向被引用变量的指针。 可以在 C 或 C++ 中执行未定义行为而无需编译器尖叫这一事实并不使其合法,也不赋予编译器或框架任何产生预期行为的义务。在 Java 中,如果将对 Thing 的引用传递给将其复制到某处的方法,则只要存在对它的任何引用,JVM 就需要确保该对象的持续存在。相比之下,如果 C++ 方法通过引用将变量传递给另一个方法,则编译器没有义务确保该变量在离开作用域后继续存在。 支持传递引用的不同语言和框架在实际防止方法不正确地持久化对传入变量的引用的长度上有所不同,但一个共同的方面是任何在超出范围后使用捕获的引用的代码都会自行承担风险。【参考方案2】:

当然,目前不同的人对“传递引用”的含义有不同的定义。这就是为什么他们在某些东西是否是通过引用的问题上存在分歧。

但是,无论您使用什么定义,您都必须一致地跨语言使用它。您不能说一种语言具有按值传递,并且在另一种语言中具有完全相同的语义并说它是按引用传递。指出语言之间的类比是解决这一争议的最好方法,因为尽管人们可能对特定语言的传递模式有强烈的看法,但当你将相同的语义与其他语言进行对比时,有时会带来反直觉的结果,迫使他们重新思考他们的定义。

一种主流观点是 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);

我声称以上在语义上是等价的;只有语法不同。唯一的语法差异是:

    C 需要显式的* 来表示指针类型; Java 的引用(指向对象的指针)类型不需要显式的*。 C 使用-&gt; 通过指针访问字段; Java 只使用. Java 使用new 为堆上的新对象动态分配内存; C使用malloc来分配它,然后我们需要初始化内存。 Java 具有垃圾收集功能

请注意,重要的是,

    在这两种情况下,使用对象调用函数的语法相同:func(something),无需执行任何操作,例如获取地址或其他任何操作。 在这两种情况下,对象都是动态分配的(它可能存在于函数范围之外)。并且 在这两种情况下,函数内的object = null; 都不会影响调用范围。

所以这两种情况下的语义是一样的,所以如果你调用Java pass-by-reference,你也必须调用C pass-by-reference。

【讨论】:

我几乎同意你的所有回答,尤其是大多数语言都是按值传递的说法;但我不同意标准 ML only 传递值的说法。我不认为有任何原则性的方法来区分应用于标准 ML 的按值传递和按引用传递语义:按值传递和按引用传递都与语言的语义一致,所以唯一的结论是,它同时是both 值传递 引用传递。 (OCaml 可能也是如此,但我对它不太熟悉,所以不会明确地说。) @ruakh:在我看来,ML 语言只是简单地按值传递,因为不可能分配给变量。它们具有可变的数据结构,例如 ref 和向量,但 Java 和 Python 也是如此。同样,如果您在 SML 参数传递方式中所做的一切在 Java 或 Python 中都以相同的方式工作,那么您必须将其称为与 Java 和 Python 相同的传递模式(无论是按值传递还是其他方式),为了一致性 是的,当然。我同意它们是按值传递的;我的观点是它们也是通过引用传递的。 (您说它们是按值传递。)它们具有与按值传递语言相同的传递模式——函数接收其参数的值——但也与传递引用语言相同的传递模式:函数接收其参数的真实别名。没有赋值意味着这两种模式是等价的。如果 SML 的新版本(奇怪地)有赋值,它的创建者将不得不决定要维护其中的哪些语义。 很好的讨论,我以前从没想过java的语义传递。 @ruakh:语义上 ML 是按值传递的。不同的编译器以不同的方式实现参数传递。例如,编译器可能不传递对象作为参数,而是传递它的一些字段;这既不是按值传递也不是按引用传递。【参考方案3】:

您的两个 C 示例实际上都演示了按值传递,因为 C 没有按引用传递。只是您传递的值是一个指针。引用传递发生在 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

【讨论】:

一个更好的例子是 Pascal,它同时具有按值传递和按引用传递。 除了政治正确之外,reference of Foopointer to foo 之间没有区别 - 毕竟这正是它在 c++ 和其他任何地方的实现方式...... @Voo:你有点困惑。在 C++ 之外,“对 ___ 的引用”和“指向 ___ 的指针”大致等价(不同的语言使用稍微可变的术语和稍微可变的语义),但 C++ 引用与 C++ 指针完全不同。显然,它们是使用指针实现的(它们可以被认为是一层薄薄的语法糖),但产生的语义却大不相同。除非你认为while-loops 和goto-statements 之间的区别在于“政治正确性”? @ruakh 好吧,Foo* ptr2Foo 无所不能,Foo&amp; ref2Foo 无所不能,反之亦然。因此,将引用视为带有一些琐碎语法糖的 foo 指针就可以了(除了少数纯粹主义者)。我们可能会为这些事物分配不同的语义值,但是在考虑后果或限制时,您可以将两者替换并仍然得出相同的结果。但这并不意味着我会用 C++ 中的引用替换指针。 @Voo:既然你说“请”,我会指出Foo * ptr2Foo = NULL; 是合法的,而Foo &amp; ref2Foo = *NULL; 会触发未定义的行为。但实际上,您的评论毫无意义。是的,您可以重写使用引用的代码,并将其更改为使用指针;是的,这本质上就是 C++ 编译器在内部做的事情;但是不,这并不意味着“按引用传递”和“按值传递,值是指针/引用”是同义词。函数和调用者的语法和语义都不同。【参考方案4】:

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

【讨论】:

【参考方案5】:

***对引用调用给出了非常明确的定义,我无法改进:

在引用调用评估(也称为引用传递)中,函数接收对用作参数的变量的隐式引用,而不是其值的副本。这通常意味着该函数可以修改(即分配给)用作参数的变量——调用者将看到的东西。

请注意,您的示例都不是按引用调用的,因为在 C 中分配形式参数永远不会修改调用者看到的参数。

但复制粘贴就够了,请阅读详细讨论(附示例)

http://en.wikipedia.org/wiki/Evaluation_strategy#Call_by_reference

【讨论】:

【参考方案6】:

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

要理解这个术语,需要深入了解语言的底层(并且用 C 代码来解释它们,而不是抓住重点)。参数传递样式是指编译器通常用来创建某些行为的机制。通常定义如下:

传值:进入子程序时将参数复制到参数中 按结果传递:进入子程序时参数未定义,子程序返回时复制到参数中 按值结果传递:参数在入口处复制到参数中,参数在返回时复制到参数中 按引用传递:对参数变量的引用被复制到参数中;对参数变量的任何访问都会透明地转换为对参数变量的访问

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

教科书通常也定义了按名称传递,但在这里很少见且不容易解释。路过的需求也是存在的。

参数传递风格的重要性在于它的效果:在按值传递中,对参数所做的任何更改都不会传达给参数;在传递结果中,对参数所做的任何更改都会在最后传达给参数;在通过引用传递时,对参数所做的任何更改都会在进行时传达给参数。

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

现在,在 Java 中,从技术上讲,我们有一种按值传递的语言,对象变量的值是对该对象的引用。这是否使 Java 在涉及对象变量的地方通过引用传递是一个个人喜好问题。

【讨论】:

我想说的是,在 C 数组中,参数被转换为指针,并且该指针是按值传递的。 是的,标准就是这么定义的。但是效果基本一样。 关键是语言在这方面是一致的,值的传递方式没有例外(尽管有表达式导致从数组转换为指针的例外,但它们是非常合乎逻辑的例外,例如:sizeof) 在任何有引用的健壮语言中,一个重要的规则是引用永远不能超过它们所指向的东西。在具有真正的按引用传递语义的语言中,传递的引用是短暂的,因此具有临时变量的方法可以将对这些变量的引用传递给另一个方法,并且知道引用会在变量之前消失。相比之下,Java 只有混杂引用,只能用于引用持久堆对象。【参考方案7】:

如果您熟悉 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 中,类类型的变量总是references,在 C 中最忠实地映射到 pointers。但是在函数调用中,指针本身是通过副本传递的。 访问 #1j 和 #1c 中的指针会修改原始变量,因此从这个意义上说,您正在传递对变量的引用。但是,变量本身只是一个指针,它本身是通过拷贝传递的。因此,当您为其分配其他内容时。与#2j 和#2c 一样,您只是在f 的本地范围内重新绑定引用/指针的副本。各个示例中的原始变量 aw 保持不变。

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

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

注意 1:这不适用于像 int 这样的基本类型,它们完全按值传递。

注意 2:C++ 示例会更简洁一些,因为我可以通过引用传递指针(我不必说 struct):void f(Foo * &amp; r) r = new Foo; f(w);

【讨论】:

像往常一样,一个很好的答案。不幸的是,这是对我没有的问题的回答。我很抱歉因为不清楚而浪费了您的时间。 不用担心。是否还有其他答案未涵盖的仍需要解释的内容? 这取决于第 3 点的答案。如果达成共识(从某种意义上说,绝大多数 CS 人都同意一个含义,就像绝大多数数学家同意“素数”的含义一样),我仍然想知道是哪个含义。如果答案是,很多人都这么说,而且很多人这么说,我还是很高兴能确认这一点,但这是我目前得到的印象。 对。我不知道自己是否存在明确的答案,但应该相当清楚抽象概念的含义(通过引用传递:更改函数中的函数参数会更改调用站点的原始对象)。 实现引用语义特定于每种语言;例如,在 C 中,您可以通过添加一层地址/传递指针来实现它,而 C++ 具有本机引用类型。我想您应该在脑海中将抽象概念和特定于语言的实现分开。 ... 所以 Java 为对象实现了“按引用”语义,但对变量没有:因为 Java 没有“引用”类型修饰符,所以不能传递变量(始终是引用类型他们自己)再次引用并通过函数调用重新绑定它们。【参考方案8】:

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

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

将变量作为引用传递意味着指针嵌套变得更深:您将指针传递给对象引用,以便您可以更改对象引用;或者你传递一个指向原语的指针,这样你就可以改变它的值。

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

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

【讨论】:

以上是关于“通过引用传递”究竟是啥意思?的主要内容,如果未能解决你的问题,请参考以下文章

Gmail API 推送通知速率限制究竟是啥意思?

通过引用传递使用 const 有啥意义? C++ [重复]

C 和 C++ 中的“通过引用传递”到底有啥区别?

通过引用传递 C++ 迭代器有啥问题?

对象通过引用传递。 call_user_func 的参数不是。是啥赋予了?

在指针上使用 reference_wrapper 与通过引用传递指针有啥好处?