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

时间:2008-12-17 01:49:29

标签: language-agnostic pass-by-reference pass-by-value

之间有什么区别
  1. 通过引用传递的参数
  2. 按值传递的参数?
  3. 你能给我一些例子吗?

16 个答案:

答案 0 :(得分:1014)

答案 1 :(得分:133)

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

  • Java仅支持按值传递。始终复制参数,即使复制对象的引用时,被调用函数中的参数也将指向同一个对象,并且将在调用者中看到对该对象的更改。由于这可能令人困惑,here就是Jon Skeet对此所说的。
  • C#支持按值传递并通过引用传递(在调用者和被调用函数中使用的关键字ref)。 Jon Skeet对这个here也有很好的解释。
  • 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

答案 2 :(得分:66)

这里的许多答案(尤其是最受高度评价的答案)实际上是不正确的,因为他们误解了通过引用和#34;真正意思。这是我试图解决问题的尝试。

TL; DR

简单来说:

  • 按值调用表示您将作为函数参数传递
  • 按引用调用表示您将变量作为函数参数传递

用隐喻术语:

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

什么"按价值呼叫"并且"通过引用呼叫" 意味着

请注意,这两个概念完全独立且与 引用类型 的概念正交(在Java中,所有类型都是Object的子类型,以及C#中的所有class类型),或C中的 指针类型 的概念(在语义上等同于Java' s" ;引用类型",只是使用不同的语法)。

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

请注意,C ++的概念是"引用" (例如int&,如Java和C#"参考类型",但 类似"以参考号码打电话#34; 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#' s readonly声明)或深度不可变(例如,使用C ++' s 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);
}

在这种情况下,paramarg不具有相同的,实际上 {{1} (仅使用不同的名称),arg可以观察到bar已经增加。

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

附录:通过对象共享调用

如果您拥有按值调用,但实际值是引用类型指针类型,那么&#34 ;值&#34;本身并不是很有趣(例如在C中它只是平台特定大小的整数) - 有趣的是指向的值。< / p>

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

再次借用URL的类比,如果我们关心的是网站,而不是网站,那么我向网站提供了复制网址的事实并不是特别有趣URL。您对URL副本进行涂鸦的事实并不会影响我的URL副本,这不是我们关心的事情(实际上,在Java和Python等语言中,&#34; URL&#34 ;或者引用类型值,根本不能被修改,只有它指向的东西可以)。

Barbara Liskov,当她发明了CLU编程语言(具有这些语义)时,意识到现有术语&#34;按值调用&#34;并且&#34;通过引用呼叫&#34;对于描述这种新语言的语义并不是特别有用。所以她发明了一个新术语:call by object sharing

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

答案 3 :(得分:55)

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

  • 它的价值。
  • 地址。

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

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

根据语言的体系结构或对象的类型(类,结构等),您可以转移"John"0x7fd5d258dd00

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

有关详情,建议您阅读dereferencing a pointer以及why choose struct (value type) over class (reference type)

答案 4 :(得分:52)

以下是一个例子:

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

答案 5 :(得分:25)

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

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

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

答案 6 :(得分:18)

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

答案 7 :(得分:11)

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

答案 8 :(得分:6)

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

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

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

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

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

答案 9 :(得分:5)

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

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

请参阅此处的c#讨论和示例link text

答案 10 :(得分:3)

示例:

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

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

答案 11 :(得分:2)

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

如果你的值是VAR1 =“快乐的家伙!”,你只会看到“快乐的家伙!”。如果VAR1变为“Happy Gal!”,你就不会知道。如果它通过引用传递,并且VAR1发生了变化,那么你将会这样做。

答案 12 :(得分:1)

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

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

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

答案 13 :(得分:1)

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

答案 14 :(得分:0)

下面是一个示例,演示传递值 - 指针值 - 参考

之间的差异
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
}

“通过引用传递”方法具有重要限制。如果参数声明为通过引用传递(因此它前面带有&amp;符号),则其对应的实际参数必须是变量

引用“通过值传递”形式参数的实际参数一般可以是表达式,因此不仅可以使用变量,还可以使用文字甚至函数调用&#39结果。

该函数无法将值放在变量以外的值中。它不能为文字指定新值或强制表达式更改其结果。

PS:您还可以在当前主题中检查Dylan Beattie的答案,用简单的词语解释它。

答案 15 :(得分:0)

1.按值传递/按值调用

   void printvalue(int x) 
   {
       x = x + 1 ;
       cout << x ;  // 6
   }

   int x = 5;
   printvalue(x);
   cout << x;    // 5

在按值调用中,当您将值传递给 printvalue(x) 即参数 5 时,它会被复制到 void printvalue(int x)。现在,我们有两个不同的值 5 和复制的值 5,这两个值存储在不同的内存位置。因此,如果您在 void printvalue(int x) 内进行任何更改,它不会反映回参数。

2.按引用传递/按引用调用

   void printvalue(int &x) 
   {
      x = x + 1 ;
      cout << x ; // 6
   }

   int x = 5;
   printvalue(x);
   cout << x;   // 6

在引用调用中,只有一个区别。我们使用 & 即地址运算符。通过做
void printvalue(int &x) 我们指的是 x 的地址,它告诉我们它指的是同一个位置。因此,函数内部所做的任何更改都会反映到外部。

既然你在这里,你也应该知道......

3.通过指针传递/通过地址调用

   void printvalue(int* x) 
   {
      *x = *x + 1 ;
      cout << *x ; // 6
   }

   int x = 5;
   printvalue(&x);
   cout << x;   // 6

在按地址传递时,指针int* x保存着传递给它的地址printvalue(&x)。因此,函数内部所做的任何更改都会反映到外部。