首先,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回收一个合适的旧术语本身并没有什么不好,但必须以某种方式清楚地表明每次使用的含义。不这样做正是导致混淆的原因。
ref
)。Jon Skeet在这里也有一个很好的解释。代码
由于我的语言是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
许多答案(特别是最受欢迎的答案)事实上是错误的,因为它们误解了“按引用传递”的真正含义。这是我试图阐明问题的尝试。
简而言之:
隐喻意义上:
请注意,这两个概念与引用类型(在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
}
使用此示例,我想定义一些重要的术语:int arg = 1;
int another_variable = arg;
这里的 arg
和 another_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
这与我们上面示例中arg
和param
之间的关系完全相同。我在此重复一遍,以保持对称性:
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编程语言(具有这些语义)时,她意识到现有的术语“按值调用”和“按引用调用”不能很好地描述这种新语言的语义。因此,她创造了一个新术语:按对象共享调用。
在讨论技术上是按值调用的语言时,但常用类型是引用或指针类型(即几乎每个现代命令式、面向对象或多范式编程语言),我发现最好避免谈论按值调用或按引用调用。坚持使用按对象共享调用(或简单地使用按对象调用),就不会产生混淆。 :-)
在了解这两个术语之前,您必须先了解以下内容。每个对象都有两个可以区分它的东西:
因此,如果您说employee.name = "John"
,请知道关于name
有两件事情。 它的值是"John"
,它在内存中的位置是一些十六进制数字,可能像这样:0x7fd5d258dd00
。
根据语言的架构或对象的类型(类,结构等),您将传输"John"
或0x7fd5d258dd00
传递"John"
称为按值传递。
传递0x7fd5d258dd00
称为按引用传递。任何指向该内存位置的人都可以访问"John"
的值。
更多信息,请阅读解除指针引用和为什么选择结构体(值类型)而不是类(引用类型)。
&var
传递引用变量将被视为按引用传递,因为没有创建新变量。 - chetan这里有一个例子:
#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
}
y
已经在前面的代码行中被赋值为2了。为什么它会回到0呢? - isekaijin获取这个最简单的方法是在一个Excel文件中。比如说,假设你有两个数字5和2,分别在单元格A1和B1中,并且你想要在第三个单元格中找到它们的和,比如A2。
你可以用两种方法来实现:
通过将它们的值传递给单元格A2,方法是在该单元格键入= 5 + 2。这种情况下,如果单元格A1或B1的值发生改变,则A2中的总和仍然保持不变。
或者通过将单元格A1和B1的“引用”传递给单元格A2,方法是在该单元格键入= A1 + B1。这种情况下,如果单元格A1或B1的值发生改变,则A2中的总和也会发生变化。
通过引用传递时,你基本上是传递变量的指针。通过值传递,你传递的是变量的副本。
在基本用法中,这通常意味着使用引用传递,在变量发生改变时,调用方法中将会看到这些改变;而在通过值传递时,则不会看到这些改变。
按值传递会发送一个存储在指定变量中的数据副本,而按引用传递会直接发送到变量本身。
如果您通过引用传递变量,然后在传递它的块内更改该变量,原始变量将被更改。如果您仅按值传递,则无法通过传递它的块更改原始变量,但您将得到调用时它包含的任何内容的副本。
按值传递 - 函数复制变量并使用副本进行操作(因此不会更改原始变量)
按引用传递 - 函数使用原始变量。如果您在其他函数中更改变量,则原始变量也会更改。
示例(复制并自行尝试):
#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;
}