为什么程序员说"pass by reference"真的很"passing references by value?"为什么这很重要?

Why do programmers say that "pass by reference" is really "passing references by value?" Why is that important?

本文关键字:为什么 by value references 真的 程序员 pass reference passing      更新时间:2023-10-16

我知道C和C++中通过引用传递的整个概念,以及在Java中仅按值传递的类似概念。但从某种角度来看,一切都是按价值传递的,不是吗?在 C 中,我们将变量的指针传递给函数。所以我们只是将引用的值传递给函数。这就是我们说Java不支持按引用传递的原因,因为我们只是将引用变量的值传递给函数。所以我们按值传递引用。虽然在C++有一种通过引用传递的方法,因为我们可以传递参数,并且该函数将使用这种格式使用相同的内存位置

void swap(int &x, int &y)

但是在 C 中通过指针通过引用传递只是按值传递指针。

void swap(int* x, int* y)

我知道这个问题可能看起来有点愚蠢,但我觉得我的整个概念中有一个很大的漏洞。那么按引用调用的实际定义是什么,这只是另一个上下文中按值调用的别名吗?

通过引用传递意味着被调用函数的参数将与调用方传递的参数相同(不是值,而是标识 - 变量本身)。按值传递意味着被调用函数的参数将是调用方传递的参数的副本。值将相同,但标识 - 变量 - 不同。因此,在一种情况下,被调用函数对参数的更改会更改传递的参数,而在另一种情况下,只是更改被调用函数中参数的值(这只是一个副本)。

C++中的简单示例

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

C++由标准的模错误定义。 C++ 标准没有指定如何实现引用,也没有指定如何将它们传递给函数。 它也没有定义一般的调用约定,也没有定义指针布局、堆栈、堆或无数其他实现细节。

相反,它试图定义C++代码的含义。

引用最终成为其他值的别名。 实现它们的最常见方法是作为底层指针:但由于无法访问该指针值,也无法通过它访问的副作用,这使得编译器很容易完全消除引用的存在,而只是直接使用引用的值。

如果编译器无法执行此操作,它通常会传递指针或等效项。

尽管经常被实现为指针,但它仍然不同,因为与它交互的语义是不同的:它们不能被重新拔插,它们不能被占用它们的地址,并且它们不能被取消初始化(null 或等效)。

当然,这些规则可以通过未定义的行为来打破。

两个要点:

  • 在 C 中没有通过引用调用。
  • 按值传递和按引用传递是不同的。它们不一样。

按值传递:被调用的函数在堆栈中创建一组新的变量,并将参数的值复制到其中。

通过引用传递

不是将值传递给被调用的函数,而是传递对原始变量的引用/指针。

为什么程序员说"按引用传递

"实际上是"按值传递引用"?

在传递指向原始变量的引用/指针时,实际上对象/地址是按值传递的。因此,您可以说按引用传递是按值传递引用,但这并不意味着按引用传递按值传递的伪名称。两者之间的区别在这个答案中得到了很好的解释。我正在复制摘录:

如果我告诉你网址,我就是通过引用传递。您可以使用该URL查看我可以看到的同一网页。如果该页面发生更改,我们都会看到更改。如果您删除 URL,您所做的只是销毁对该页面的引用 - 您不会删除实际页面本身。

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

如果不给出"引用"的定义,就不能说"按引用传递"实际上是"按值传递引用"。

Java 中,该语句为 true(起初大约为真),因为在函数中,您可以更改作为引用的形式参数的值,而无需更改调用中传递的参数的值:

void f(Object param) {
  param = null;
}
void g() {
  Object o = new Object();
  System.out.println(o);
  f(o);
  System.out.printn(o);
}

每个人都知道这两个打印语句将给出完全相同的结果。事实上,在Java中没有通过引用传递,只有可以传递的引用;传递参数的方法只有一种:按值。

C++,这真的很不同。更改函数内部引用参数的值时,将修改调用中传递的参数:

void f(int &param) {
  param = 0;
}
void g() {
  int i=12;
  cout << i << endl;
  f(i);
  cout << i << endl;
}

每个C++程序员都知道第二次打印将显示 0(而第一次打印为 12)。

因此,在C++中,您不能说"按引用传递"是"按值传递引用"。在C++引用没有价值,它们只是与内存块相关联的名称(标准部分 8.3.2 引用:[ 注意:引用可以被认为是对象的名称。f()内部只有一个变量有两个名称。当然,您可能会反对大多数引用实现都是隐藏指针C++因此该语句可能是真的;但您应该知道,在许多情况下,可以避免使用隐藏指针来实现引用(标准部分 8.3.2/7 引用。未指定引用是否需要存储)...因此,在C++中,该声明无效。C++实际上有两种传递参数的方式:按值传递和按引用传递参数。