传递引用与传递值之间有什么区别?

707
“按引用传递”或“按值传递”是什么意思?这些参数有何区别?

1
如果您不知道地址是什么,请参见这里 - mfaani
18个回答

1256

首先,CS理论中定义的“按值传递 vs. 按引用传递”的区别现在已经过时,因为最初定义为“按引用传递”的技术已经不再流行,现在很少使用。1

较新的语言2倾向于使用一对不同(但类似)的技术来实现相同的效果(见下文),这是混淆的主要原因。

第二个混淆的来源是“按引用传递”中,“引用”的含义比一般术语“引用”的含义更狭窄(因为该短语早于它的出现)。


现在,正式的定义是:

  • 当参数被引用传递时,调用方和被调用方使用同一个变量来表示该参数。如果被调用方修改了参数变量,则调用方的变量也会受到影响。

  • 当参数被值传递时,调用方和被调用方各自拥有两个相同值的独立变量。如果被调用方修改了参数变量,则对调用方的变量没有影响。

需要注意此定义中的内容:

  • 在这里,“变量”指的是调用者(本地或全局)变量本身——也就是说,如果我通过引用传递一个本地变量并对其进行赋值,我将改变调用者的变量本身,而不是例如它指向的任何内容(如果它是指针)。

    • 现在认为这是一种不好的做法(作为隐式依赖项)。因此,几乎所有较新的语言都是仅通过值传递,或几乎仅通过值传递。按引用传递现在主要用于“输出/输入参数”的形式,在这些语言中,函数无法返回多个值。
  • “按引用传递”中“引用”的含义。与一般“引用”术语的区别在于,这个“引用”是临时和隐式的。被调用方得到的是某种程度上“与原始变量相同”的“变量”。具体实现方式如何并不重要(例如,语言可能还公开一些实现细节——地址、指针、解引用——这些都不重要;如果净效果是这样,那么就是按引用传递)。


现代编程语言中,变量往往是“引用类型”(这是一个比“按引用传递”更晚发明并受其启发的概念),即实际对象数据单独存储在某个地方(通常是堆上),而只有对它的“引用”被保存在变量中并作为参数传递。3

传递这样的引用属于按值传递,因为变量的值技术上是引用本身,而不是所引用的对象。然而,对程序的净影响可能与按值传递或按引用传递相同:

  • 如果一个引用只是从调用者的变量中获取并作为参数传递,这与按引用传递具有相同的效果:如果在被调用者中对所引用的对象进行了更改,调用者将看到这个变化。
    • 然而,如果持有该引用的变量被重新赋值,它将停止指向那个对象,因此对该变量的任何进一步操作将影响它现在指向的任何内容。
  • 要达到按值传递的效果,需要在某个时刻创建对象的副本。选项包括:
    • 调用者可以在调用之前制作一个私有副本,并将该副本的引用提供给被调用者。
    • 在某些语言中,某些对象类型是“不可变”的:对它们进行的任何似乎改变值的操作都会创建一个全新的对象,而不会影响原始对象。因此,将此类类型的对象作为参数传递总是具有按值传递的效果:如果需要更改,被调用者将自动创建副本,而调用者的对象将永远不会受到影响。
      • 在函数式语言中,所有对象都是不可变的。

正如您所看到的,这对技术与定义中的那些几乎相同,只是多了一层间接性:将“变量”替换为“引用对象”。

它们没有被确定的名称,导致了扭曲的解释,例如“按值调用其中的值是引用”。 1975年,Barbara Liskov 提出了术语 "共享对象调用"(有时仅称为“共享调用”),但它从未真正流行起来。此外,这两个短语都没有与原始对成比例。难怪在没有更好的东西的情况下,旧术语被重用,导致混淆。4

(我会使用术语 “新”的“间接”的按值/按引用传递 来表示这些新技术。)


注意:长时间以来,这个答案曾经说过:

假设我想与您分享一个网页。如果我告诉您URL,我是通过引用传递。您可以使用该URL查看与我相同的网页。如果该页面被更改,我们都会看到更改。如果您删除URL,则仅销毁了对该页面的引用-您并未删除实际页面本身。

如果我打印页面并将其给您,我是通过值传递。您的页面是原始页面的断开副本。您将不会看到任何后续更改,并且您进行的任何更改(例如在打印输出上涂鸦)将不会显示在原始页面上。如果您销毁打印输出,您已销毁对象的副本-但原始网页仍然完好无损。

这基本上是正确的,但“引用”的狭义含义——它既是临时的又是隐式的(它不必须这样,但显式和/或持久性是附加功能,而不是按引用传递语义的一部分,如上所述)。更接近的类比是给您一份文件副本与邀请您在原始文件上工作之间的区别。


1除非您在使用Fortran或Visual Basic进行编程,否则这不是默认行为,在大多数现代语言中,真正的按引用调用甚至都不可能。

2相当数量的旧语言也支持它。

3在几种现代语言中,所有类型都是引用类型。这种方法最初由CLU语言于1975年开创,并已被许多其他语言采用,包括Python和Ruby。还有许多语言采用混合方法,其中一些类型是“值类型”,而另一些类型是“引用类型”——其中包括C#、Java和JavaScript。

4回收一个合适的旧术语本身并没有什么不好,但必须以某种方式清楚地表明每次使用的含义。不这样做正是导致混淆的原因。


2
你提供的“真正”的定义并不是几乎所有入门编程课程中给出的定义。如果你谷歌一下什么是按引用传递,你不会得到那个答案。你提供的“真正”定义误用了“引用”这个词,因为当你遵循那个定义时,你使用的是一个别名而不是引用:你有两个实际上是同一个变量的变量,那就是别名而不是引用。你提供的“真正”定义没有任何意义地导致了大量混淆。只需说按引用传递意味着传递地址。这是有道理的,也可以避免这种无意义的混淆。 - ICW
3
请提供一个“几乎每个编程入门课程中都会提到的定义”的链接。请注意,这应该清楚地反映今天的现实情况,而不是几十年前某些计算机科学课程写作时的情况。在定义中不能使用“地址”,因为它有意抽象化了可能的实现方式。例如,有些编程语言(如Fortran)没有指针;它们也在是否向用户公开原始地址方面存在差异(VB不支持);而且它也不一定是一个原始的内存地址,任何可以链接到变量的东西都可以。 - ivan_pozdeev
1
@YungGun "太长了,没看完"。一瞥就能看到答案中概述的混淆情况。传递引用是一种与实现无关的抽象技术。重要的不是在底层究竟传递了什么,而是对程序产生的影响。 - ivan_pozdeev
1
“对程序的影响可以与传值或传引用相同”:我不同意效果与旧的“传引用”相同,因为调用者变量无法从被调用方内部重新分配。 - Rafael Eyng
2
芭芭拉·利斯科夫建议使用“对象共享调用”这个术语,但目前的文本并没有明确指出这个名称是指第一种还是第二种技术。 - Rafael Eyng
显示剩余9条评论

181
这是一种传递函数参数的方式。按引用传递意味着被调用函数的参数将与调用者传递的参数相同(不是值,而是身份 - 变量本身)。按值传递意味着被调用函数的参数将是调用者传递参数的副本。值将相同,但身份 - 变量 - 不同。因此,在一种情况下,由被调用函数对参数所做的更改会更改传递的参数,而在另一种情况下,仅更改被调用函数中参数的值(这仅是副本)。快速概括:

  • Java仅支持按值传递。即使在复制对象的引用时,被调用函数的参数也将指向相同的对象,并且对该对象的更改将在调用者中看到。由于这可能会令人困惑,这里是Jon Skeet对此的解释。
  • C#支持按值传递和按引用传递(在调用方和被调用函数中使用关键字ref)。Jon Skeet在这里也有一个很好的解释。
  • C ++支持按值传递和按引用传递(在被调用函数中使用引用参数类型)。您将在下面找到对此的解释。

代码

由于我的语言是C ++,因此我将在这里使用它

// passes a pointer (called reference in java) to an integer
void call_by_value(int *p) { // :1
    p = NULL;
}

// passes an integer
void call_by_value(int p) { // :2
    p = 42;
}

// passes an integer by reference
void call_by_reference(int & p) { // :3
    p = 42;
}

// this is the java style of passing references. NULL is called "null" there.
void call_by_value_special(int *p) { // :4
    *p = 10; // changes what p points to ("what p references" in java)
    // only changes the value of the parameter, but *not* of 
    // the argument passed by the caller. thus it's pass-by-value:
    p = NULL;
}

int main() {
    int value = 10;
    int * pointer = &value;

    call_by_value(pointer); // :1
    assert(pointer == &value); // pointer was copied

    call_by_value(value); // :2
    assert(value == 10); // value was copied

    call_by_reference(value); // :3
    assert(value == 42); // value was passed by reference

    call_by_value_special(pointer); // :4
    // pointer was copied but what pointer references was changed.
    assert(value == 10 && pointer == &value);
}

并且Java的例子也不会有害:

class Example {
    int value = 0;

    // similar to :4 case in the c++ example
    static void accept_reference(Example e) { // :1
        e.value++; // will change the referenced object
        e = null; // will only change the parameter
    }

    // similar to the :2 case in the c++ example
    static void accept_primitive(int v) { // :2
        v++; // will only change the parameter
    }        

    public static void main(String... args) {
        int value = 0;
        Example ref = new Example(); // reference

        // note what we pass is the reference, not the object. we can't 
        // pass objects. The reference is copied (pass-by-value).
        accept_reference(ref); // :1
        assert ref != null && ref.value == 1;

        // the primitive int variable is copied
        accept_primitive(value); // :2
        assert value == 0;
    }
}

维基百科

http://en.wikipedia.org/wiki/Pass_by_reference#Call_by_value

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

这个人几乎完美地描述了它:

http://javadude.com/articles/passbyvalue.htm


你传递指针到函数的地方。指针只是允许你修改它所指向的值,并且这个值会反映在指针所指向的值上。如果使用指针修改形式参数,那么参数也应该改变吗?还是我漏掉了什么?难道不应该是按引用传递吗? - Avan

127

许多答案(特别是最受欢迎的答案)事实上是错误的,因为它们误解了“按引用传递”的真正含义。这是我试图阐明问题的尝试。

TL;DR

简而言之:

  • 按值传递意味着您将作为函数参数传递
  • 按引用传递意味着您将变量作为函数参数传递

隐喻意义上:

  • 按值传递是我在纸上写下一些东西,然后把它交给你。也许它是一个URL,也许是《战争与和平》的完整副本。不管它是什么,它都在一张我交给你的纸上,所以现在它有效地成为你的纸。你现在可以在那张纸上涂鸦,或者使用那张纸去找到其他地方的东西并搞一下,随便。
  • 按引用传递是我给你我的笔记本,里面写着一些东西。你可以在我的笔记本上涂鸦(也许我想让你这样做,也许不想),之后我保留我的笔记本,其中包含你放置的任何涂鸦信息。此外,如果你或我在那里写下的是有关如何寻找其他东西的信息,则你或我都可以去那里并搞一下该信息。

“按值传递”和“按引用传递”不意味着什么

请注意,这两个概念与引用类型(在Java中是所有子类型为Object的类型,在C#中是所有class类型)或类似C的指针类型(在语义上等同于Java的“引用类型”,只是具有不同的语法)的概念完全独立且正交。

“引用类型”的概念对应于URL:它既是一个信息本身,又是一个引用(如果您愿意,是一个指针)到其他信息。您可以在不同的位置拥有多个URL副本,它们不会更改它们所有的链接网站;如果更新了网站,则每个URL副本仍将导向已更新的信息。反之,在任何一个地方更改URL都不会影响URL的任何其他书面副本。

请注意,C++ 中有一个名为“引用”的概念(例如 int&),它与 Java 和 C# 的“引用类型”不同,但类似于“按引用传递”。Java 和 C# 的“引用类型”,以及 Python 中的所有类型,都类似于 C 和 C++ 中所谓的“指针类型”(例如 int*)。


好的,下面是更长、更正式的解释。

术语

首先,我想强调一些重要的术语,以帮助澄清我的答案并确保我们在使用词语时都指的是相同的概念。(实际上,我认为这些主题的绝大部分混乱都源自使用词语的方式不足以完全传达预期的含义。)

首先,以下是某种类 C 语言的函数声明示例:

void foo(int param) {  // line 1
    param += 1;
}

以下是调用此函数的示例:

void bar() {
    int arg = 1;  // line 2
    foo(arg);     // line 3
}
使用此示例,我想定义一些重要的术语:
- `foo` 是在第1行声明的一个函数(Java 坚持使所有函数都成为方法,但在不失一般性的情况下,概念相同;C 和 C++ 在此处区分声明和定义,我将不在此展开) - `param` 是 `foo` 的一个形式参数,也在第1行声明 - `arg` 是一个变量,特别是 `bar` 函数的一个局部变量,在第2行声明和初始化 - `arg` 也是在第3行对 `foo` 的特定调用的一个参数
这里需要区分两个非常重要的概念集。第一个是“值”与“变量”:
- “值”是在语言中“评估表达式”的结果。例如,在上面的 `bar` 函数中,在 `int arg = 1;` 这一行后,表达式 `arg` 具有值 `1`。 - “变量”是“值的容器”。变量可以是可变的(这是大多数 C 类语言的默认设置),只读的(例如使用 Java 的 `final` 或 C# 的 `readonly` 声明)或深度不可变的(例如使用 C++ 的 `const`)。
另一个重要的概念对是“参数”与“参数值”:
- “参数”(也称为“形式参数”)是必须由调用函数的调用方提供的变量。 - “参数值”是由函数的调用方提供的值,以满足该函数的特定形式参数。
按值调用:
在“按值调用”中,函数的形式参数是为函数调用新创建的变量,并以其参数的值初始化。
这与任何其他类型的变量以值初始化的方式完全相同。例如:
int arg = 1;
int another_variable = arg;

这里的 arganother_variable 是完全独立的变量,它们的值可以相互独立地改变。然而,在声明 another_variable 的时候,它被初始化为与 arg 持有相同的值,即 1

由于它们是独立变量,对 another_variable 的更改不会影响到 arg

int arg = 1;
int another_variable = arg;
another_variable = 2;

assert arg == 1; // true
assert another_variable == 2; // true

这与我们上面示例中argparam之间的关系完全相同。我在此重复一遍,以保持对称性:

void foo(int param) {
  param += 1;
}

void bar() {
  int arg = 1;
  foo(arg);
}

就好像我们是这样编写代码一样:

// entering function "bar" here
int arg = 1;
// entering function "foo" here
int param = arg;
param += 1;
// exiting function "foo" here
// exiting function "bar" here

也就是说,所谓的按值调用的定义特征是被调用者(在这个例子中为foo)接收参数时是作为传递的,但它有自己独立的变量来保存这些值,不会影响调用者(在这个例子中为bar)的变量。

回到我之前提到的比喻,如果我是bar,你是foo,当我调用你时,我会给你一张写着一个值的纸条。你把那张纸叫做param。那个值是我笔记本上写下的值(也就是我的局部变量),在你这边以一个名为arg的变量保存了一份拷贝

(附带一提:根据硬件和操作系统的不同,有各种关于如何从一个函数调用另一个函数的调用约定。调用约定就像我们决定我是将值写在我的纸上然后递给你,还是你有一张纸让我在上面写,或者我在我们两个人面前的墙上写下这个值。这也是一个有趣的主题,但已经远远超出了这个已经很长的回答的范围。)

按引用调用

按引用调用中,函数的形式参数只是调用者提供的参数变量的另一种名称而已。

回到我们上面的例子,这等价于:

// entering function "bar" here
int arg = 1;
// entering function "foo" here
// aha! I note that "param" is just another name for "arg"
arg /* param */ += 1;
// exiting function "foo" here
// exiting function "bar" here

param只是另一个名字来代替arg,也就是说,它们是同一变量,对param所做的更改会反映在arg中。这是传递引用与传递值之间根本的不同之处。

很少有语言支持传递引用,但C++可以像这样实现:

void foo(int& param) {
  param += 1;
}

void bar() {
  int arg = 1;
  foo(arg);
}

在这种情况下,param不仅具有与arg相同的,实际上它是arg(只是另一个名称),因此bar可以观察到arg已经被增加。

请注意,这不是任何Java、JavaScript、C、Objective-C、Python或几乎任何其他流行语言的工作方式。 这意味着那些语言不是按引用调用,而是按值调用。

补充说明:按对象共享调用

如果您拥有的是按值调用,但实际值是引用类型指针类型,那么“值”本身并不是很有趣(例如,在C中,它只是特定于平台大小的整数)--有趣的是该值指向的内容。

如果该引用类型(即指针)指向的是可变的,则可能出现有趣的效果:您可以修改所指向的值,并且调用者可以观察到所指向的值的更改,尽管调用者无法观察指针本身的更改。

再次借用URL的比喻,如果我们都关心的是网站而不是URL,则我向您提供URL的副本并不特别有趣。您在自己的URL副本上涂鸦并不会影响到我的URL副本,这并不是我们关心的事情(实际上,在像Java和Python这样的语言中,“URL”或引用类型值根本不能被修改,只能修改指向它的内容)。

当芭芭拉·利斯科夫(Barbara Liskov)发明CLU编程语言(具有这些语义)时,她意识到现有的术语“按值调用”和“按引用调用”不能很好地描述这种新语言的语义。因此,她创造了一个新术语:按对象共享调用

在讨论技术上是按值调用的语言时,但常用类型是引用或指针类型(即几乎每个现代命令式、面向对象或多范式编程语言),我发现最好避免谈论按值调用按引用调用。坚持使用按对象共享调用(或简单地使用按对象调用),就不会产生混淆。 :-)


更好地解释:这里有两组非常重要的概念需要区分。 第一组是值与变量。 另一个需要区分的重要概念对是参数与参数值。 - S.K. Venkat
7
非常好的回答。我认为我可以补充一点,那就是在按引用传递时不需要创建新的存储空间。参数名称引用了原始存储(内存)。 - drlolly
3
在我看来,最佳答案是 - Rafael Eyng

77

在了解这两个术语之前,您必须先了解以下内容。每个对象都有两个可以区分它的东西:

  • 它的值。
  • 它的地址。

因此,如果您说employee.name = "John",请知道关于name有两件事情。 它的值是"John",它在内存中的位置是一些十六进制数字,可能像这样:0x7fd5d258dd00

根据语言的架构或对象的类型(类,结构等),您将传输"John"0x7fd5d258dd00

传递"John"称为按值传递。

传递0x7fd5d258dd00称为按引用传递。任何指向该内存位置的人都可以访问"John"的值。

更多信息,请阅读解除指针引用为什么选择结构体(值类型)而不是类(引用类型)


4
这正是我在寻找的,实际上应该寻找概念而不仅是解释,点赞兄弟。 - Haisum Usman
1
Java始终是按值传递的。在Java中传递对象引用被认为是按值传递。这与您的声明“传递0x7fd5d258dd00被称为按引用传递”相矛盾。 - chetan
抱歉,伙计。我没有C++经验。 - mfaani
@chetan 不,这不是矛盾。通过地址传递对象/字符串/整数/任何东西被称为“按引用传递对象/字符串等”,这相当于“按值传递其地址”。因此,它从来不仅仅是按引用传递或按值传递;它总是按引用或按值传递某些东西。 - Vadim Samokhin
@VadimSamokhin 这是一个矛盾之处。传递对象的地址并不是按引用传递。这样Java就会按对象的引用传递。真正的区别在于函数中是否创建了新变量。例如,在C中使用&var传递引用变量将被视为按引用传递,因为没有创建新变量。 - chetan
显示剩余2条评论

55

这里有一个例子:

#include <iostream>

void by_val(int arg) { arg += 2; }
void by_ref(int&arg) { arg += 2; }

int main()
{
    int x = 0;
    by_val(x); std::cout << x << std::endl;  // prints 0
    by_ref(x); std::cout << x << std::endl;  // prints 2

    int y = 0;
    by_ref(y); std::cout << y << std::endl;  // prints 2
    by_val(y); std::cout << y << std::endl;  // prints 2
}

1
我认为有一个问题,因为最后一行应该打印0而不是2。请告诉我是否有遗漏。 - Taimoor Changaiz
@TaimoorChangaiz; 你说的“最后一行”是指哪个?顺便说一句,如果你会用IRC,请来Freenode上的##programming频道。在那里解释东西会更容易些。我的昵称是“pyon”。 - isekaijin
1
@EduardoLeón by_val(y); std::cout << y << std::endl; // 输出2 - Taimoor Changaiz
6
为什么不会输出2呢?变量y已经在前面的代码行中被赋值为2了。为什么它会回到0呢? - isekaijin
@EduardoLeón 我错了。是的,你是对的。谢谢纠正。 - Taimoor Changaiz

32

获取这个最简单的方法是在一个Excel文件中。比如说,假设你有两个数字5和2,分别在单元格A1和B1中,并且你想要在第三个单元格中找到它们的和,比如A2。

你可以用两种方法来实现:

  • 通过将它们的值传递给单元格A2,方法是在该单元格键入= 5 + 2。这种情况下,如果单元格A1或B1的值发生改变,则A2中的总和仍然保持不变。

  • 或者通过将单元格A1和B1的“引用”传递给单元格A2,方法是在该单元格键入= A1 + B1。这种情况下,如果单元格A1或B1的值发生改变,则A2中的总和也会发生变化。


1
这是所有答案中最简单和最好的例子。 - Amit Ray

23

通过引用传递时,你基本上是传递变量的指针。通过值传递,你传递的是变量的副本。

在基本用法中,这通常意味着使用引用传递,在变量发生改变时,调用方法中将会看到这些改变;而在通过值传递时,则不会看到这些改变。


18

按值传递会发送一个存储在指定变量中的数据副本,而按引用传递会直接发送到变量本身。

如果您通过引用传递变量,然后在传递它的块内更改该变量,原始变量将被更改。如果您仅按值传递,则无法通过传递它的块更改原始变量,但您将得到调用时它包含的任何内容的副本。


15

看一下这张照片:

在第一个例子中(按引用传递),当变量在函数内被设置或更改时,外部变量也会改变。

但在第二个例子中(按值传递),在函数内更改变量不会对外部变量产生任何影响。

阅读本文,请查看此链接

按引用传递 vs 按值传递


9

按值传递 - 函数复制变量并使用副本进行操作(因此不会更改原始变量)

按引用传递 - 函数使用原始变量。如果您在其他函数中更改变量,则原始变量也会更改。

示例(复制并自行尝试):

#include <iostream>

using namespace std;

void funct1(int a) // Pass-by-value
{
    a = 6; // Now "a" is 6 only in funct1, but not in main or anywhere else
}

void funct2(int &a)  // Pass-by-reference
{
    a = 7; // Now "a" is 7 both in funct2, main and everywhere else it'll be used
}

int main()
{
    int a = 5;

    funct1(a);
    cout << endl << "A is currently " << a << endl << endl; // Will output 5
    funct2(a);
    cout << endl << "A is currently " << a << endl << endl; // Will output 7

    return 0;
}

保持简单明了,朋友们。冗长的文字可能是一种不好的习惯。

这对于理解参数值是否被更改非常有帮助,谢谢! - Kevin Zhao

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接