按引用传递还是按值传递?[已结束]

Pass by reference or pass by value? [closed]

提问人: 提问时间:8/5/2008 最后编辑:8 revs, 4 users 65%Sven 更新时间:2/16/2019 访问量:19944

问:

在学习一种新的编程语言时,您可能会遇到的一个可能的障碍是该语言在默认情况下是按值传递还是按引用传递的问题。

所以这是我向你们所有人提出的问题,用你们最喜欢的语言,它实际上是如何完成的?可能存在的陷阱是什么?

当然,你最喜欢的语言可以是你曾经玩过的任何语言:流行的晦涩的、深奥的、新的旧的......

与语言无关的 OOP 参数 按引用传递值传递

评论

0赞 Mat 8/5/2008
这里已经有一个答案可以解释 PHP5 中的情况。
4赞 Andrew Grimm 6/10/2010
哇 - 这个问题有一个指向测试版服务器的链接。我想你会测试修复链接。
0赞 Hamas are war criminals 12/26/2021
为什么关闭?

答:

31赞 sven 8/5/2008 #1

这是我自己对 Java 编程语言的贡献。

首先,一些代码:

public void swap(int x, int y)
{
  int tmp = x;
  x = y;
  y = tmp;
}

调用此方法将导致以下结果:

int pi = 3;
int everything = 42;

swap(pi, everything);

System.out.println("pi: " + pi);
System.out.println("everything: " + everything);

"Output:
pi: 3
everything: 42"

即使使用“真实”对象也会显示类似的结果:

public class MyObj {
    private String msg;
    private int number;

    //getters and setters
    public String getMsg() {
        return this.msg;
    }


    public void setMsg(String msg) {
        this.msg = msg;
    }


    public int getNumber() {
        return this.number;
    }


    public void setNumber(int number) {
        this.number = number;
    }

    //constructor
    public MyObj(String msg, int number) {
        setMsg(msg);
        setNumber(number);
    }
}

public static void swap(MyObj x, MyObj y)
{
    MyObj tmp = x;
    x = y;
    y = tmp;
}

public static void main(String args[]) {
    MyObj x = new MyObj("Hello world", 1);
    MyObj y = new MyObj("Goodbye Cruel World", -1); 

    swap(x, y);

    System.out.println(x.getMsg() + " -- "+  x.getNumber());
    System.out.println(y.getMsg() + " -- "+  y.getNumber());
}


"Output:
Hello world -- 1
Goodbye Cruel World -- -1"

因此,很明显,Java 值传递其参数,因为 pieverything 的值以及 MyObj 对象不会交换。 请注意,“按值”是 Java 中将参数传递给方法的唯一方法。(例如,像 C++ 这样的语言允许开发人员在参数类型后使用“&”通过引用传递参数)

现在是棘手的部分,或者至少是会让大多数新的 Java 开发人员感到困惑的部分:(借自 Javaworld
原作者:Tony Sintes

public void tricky(Point arg1, Point arg2)
{
    arg1.x = 100;
    arg1.y = 100;
    Point temp = arg1;
    arg1 = arg2;
    arg2 = temp;
}
public static void main(String [] args)
{
    Point pnt1 = new Point(0,0);
    Point pnt2 = new Point(0,0);
    System.out.println("X: " + pnt1.x + " Y: " +pnt1.y); 
    System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);
    System.out.println(" ");
    tricky(pnt1,pnt2);
    System.out.println("X: " + pnt1.x + " Y:" + pnt1.y); 
    System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);  
}


"Output
X: 0 Y: 0
X: 0 Y: 0
X: 100 Y: 100
X: 0 Y: 0"

Tricky 成功更改了 pnt1 的值! 这意味着对象是通过引用传递的,但事实并非如此! 正确的语句是:对象引用按值传递。

更多来自 Tony Sintes:

该方法成功地将 pnt1 的值,即使它是 按值传递;但是,交换 pnt1 和 pnt2 失败!这是主要的 混乱的根源。在 main() 中 方法,PNT1 和 PNT2 仅此而已 比对象引用。当你通过时 pnt1 和 pnt2 添加到 Tricky() 方法中, Java 按值传递引用 就像任何其他参数一样。这 表示传递给 方法实际上是 原始参考资料。下图 1 显示两个指向 Java 传递 对象添加到方法中。

figure 1
(来源: javaworld.com

结论或长话短说:

  • Java 按值传递参数
  • “按值”是 Java 中将参数传递给方法的唯一方法
  • 使用作为参数给出的对象的方法将更改对象,因为引用指向原始对象。(如果该方法本身更改了某些值)

有用的链接:

4赞 DShook 8/5/2008 #2

按值

  • 比引用慢,因为系统必须复制参数
  • 仅用于输入

通过引用

  • 速度更快,因为只传递了一个指针
  • 用于输入输出
  • 如果与全局变量结合使用,可能会非常危险

评论

0赞 MPelletier 11/22/2009
并没有真正回答这个问题,但 +1 用于陈述事实。
5赞 Matthew Schinckel 8/5/2008 #3

别忘了还有 pass by name,以及 pass by value-result

Pass by value-result 类似于 pass by value,但增加了一个方面,即在作为参数传递的原始变量中设置值。在某种程度上,它可以避免对全局变量的干扰。在分区内存中,它显然更好,其中通过引用传递可能会导致页面错误(参考)。

按名称传递意味着仅在实际使用值时计算值,而不是在过程开始时计算值。Algol 使用按名称传递,但一个有趣的副作用是很难编写交换过程(参考)。此外,每次访问按名称传递的表达式时都会重新计算,这也可能会产生副作用。

20赞 sven 8/6/2008 #4

这是另一篇关于 c# 编程语言的文章

C# 按值传递其参数(默认情况下)

private void swap(string a, string b) {
  string tmp = a;
  a = b;
  b = tmp;
}

因此,调用此版本的 swap 将没有结果:

string x = "foo";
string y = "bar";
swap(x, y);

"output: 
x: foo
y: bar"

但是,与 Java 不同的是,C# 确实为开发人员提供了通过引用传递参数的机会,这是通过在参数类型之前使用“ref”关键字来完成的:

private void swap(ref string a, ref string b) {
  string tmp = a;
  a = b;
  b = tmp;
} 

此交换更改引用参数的值:

string x = "foo";
string y = "bar";
swap(x, y);

"output: 
x: bar
y: foo"

C# 也有一个 out 关键字,ref 和 out 之间的区别很微妙。来自 MSDN:

获取 out 参数的方法的调用方不需要 赋值给作为 调用前的 out 参数; 但是,被叫方必须 分配给 out 参数之前 返回。

相比之下,ref 参数最初被认为是由 被叫方。因此,被叫方不是 需要在使用前分配给 ref 参数。Ref 参数 传入和传出 方法。

一个小陷阱是,就像在 Java 中一样,按 value 传递的对象仍然可以使用其内部方法进行更改

结论:

  • 默认情况下,C# 按值传递其参数
  • 但是,如果需要,也可以使用 ref 关键字通过引用传递参数
  • 来自 value 传递的参数的内部方法将更改对象(如果该方法本身更改了某些值)

有用的链接:

6赞 Karl Seguin 8/7/2008 #5

这里对 .NET 有一个很好的解释

很多人对引用对象实际上是按值传递的感到惊讶(在 C# 和 Java 中)。它是堆栈地址的副本。这可以防止方法更改对象实际指向的位置,但仍允许方法更改对象的值。在 C# 中,可以逐个引用传递引用,这意味着您可以更改实际对象指向的位置。

2赞 grom 8/11/2008 #6

PHP 也是按值传递的。

<?php
class Holder {
    private $value;

    public function __construct($value) {
        $this->value = $value;
    }

    public function getValue() {
        return $this->value;
    }
}

function swap($x, $y) {
    $tmp = $x;
    $x = $y;
    $y = $tmp;
}

$a = new Holder('a');
$b = new Holder('b');
swap($a, $b);

echo $a->getValue() . ", " . $b->getValue() . "\n";

输出:

a b

然而,在 PHP4 中,对象被视为基元。这意味着:

<?php
$myData = new Holder('this should be replaced');

function replaceWithGreeting($holder) {
    $myData->setValue('hello');
}

replaceWithGreeting($myData);
echo $myData->getValue(); // Prints out "this should be replaced"
20赞 yukondude 8/24/2008 #7

Python 使用按值传递,但由于所有这些值都是对象引用,因此最终效果类似于按引用传递。但是,Python 程序员更多地考虑对象类型是变的还是不可变的。可变对象可以就地更改(例如,字典、列表、用户定义的对象),而不可变对象则不能(例如,整数、字符串、元组)。

下面的示例演示一个函数,该函数传递了两个参数、一个不可变字符串和一个可变列表。

>>> def do_something(a, b):
...     a = "Red"
...     b.append("Blue")
... 
>>> a = "Yellow"
>>> b = ["Black", "Burgundy"]
>>> do_something(a, b)
>>> print a, b
Yellow ['Black', 'Burgundy', 'Blue']

该行仅为字符串值创建一个本地名称 ,对传入的参数(现在已隐藏,因为从那时起必须引用本地名称)没有影响。赋值不是就地操作,无论参数是可变的还是不可变的。a = "Red"a"Red"a

该参数是对可变列表对象的引用,该方法执行列表的就地扩展,并附加新的字符串值。b.append()"Blue"

(由于字符串对象是不可变的,因此它们没有任何支持就地修改的方法。

一旦函数返回,对 has 的重新赋值就没有效果,而 的扩展清楚地显示了按引用传递样式的调用语义。ab

如前所述,即使参数 for 是可变类型,函数内的重新赋值也不是就地操作,因此传递的参数的值不会发生变化:a

>>> a = ["Purple", "Violet"]
>>> do_something(a, b)
>>> print a, b
['Purple', 'Violet'] ['Black', 'Burgundy', 'Blue', 'Blue']

如果不希望被调用的函数修改列表,则应改用不可变元组类型(由文本形式的括号标识,而不是方括号),该类型不支持就地方法:.append()

>>> a = "Yellow"
>>> b = ("Black", "Burgundy")
>>> do_something(a, b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in do_something
AttributeError: 'tuple' object has no attribute 'append'

评论

2赞 Barry Kelly 2/13/2010
从我在网络上快速浏览 Python 参数传递讨论的内容来看,大多数 Python 人不知道通过引用传递是什么意思。Python 绝对是按值传递的。值的不变性是一个单独的问题。还有一些人对字典绑定感到困惑,他们不明白绑定到字典中值引用的符号与包含值引用的变量是一回事。“按引用传递”是指将引用传递到变量,而不是值;或者用符号术语来说,你传递一个绑定的可变名称。
4赞 MPelletier #8

关于 J,虽然只有 AFAIK,按值传递,但有一种通过引用传递的形式可以移动大量数据。您只需将称为区域设置的内容传递给动词(或函数)。它可以是类的实例,也可以只是一个泛型容器。

spaceused=: [: 7!:5 <
exectime =: 6!:2
big_chunk_of_data =. i. 1000 1000 100
passbyvalue =: 3 : 0
    $ y
    ''
)
locale =. cocreate''
big_chunk_of_data__locale =. big_chunk_of_data
passbyreference =: 3 : 0
    l =. y
    $ big_chunk_of_data__l
    ''
)
exectime 'passbyvalue big_chunk_of_data'
   0.00205586720663967
exectime 'passbyreference locale'
   8.57957102144893e_6

明显的缺点是,您需要在被调用的函数中以某种方式知道变量的名称。但是这种技术可以毫不费力地移动大量数据。这就是为什么,虽然从技术上讲不是通过引用,但我称之为“差不多”。

-1赞 oosterwal #9

默认情况下,ANSI/ISO C 使用其中一种 - 这取决于您如何声明函数及其参数。

如果将函数参数声明为指针,则函数将是按引用传递的,如果将函数参数声明为非指针变量,则函数将按值传递。

void swap(int *x, int *y);   //< Declared as pass-by-reference.
void swap(int x, int y);     //< Declared as pass-by-value (and probably doesn't do anything useful.)

如果创建的函数返回指向在该函数中创建的非静态变量的指针,则可能会遇到问题。以下代码的返回值是未定义的 -- 无法知道分配给函数中创建的临时变量的内存空间是否被覆盖。

float *FtoC(float temp)
{
    float c;
    c = (temp-32)*9/5;
    return &c;
}

但是,您可以返回对静态变量的引用或在参数列表中传递的指针。

float *FtoC(float *temp)
{
    *temp = (*temp-32)*9/5;
    return temp;
}

评论

6赞 Jörg W Mittag 2/25/2012
-1.这是错误的。C 始终是按值传递的。如果将参数声明为 ,则将按值传递,如果将参数声明为 ,则将按值传递,如果将参数声明为指针,则指针将按值传递,但永远不会有按引用传递。intintfloatfloat
0赞 oosterwal 4/7/2012
@JörgWMittag:在 ANSI/ISO C 中,指针是引用。
0赞 Aluan Haddad 4/24/2016
大错特错。两个交换函数中的参数都是值。区别在于类型之一。第一个签名定义了指向 int 的两个指针参数,第二个签名定义了 int 类型的参数。
8赞 LeoNerd #10

由于我还没有看到Perl的答案,所以我想我会写一个。

在后台,Perl 有效地作为传递引用工作。作为函数调用参数的变量以引用方式传递,常量作为只读值传递,表达式的结果作为临时值传递。通常的习语是通过列表赋值来构造参数列表的,或者倾向于对用户隐藏这一点,从而呈现出按值传递的外观:@_shift

sub incr {
  my ( $x ) = @_;
  $x++;
}

my $value = 1;
incr($value);
say "Value is now $value";

这将打印,因为 递增了函数中声明的词法变量,而不是传入的变量。这种按值传递的样式通常是大多数时候需要的,因为修改其参数的函数在 Perl 中很少见,因此应避免使用这种样式。Value is now 1$x++incr()

但是,如果出于某种原因特别需要这种行为,则可以通过直接对数组的元素进行操作来实现,因为它们将是传递到函数中的变量的别名。@_

sub incr {
  $_[0]++;
}

my $value = 1;
incr($value);
say "Value is now $value";

这次它将打印,因为表达式递增了实际变量。其工作方式是,在引擎盖下不是像大多数其他数组那样的真实数组(例如由 ),而是直接从传递给函数调用的参数中构建的元素。这允许您在需要时构造按引用传递语义。作为普通变量的函数调用参数按原样插入到此数组中,更复杂的表达式的常量或结果将作为只读临时变量插入。Value is now 2$_[0]++$value@_my @array

然而,在实践中这样做是极其罕见的,因为Perl支持引用值;也就是说,引用其他变量的值。通常,通过传递对变量的引用来构造对变量有明显副作用的函数要清楚得多。这向呼叫站点的读者清楚地表明,按引用传递语义是有效的。

sub incr_ref {
  my ( $ref ) = @_;
  $$ref++;
}

my $value = 1;
incr(\$value);
say "Value is now $value";

在这里,运算符以与 C 中的 address-of 运算符大致相同的方式生成引用。\&

5赞 newacct #11

无论您所说的按值传递还是按引用传递,都必须在不同语言之间保持一致。跨语言使用的最常见和最一致的定义是,通过按引用传递,您可以“通常”将变量传递给函数(即不显式采用地址或类似的东西),并且函数可以分配给(而不是改变)函数内部的参数,它将具有与在调用范围内分配给变量相同的效果。

从这个角度来看,语言分为以下几类;每个组都具有相同的传递语义。如果你认为两种语言不应该放在同一个组中,我挑战你想出一个例子来区分它们。

绝大多数语言,包括 C、JavaPythonRubyJavaScriptSchemeOCamlStandard MLGoObjective-CSmalltalk 等,都只是按值传递的。传递指针值(某些语言称其为“引用”)不计为按引用传递;我们只关心传递的事物,指针,而不是指向的事物。

C++C#PHP 等语言与上述语言一样,默认情况下是按值传递的,但函数可以使用 或 显式声明参数为按引用传递。&ref

Perl 始终是按引用传递的;然而,在实践中,人们几乎总是在获取值后复制它,从而以按值传递的方式使用它。

评论

0赞 fishinear 3/2/2014
C 不应该与 Java 等属于同一组,因为在 C 中,可以获取变量的地址并将其传递给函数。这使得被调用的函数可以更改变量的值。也就是说,可以在 C 语言中进行引用传递。
1赞 newacct 3/3/2014
@fishinear:没有。这就是传递价值。它正在复制传递的值(指针)。
1赞 newacct 3/3/2014
@fishinear:没有。Pass-by-value 和 pass-by-reference 是处理语法结构的语义概念。它与“概念”无关。C 或 Objective-C 中没有引用传递。
1赞 newacct 3/4/2014
@fishinear:你的“概念”没有明确定义。事实上,在任何语言中都可以进行“概念性引用传递”。在 Java 中:简单。只需使用 1 个元素的数组来代替所有变量即可。若要读取变量,请访问元素 0。若要写入变量,请写入元素 0。当你“通过引用传递”时,只需传递数组。
1赞 newacct 3/5/2014
@fishinear:再说一遍,你不是“把它作为参数传递”。放一个不是一个“技术细节”——它是最重要的细节。传递引用是一个非常技术性的术语,涉及语法和语义。只有当你直接传递变量时,某些东西才是“通过引用传递”的,而你没有对它做额外的事情。如果你不想对这些事情严谨,你就不应该使用这些术语。从技术上讲,C 中没有引用传递。这是众所周知的,没有争议。只需在 StackOverflow 上搜索即可。&