有什么区别
通过引用传递的参数
一个由值传递的参数?
你能给我一些例子吗?
首先,CS理论中定义的"按值传递与按引用传递"的区别现在已经过时,因为最初定义为"通过引用传递"的技术已经失宠,现在很少使用.1
较新的语言2倾向于使用不同(但相似)的技术对来实现相同的效果(见下文),这是混淆的主要原因.
混淆的第二个原因是,在"通过引用传递"中,"引用"具有比通用术语"引用"更窄的含义(因为该术语早于它).
现在,真正的定义是:
当通过引用传递参数时,调用者和被调用者对参数使用相同的变量.如果被调用者修改了参数变量,则调用者的变量可以看到该效果.
当一个参数按值传递时,调用者和被调用者有两个具有相同值的独立变量.如果被调用者修改了参数变量,则调用者看不到该效果.
此定义中要注意的事项是:
"变量"在这里表示调用者(本地或全局)变量本身 - 即如果我通过引用传递局部变量并分配给它,我将更改调用者的变量本身,而不是例如它指向的是否是指针.
现在这被认为是不好的做法(作为隐含的依赖).因此,几乎所有较新的语言都是专有或几乎完全按价值传递.传递引用现在主要以"输出/输出参数"的形式使用,其中函数不能返回多个值.
"通过引用传递"中"引用"的含义.与一般"参考"术语的不同之处在于,这个"参考"是暂时的和隐含的.被调用者基本上得到的是一个"变量",它与原始的"变量"在某种程度上"相同".具体如何实现这种效果是无关紧要的(例如,语言也可能暴露一些实现细节 - 地址,指针,解除引用 - 这都是无关紧要的;如果净效应是这样的话,它就是传递参考).
现在,在现代语言中,变量往往是"引用类型"(后来发明的另一个概念,而不是"通过引用传递"并受其启发),即实际的对象数据分别存储在某个地方(通常在堆上),以及只有"引用"它才会被保存在变量中并作为参数传递.3
传递这样的引用属于pass-by-value,因为变量的值在技术上是引用本身,而不是引用的对象.但是,对程序的净影响可以与按值传递或按引用传递相同:
如果引用只是从调用者的变量中获取并作为参数传递,则这与引用传递具有相同的效果:如果被调用的对象在被调用者中发生变异,则调用者将看到更改.
但是,如果重新保持包含此引用的变量,它将停止指向该对象,因此对此变量的任何进一步操作将影响它现在指向的任何内容.
要获得与传值相同的效果,可以在某个时刻创建对象的副本.选项包括:
调用者可以在调用之前创建一个私有副本,并为被调用者提供对它的引用.
在某些语言中,某些对象类型是"不可变的":对它们的任何操作似乎改变了值实际上创建了一个全新的对象,而不会影响原始对象.因此,传递此类型的对象作为参数始终具有传值的效果:如果需要更改,则会自动生成被调用者的副本,并且调用方的对象永远不会受到影响.
在函数式语言中,所有对象都是不可变的.
正如您所看到的,这对技术几乎与定义中的技术相同,只是具有间接级别:只需将"变量"替换为"引用对象".
它们之间没有达成一致的名称,这导致了扭曲的解释,例如"价值呼叫,其中价值是参考".1975年,芭芭拉·莱斯科夫(Barbara Liskov)提出了" 按对象分享 " 这一术语(或者有时仅仅是"分享呼叫"),尽管它从未完全流行起来.而且,这些短语都没有与原始对相平行.难怪旧条款最终被重复使用,导致混乱.4
注意:很长一段时间,这个答案常说:
说我想和你分享一个网页.如果我告诉你URL,我通过引用传递.您可以使用该URL查看我可以看到的同一网页.如果该页面已更改,我们都会看到更改.如果您删除了URL,那么您所做的只是破坏对该页面的引用 - 您不会删除实际页面本身.
如果我打印出页面并给你打印输出,我就会超值.您的页面是原始的断开连接的副本.您不会看到任何后续更改,并且您所做的任何更改(例如,在您的打印输出上涂写)都不会显示在原始页面上.如果您销毁打印输出,实际上已经销毁了对象的副本 - 但原始网页保持不变.
除了 "引用"的狭义含义之外,这大多是正确的 - 它既是临时的又是隐含的(它不是必须的,但是显式和/或持久性是附加特征,而不是传递参考语义的一部分) ,如上所述).更接近的类比是给你一份文件副本,邀请你去处理原件.
1 除非您使用Fortran或Visual Basic进行编程,否则它不是默认行为,并且在现代使用的大多数语言中,甚至不可能实现真正的逐个引用.
2 相当数量的老年人也支持它
3 在几种现代语言中,所有类型都是引用类型.这种方法是由语言CLU在1975年开创的,并且已被许多其他语言采用,包括Python和Ruby.还有更多的语言使用混合方法,其中一些类型是"值类型"而另一些类型是"引用类型" - 其中包括C#,Java和JavaScript.
4 回收一个合适的旧术语本身没有什么不好,但是必须以某种方式明确每次使用哪个含义.不这样做正是导致混乱的原因.
这是一种如何将参数传递给函数的方法.通过引用传递意味着被调用函数的参数将与调用者的传递参数相同(不是值,而是标识 - 变量本身).按值传递意味着被调用函数的参数将是调用者传递的参数的副本.值将是相同的,但身份 - 变量 - 是不同的.因此,在一种情况下由被调用函数完成的参数的改变改变了传递的参数,而在另一种情况下,仅改变被调用函数中的参数的值(这只是一个拷贝).快点:
Java仅支持按值传递.始终复制参数,即使复制对象的引用时,被调用函数中的参数也将指向同一个对象,并且将在调用者中看到对该对象的更改.由于这可能令人困惑,这就是Jon Skeet对此的评论.
C#支持按值传递并通过引用传递(ref
在调用者和被调用函数中使用的关键字).乔恩斯基特也有这样一个很好的解释在这里.
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
这里的许多答案(特别是最受高度评价的答案)实际上是不正确的,因为他们误解了"引用的呼叫"的真正含义.这是我试图解决问题的尝试.
简单来说:
按值调用意味着您将值作为函数参数传递
通过引用调用意味着您将变量作为函数参数传递
用比喻来说:
按价值呼叫是我在一张纸上写下东西然后交给你的地方.也许它是一个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
也是一个参数到特定的调用的foo
第3行
这里有两个非常重要的概念要区分.第一个是价值与变量:
甲值是计算表达式的结果的语言.例如,在bar
上面的函数中,在行之后int arg = 1;
,表达式arg
具有值 1
.
甲变量是一个对值的容器.变量可以是可变的(这是大多数类C语言中的默认值),只读(例如使用Java 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编程语言(具有这些语义)时,意识到现有术语"按值调用"和"按引用调用"对于描述这种新语言的语义并不是特别有用.所以她发明了一个新术语:通过对象共享来调用.
在讨论技术上按值调用的语言时,但是使用的常用类型是引用或指针类型(即:几乎所有现代命令式,面向对象或多范式编程语言),我发现它不那么令人困惑.简单地避免谈论价值呼叫或通过参考呼叫.坚持通过对象共享调用(或简单地按对象调用),没有人会感到困惑.:-)
在理解这两个术语之前,您必须了解以下内容.每个对象都有两件可以区分的东西.
它的价值.
它的地址.
所以,如果你说 employee.name = "John"
知道有两件事name
.它的值"John"
以及它在内存中的位置是一些十六进制数可能是这样的:0x7fd5d258dd00
.
根据语言的结构或类型的对象(类,结构等),你会要么转移"John"
或0x7fd5d258dd00
传递"John"
被称为传递价值.传递0x7fd5d258dd00
被称为通过引用传递.指向此内存位置的任何人都可以访问其值"John"
.
有关这方面的更多信息,我建议您阅读有关取消引用指针的信息,以及为什么选择struct(值类型)而不是类(引用类型)
这是一个例子:
#include
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
}
获取此信息的最简单方法是在Excel文件上.例如,假设您在单元格A1和B1中有两个数字5和2,并且您希望在第三个单元格中找到它们的总和,假设为A2.你可以用两种方式做到这一点.
通过在此单元格中键入= 5 + 2 将其值传递给单元格A2.在这种情况下,如果单元格A1或B1的值改变,则A2中的总和保持不变.
或者通过键入= A1 + B1 将单元格A1和B1的"引用"传递给单元格A2.在这种情况下,如果单元格A1或B1的值改变,则A2中的总和也改变.
当你通过ref传递时,你基本上是传递一个指向变量的指针.通过值传递您传递变量的副本.在基本用法中,这通常意味着传递给变量的变量将被视为调用方法并且通过值传递它们不会.
传递值发送存储在您指定的变量中的数据的COPY,通过引用传递直接链接到变量本身.因此,如果您通过引用传递变量然后更改传递给它的块内的变量,则原始变量将被更改.如果单纯按值传递,原始变量将无法通过你传入到该块被改变,但你会得到什么它包含在呼叫副本的时候.
按值传递 - 该函数复制变量并使用副本(因此它不会更改原始变量中的任何内容)
通过引用传递 - 该函数使用原始变量,如果更改另一个函数中的变量,它也会更改原始变量.
示例(复制并使用/自己试试看看):
#includeusing 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< 保持简单,窥视.文字墙可能是一个坏习惯.
10> 小智..:它们之间的主要区别在于值类型变量存储值,因此在方法调用中指定值类型变量会将该变量值的副本传递给该方法.引用类型变量存储对对象的引用,因此将引用类型变量指定为参数会将方法传递给引用该对象的实际引用的副本.即使引用本身是按值传递的,该方法仍然可以使用它接收的引用来与原始对象进行交互,并可能修改原始对象.类似地,当通过return语句从方法返回信息时,该方法返回存储在value-type变量中的值的副本或存储在reference-type变量中的引用的副本.返回引用时,调用方法可以使用该引用与引用的对象进行交互.所以,
在c#中,要通过引用传递变量,以便被调用的方法可以修改变量,C#提供关键字ref和out.将ref关键字应用于参数声明允许您通过引用将变量传递给方法 - 被调用的方法将能够修改调用者中的原始变量.ref关键字用于已在调用方法中初始化的变量.通常,当方法调用包含未初始化的变量作为参数时,编译器会生成错误.在关键字out之前添加参数会创建输出参数.这向编译器指示参数将通过引用传递给被调用的方法,并且被调用的方法将为调用者中的原始变量赋值.如果方法未在每个可能的执行路径中为输出参数赋值,则编译器会生成错误.这还可以防止编译器为作为参数传递给方法的未初始化变量生成错误消息.方法只能通过return语句向其调用者返回一个值,但可以通过指定多个输出(ref和/或out)参数来返回许多值.
请参阅c#discussion和examples 链接文本