it-swarm.dev

传递参考与传递值之间有什么区别?

有什么区别

  1. 通过引用传递的参数
  2. 一个由值传递的参数?

你能给我一些例子吗?

506
ritu

首先, CS理论中定义的“按值传递与按引用传递”的区别现在已过时 因为 最初定义为“通过引用传递”的技术已经失效青睐 现在很少使用。1

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

混淆的第二个原因是 在“通过引用传递”中,“引用”具有比通用术语“引用”更窄的含义 (因为该术语早于它)。


现在,真正的定义是:

  • 当参数是 通过引用 传递时,调用者和被调用者 对参数使用相同的变量 。如果被调用者修改了参数变量,则调用者的变量可以看到该效果。

  • 当参数是 通过值 传递时,调用者和被调用者具有 两个独立变量 具有相同的值。如果被调用者修改了参数变量,则调用者看不到该效果。

此定义中要注意的事项是:

  • “变量”在这里表示调用者(本地或全局)变量本身 - 即如果我通过引用传递局部变量并赋值给它,我将更改调用者的变量本身,而不是例如无论它指向什么,如果它是一个指针。

    • 现在这被认为是不好的做法(作为隐含的依赖)。因此, 几乎所有较新的语言都是专有或几乎完全按值传递。 Pass-by-reference现在主要以“output/inout arguments”的形式用于语言中,其中函数不能返回多个值。
  • “通过引用传递”中“引用”的含义 。与一般“参考”术语的区别在于 这个“参考”是暂时的和隐含的。 被调用者基本上得到的是 一个“变量”,它与原始的“变量”在某种程度上“相同”。 如何具体实现这种效果是无关紧要的(例如,语言也可能暴露一些实现细节 - 地址,指针,解除引用 - 这都是无关紧要的;如果净效果是这样的话,它就是传递参考)。


现在,在现代语言中,变量往往是“引用类型” (后来发明的另一个概念,而不是“通过引用传递”并受其启发),即实际的对象数据在某处单独存储(通常,在堆上),只有“引用”才能保存在变量中并作为参数传递。3

传递这样的引用属于pass-by-value 因为变量的值在技术上是引用本身,而不是引用的对象。但是, 对程序的净影响可以与按值传递或按引用传递相同:

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

正如您所看到的, 这对技术几乎与定义中的技术相同,只是具有间接级别:只需将“variable”替换为“引用对象”。

它们之间没有达成一致的名称,这导致了扭曲的解释,例如“价值呼叫,其中价值是参考”。 1975年,芭芭拉·莱斯科夫(Barbara Liskov)建议使用“ 按对象分享 ”(或有时只是“分享呼叫”)一词,尽管它从未完全流行起来。而且,这些短语都没有与原始对平行。难怪旧条款最终被重复使用,导致混乱。4


_ note _ :很长一段时间,这个答案常说:

说我想和你分享一个网页。如果我告诉你URL,我通过引用传递。您可以使用该URL查看我可以看到的同一网页。如果该页面已更改,我们都会看到更改。如果您删除了URL,那么您所做的只是破坏对该页面的引用 - 您不会删除实际页面本身。

如果我打印出页面并给你打印输出,我就会超值。您的页面是原始的断开连接的副本。您不会看到任何后续更改,并且您所做的任何更改(例如,在您的打印输出上涂鸦)都不会显示在原始页面上。如果您销毁打印输出,则实际上已经销毁了对象的副本 - 但原始网页保持不变。

这是大多数正确除外“引用”的狭义含义 - 它既是临时的又是隐含的(它不是必须的,但是显式和/或持久性是附加特征,而不是传递参考语义的一部分,如上所述)。更接近的类比是给你一份文件副本,邀请你去处理原件。


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

2相当数量的老年人也支持它

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

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

1011
Dylan Beattie

这是一种如何将参数传递给函数的方法。通过引用传递意味着被调用函数的参数将与调用者的传递参数相同(不是值,而是标识 - 变量本身)。按值传递意味着被调用函数的参数将是调用者传递参数的副本。值将是相同的,但身份 - 变量 - 是不同的。因此,在一种情况下由被调用函数完成的参数的改变改变了传递的参数,而在另一种情况下,仅改变被调用函数中的参数的值(这只是一个拷贝)。快点:

  • 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

132
Johannes Schaub - litb

这里的许多答案(特别是最受高度评价的答案)实际上是不正确的,因为他们误解了“引用的呼叫”的真正含义。这是我试图解决问题的尝试。

TL; DR

简单来说:

  • 按值调用表示您将 values 作为函数参数传递
  • 通过引用调用表示您将 variables 作为函数参数传递

用比喻来说:

  • 按价值调用是在哪里 我在一张纸上写下来并把它交给你 。也许它是一个URL,也许它是战争与和平的完整副本。无论它是什么,它都放在我给你的一张纸上,所以现在它实际上是 你的纸 。你现在可以自由地在那张纸上乱涂乱画,或者用那张纸在其他地方找到一些东西并随便弄乱,无论如何。
  • 通过引用调用是什么时候 我给你的笔记本上写下了一些东西 。你可以在我的笔记本上乱涂乱画(也许我想要你,也许我不会),然后我把笔记本放在你的笔记本里,随便放一些涂鸦。此外,如果您或我写的是有关如何在其他地方找到某些内容的信息,您或我可以去那里并提供相关信息。

什么是“按价值呼叫”和“按参考呼叫”不要的意思

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

引用类型的概念对应于一个URL:它本身就是一条信息,它是reference(a pointer,如果你愿意的话)其他信息。您可以在不同的地方拥有许多URL副本,并且不会更改所有链接到的网站;如果网站更新,那么每个URL副本仍将导致更新的信息。相反,在任何一个地方更改URL都不会影响URL的任何其他书面副本。

请注意,C++有一个“引用”的概念(例如int&),它是 not 类似Java和C#的“引用类型”,但是喜欢“按引用调用”。 Java和C#的“引用类型”和Python中的all类型就像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行声明的function(Java坚持使用所有函数方法,但概念是相同的而不失一般性; C和C++区分声明和定义,我不会去进入这里)
  • param形式参数foo,也在第1行声明
  • arg变量,特别是函数bar局部变量,在第2行声明并初始化
  • arg也是参数到第3行foo的特定调用

这里有两个非常重要的概念要区分。第一个是变量

  • Avalue 在语言中评估表达式的结果 。例如,在上面的bar函数中,在int arg = 1;行之后,表达式arg具有value1
  • A变量是a 容器的值 。变量可以是可变的(这是大多数类C语言中的默认值),只读(例如使用Java的final或C#的readonly声明)或深度不可变(例如使用C++的const)。

要区分的另一个重要概念是参数参数

  • A参数(也称为形式参数)是变量,在调用函数时必须由调用者提供。
  • argument是一个value,它由函数的调用者提供,以满足该函数的特定形式参数

按价值呼叫

按值调用中,函数的形式参数是为函数调用新创建的变量,并使用其参数的values进行初始化。

这与使用值初始化任何其他类型的变量完全相同。例如:

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

也就是说,call by value的定义特征是被调用者(在这种情况下为foo)接收values作为参数,但是它有自己独立的变量来自调用者变量的那些值(在本例中为bar)。

回到我上面的比喻,如果我bar并且你是foo,当我打电话给你时,我递给你一张写有的纸。你把那张纸叫做param。该值是我在笔记本中写入的值(我的局部变量)的 copy ,在我调用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相同的value,它实际上是arg(只是使用不同的名称),因此bar可以观察到arg已经递增。

请注意,这是 not 如何使用Java,JavaScript,C,Objective-C,Python或几乎任何其他流行语言。这意味着这些语言是 not 通过引用调用,它们是按值调用的。

附录:通过对象共享调用

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

如果该引用类型(即指针)指向的是 mutable 那么可能会产生一个有趣的效果:您可以修改指向的值,并且调用者可以观察到指向值的更改,即使调用者无法观察指针本身的变化。

再次借用URL的类比,如果我们关心的是网站而不是URL,我给你一个网站的URL copy 这一事实并不是特别有趣。您在URL副本上涂鸦并不会影响我的URL副本这一事实并不是我们关心的事情(事实上,在Java和Python等语言中,“URL”或引用类型值可以根本没有被修改,只有它指向的东西可以)。

Barbara Liskov在发明CLU编程语言(具有这些语义)时,意识到现有术语“按值调用”和“按引用调用”对于描述这种新语言的语义并不是特别有用。所以她发明了一个新术语: 通过对象共享调用

在讨论技术上按值调用的语言时,但是使用的常用类型是引用或指针类型(即:几乎所有现代命令式,面向对象或多范式编程语言),我发现它不那么令人困惑。简单地避免谈论按值调用通过引用调用。坚持 通过对象共享调用 (或简单地 按对象调用 )并且没有人会感到困惑。 :-)

66
Daniel Pryden

在理解2个术语之前,你 _必须_ 了解以下内容。每个对象都有两件可以区分的东西。

  • 它的价值。
  • 它的地址。

所以,如果你说employee.name = "John"

知道name有2件事。它的值是"John",它在内存中的位置是一些十六进制数,可能是这样的:0x7fd5d258dd00

根据语言的体系结构或对象的type(class,struct等),您可以传输"John"0x7fd5d258dd00

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

有关这方面的更多信息,我建议您阅读 解除引用指针 以及 为什么选择struct(值类型)而不是类(引用类型)

53
Honey

这是一个例子:

#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
}
52
pyon

获取此信息的最简单方法是在Excel文件上。例如,假设您在单元格A1和B1中有两个数字5和2,并且您希望在第三个单元格中找到它们的总和,假设为A2。你可以用两种方式做到这一点。

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

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

24
Than Skourtan

当你通过ref传递时,你基本上是传递一个指向变量的指针。通过值传递您传递变量的副本。在基本用法中,这通常意味着传递给变量的变量将被视为调用方法并且通过值传递它们不会。

18
Craig

传递值发送存储在您指定的变量中的数据的COPY,通过引用传递直接链接到变量本身。因此,如果您通过引用传递变量然后更改传递给它的块内的变量,则原始变量将被更改。如果您只是按值传递,原始变量将无法通过您传入的块进行更改,但您将获得调用时包含的任何内容的副本。

11
MetaGuru

它们之间的主要区别在于值类型变量存储值,因此在方法调用中指定值类型变量会将该变量值的副本传递给该方法。引用类型变量存储对象的引用,因此将引用类型变量指定为参数会向方法传递引用该对象的实际引用的副本。即使引用本身是通过值传递的,该方法仍然可以使用它接收的引用来与原始对象进行交互,并可能修改原始对象。类似地,当通过return语句从方法返回信息时,该方法返回存储在value-type变量中的值的副本或存储在reference-type变量中的引用的副本。返回引用时,调用方法可以使用该引用与引用的对象进行交互。因此,实际上,对象总是通过引用传递。

在c#中,要通过引用传递变量,以便被调用的方法可以修改变量,C#提供关键字ref和out。将ref关键字应用于参数声明允许您通过引用将变量传递给方法 - 被调用的方法将能够修改调用者中的原始变量。 ref关键字用于已在调用方法中初始化的变量。通常,当方法调用包含未初始化的变量作为参数时,编译器会生成错误。在关键字out之前添加参数会创建输出参数。这向编译器指示参数将通过引用传递给被调用的方法,并且被调用的方法将为调用者中的原始变量赋值。如果方法未在每个可能的执行路径中为输出参数赋值,则编译器会生成错误。这还可以防止编译器为作为参数传递给方法的未初始化变量生成错误消息。方法只能通过return语句向其调用者返回一个值,但可以通过指定多个输出(ref和/或out)参数来返回许多值。

请参阅此处的c#讨论和示例 链接文本

5
Tina Endresen

按值传递 - 该函数复制变量并使用副本(因此它不会更改原始变量中的任何内容)

通过引用传递 - 该函数使用原始变量,如果更改另一个函数中的变量,它也会更改原始变量。

示例(复制并使用/自己试试看看):

#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;
}

保持简单,窥视。文字墙可能是一个坏习惯。

5
user326964

例子:

class Dog 
{ 
public:
    barkAt( const std::string& pOtherDog ); // const reference
    barkAt( std::string pOtherDog ); // value
};

const &通常是最好的。您不会受到施工和破坏的惩罚。如果引用不是const,则您的接口建议它将更改传入的数据。

3
DEADFACE

简而言之,Passed by value是什么,并通过引用传递它是什么。

如果你的值是VAR1 =“快乐的家伙!”,你只会看到“快乐的家伙!”。如果VAR1变为“Happy Gal!”,你就不会知道。如果它通过引用传递,并且VAR1更改,您将。

2
Monster

按值传递意味着如何通过使用参数将值传递给函数。在传递值时,我们复制存储在我们指定的变量中的数据,并且比复制数据时通过引用传递更慢。我们对复制的数据进行更改,原始数据不受影响。通过引用传递或通过地址传递我们发送直接链接到变量本身。或将指针传递给变量。它的消耗时间更短

1
abhinisha thakur

如果您不想在将原始变量传递给函数后更改原始变量的值,则应使用“ 按值传递 ”参数构造该函数。

然后该函数将只有值而不是传入的变量的地址。如果没有变量的地址,函数内部的代码就不能改变从函数外部看到的变量值。

但是如果你想给函数赋值 能够改变变量的值 从外面看,你需要使用 传递引用 。由于值和地址(引用)都在函数内传递并可用。

1
Stanley

下面是一个演示 传递值 - 指针值 - 参考 之间差异的示例

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的答案,用简单的词语解释它。

0
BugShotGG