Answers:
首先,在CS理论中定义的“按值传递与按引用传递”的区别现在已过时,因为最初定义为“按引用传递”的技术已不再受欢迎,并且现在很少使用。1个
较新的语言2倾向于使用不同的(但相似的)技术对来达到相同的效果(见下文),这是造成混淆的主要原因。
造成混淆的第二个原因是,在“通过引用传递”中,“引用”的含义比通用术语“引用”的含义要窄(因为该短语早于它)。
现在,真实的定义是:
通过引用传递参数时,调用者和被调用者对该参数使用相同的变量。如果被调用方修改了参数变量,则效果对调用方的变量可见。
通过value传递参数时,调用方和被调用方将具有两个具有相同值的独立变量。如果被调用方修改了参数变量,则该效果对调用方不可见。
在此定义中要注意的是:
这里的“变量”是指调用方的(本地或全局)变量本身 -即,如果我通过引用传递局部变量并将其分配给它,那么我将更改调用方的变量本身,而不是例如它是指针时指向的变量。
“通过引用传递”中“引用”的含义。与“参考”一词的不同之处在于,该“参考”是临时的和隐含的。被呼叫者基本上得到的是一个“变量”,它在某种程度上与原始变量“相同”。这种效果的具体实现方式是无关紧要的(例如,语言还可以公开一些实现细节-地址,指针,取消引用-都是不相关的;如果最终效果是,则是按引用传递)。
现在,在现代语言中,变量倾向于是“引用类型”(另一种概念是在“按引用传递”之后发明并受其启发),即,实际的对象数据分别存储在某个地方(通常在堆上),并且仅对其的“引用”曾经保存在变量中并作为参数传递。3
传递这样的引用属于传递值,因为从技术上讲变量的值是引用本身,而不是引用的对象。但是,对程序的净影响可以与按值传递或按引用传递相同:
如您所见,这对技术与定义中的技术几乎相同,只是间接的程度:只需将“变量”替换为“引用对象”即可。
它们没有统一的名称,这导致了诸如“按值调用,其中值是引用”的错误解释。1975年,芭芭拉·里斯科夫(Barbara Liskov)提出了“ 按对象共享调用 ”(有时也称为“按共享调用”)一词,尽管该术语从未流行开来。而且,这两个短语都没有与原始短语相似。难怪旧的术语最终会在没有更好的东西的情况下被重用,从而导致混乱。4
注意:很长一段时间,这个答案曾经说过:
假设我想与您共享一个网页。如果我告诉您该URL,那么我是通过引用传递的。您可以使用该URL查看与我看到的网页相同的网页。如果该页面被更改,我们都会看到更改。如果删除该URL,那么您所做的就是破坏对该页面的引用-您并不是在删除实际的页面本身。
如果我打印出页面并给你打印输出,那我就按价值传递。您的页面是原始文档的断开连接副本。您将不会看到任何后续更改,并且您所做的任何更改(例如,在打印输出上进行涂鸦)都不会显示在原始页面上。如果销毁了打印输出,则实际上已销毁了对象的副本-但原始网页仍保持不变。
除 “引用”的狭义含义外,这基本上是正确的-它既是临时的又是隐式的(不是必须的,但显式和/或持久的是附加功能,而不是“按引用传递”语义的一部分) ,如上所述)。更为贴近的类比是给您一份文档副本,而不是邀请您使用原始文档。
1 除非您使用Fortran或Visual Basic进行编程,否则它不是默认行为,并且在现代使用的大多数语言中,甚至不可能进行真正的按引用调用。
2 相当多的老年人也支持它
3 在几种现代语言中,所有类型都是引用类型。这种方法由1975年的CLU语言首创,此后被包括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
}
使用此示例,我想定义一些重要的术语:
foo
是在第1行上声明的函数(Java坚持要使用所有函数方法,但是概念是相同的,并且不会失去一般性; C和C ++在声明和定义之间进行区分,我将不在这里介绍)param
是一个正式的参数来foo
,还宣布1号线arg
是变量,特别是函数的局部变量,bar
在第2行上进行了声明和初始化arg
也是一个参数到特定的调用的foo
第3行这里要区分两个非常重要的概念。首先是值与变量:
bar
上面的函数中,该行int arg = 1;
之后的表达式arg
具有值 1
。final
或C#声明readonly
)或深度不可变(例如,使用C ++ const
)。要区分的另一对重要概念是参数与参数:
在按值调用中,函数的形式参数是为函数调用新创建的变量,并使用其参数的值进行初始化。
这与使用值初始化任何其他种类的变量的方式完全相同。例如:
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编程语言(具有这些语义)时,她意识到现有的术语“按值调用”和“按引用调用”对于描述这种新语言的语义并不是特别有用。因此,她发明了一个新术语:通过对象共享进行调用。
在讨论从技术上讲是按值调用的语言,但在使用的通用类型是引用或指针类型的语言(即:几乎每种现代命令式,面向对象或多范式编程语言)时,我发现它的混淆要少得多只需避免谈论按值调用或按引用调用。坚持通过对象共享进行调用(或简单地通过object进行调用),没有人会感到困惑。:-)
The first is value versus variable.
The other important pair of concepts to distinguish is parameter versus argument:
在理解这两个术语之前,您必须了解以下内容。每个对象都有两个可以区分的事物。
所以如果你说 employee.name = "John"
我知道关于有两件事name
。它的值,它"John"
同时也是它在这也许是一些像这样的十六进制数的存储位置:0x7fd5d258dd00
。
根据语言的体系结构或对象的类型(类,结构等),您将转移"John"
或0x7fd5d258dd00
传递"John"
称为按值传递。通过0x7fd5d258dd00
称为通过引用传递。指向此存储位置的任何人都可以访问的值"John"
。
有关此的更多信息,我建议您阅读有关取消引用指针的知识,以及为什么选择结构(值类型)而不是类(引用类型)
这是一个例子:
#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?
按值传递-该函数复制变量并使用副本(因此它不会更改原始变量中的任何内容)
通过引用传递-该函数使用原始变量,如果您在另一个函数中更改了该变量,它也会在原始变量中也进行更改。
示例(复制并自己使用/尝试一下):
#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;
}
保持简单,偷看。文字墙可能是个坏习惯。
它们之间的主要区别在于,值类型变量存储值,因此在方法调用中指定值类型变量会将该变量值的副本传递给方法。引用类型变量存储对对象的引用,因此将引用类型变量指定为参数将使该方法将引用对象的实际引用的副本传递给该方法。即使引用本身是按值传递的,该方法仍可以使用接收到的引用与原始对象进行交互,并可能对其进行修改。类似地,当通过return语句从方法返回信息时,该方法返回存储在value-type变量中的值的副本或存储在reference-type变量中的引用的副本。当返回引用时,调用方法可以使用该引用与引用的对象进行交互。所以,
在c#中,要通过引用传递变量,以便被调用的方法可以修改变量的值,C#提供了关键字ref和out。将ref关键字应用于参数声明使您可以通过引用将变量传递给方法-被调用的方法将能够在调用方中修改原始变量。ref关键字用于在调用方法中已初始化的变量。通常,当方法调用包含未初始化的变量作为参数时,编译器会生成错误。在带有关键字out的参数之前创建一个输出参数。这向编译器指示该参数将通过引用传递给被调用的方法,并且被调用的方法将为调用方中的原始变量分配一个值。如果该方法未在每个可能的执行路径中为输出参数分配值,则编译器将生成错误。这也可以防止编译器为未初始化的变量生成错误消息,该错误消息作为参数传递给方法。一个方法只能通过return语句将一个值返回给其调用方,但是可以通过指定多个输出(ref和/或out)参数来返回许多值。
请参阅c#讨论和示例,在此处链接文本
按值传递是指如何通过使用参数将值传递给函数。在按值传递时,我们复制存储在指定变量中的数据,这比复制数据要慢于通过引用传递数据。我们对复制的数据进行更改,原始数据不受影响。通过引用或地址传递,我们将直接链接发送到变量本身。或将指针传递给变量。它更快,花费更少的时间
这是一个示例,演示按值传递-指针值-引用之间的区别:
void swap_by_value(int a, int b){
int temp;
temp = a;
a = b;
b = temp;
}
void swap_by_pointer(int *a, int *b){
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void swap_by_reference(int &a, int &b){
int temp;
temp = a;
a = b;
b = temp;
}
int main(void){
int arg1 = 1, arg2 = 2;
swap_by_value(arg1, arg2);
cout << arg1 << " " << arg2 << endl; //prints 1 2
swap_by_pointer(&arg1, &arg2);
cout << arg1 << " " << arg2 << endl; //prints 2 1
arg1 = 1; //reset values
arg2 = 2;
swap_by_reference(arg1, arg2);
cout << arg1 << " " << arg2 << endl; //prints 2 1
}
“通过引用”方法具有重要的局限性。如果一个参数被声明为通过引用传递(因此它前面带有&符号),则其对应的实际参数必须是一个变量。
引用“按值传递”形式参数的实际参数可以是表达式通常,因此,它不仅可以使用变量,还可以使用文字或函数调用的结果。
该函数不能将值放在变量以外的其他值中。它不能为文字分配新值,也不能强制表达式更改其结果。
PS:您也可以在当前主题中检查Dylan Beattie答案,以简单的语言对其进行解释。