在C和C++中,“按引用传递”有什么区别?

40

「传递引用」这个词汇在C和C++开发者中都被使用,但似乎它们所表示的意义不同。那么这个模糊的短语在每种语言中确切的区别是什么?


尽管有一些类似的问题和答案,但我很惊讶没有找到一个真正涵盖这个问题的答案(特别是C和C++之间差异的深入讨论)。这是一个常见的误解,特别是对于那些以某种原因同时学习C和C++或从一个语言转换到另一个语言的人。我认为,许多初学者在C和C++编程中遇到的问题源于对每种语言发生了什么的混淆。 - Joseph Mansfield
2
你是否曾经读过C++中“按引用传递/按值传递”的最高赞答案(https://dev59.com/i3RC5IYBdhLWcg3wFdJx)? - Jesse Good
1
@JesseGood 我已经查过了,但我想着重讲述这两种语言之间的差异,以及在不同情境下应该使用哪种短语。虽然答案中有一些重复的内容,但我认为我的问题还是很有区别性的。 - Joseph Mansfield
请注意链接答案中对绑定临时对象的区分,这与您的答案略有不同。 - Jesse Good
1个回答

81

有些问题已经涉及到按值传递和按引用传递的区别。实质上,将参数按值传递给函数意味着该函数将拥有其自己的参数副本-它的被复制了。修改该副本不会修改原始对象。但是,当按引用传递时,在函数内部的参数指向传入的相同对象-函数内部的任何更改都将在外部可见。

不幸的是,“按值传递”和“按引用传递”这两个短语的使用方式存在两种可能会引起混淆的情况。我认为这在某种程度上是导致新的C++程序员难以采用指针和引用的原因,特别是当他们来自C背景的时候。

C

在C中,严格来说,所有的东西都是按值传递的。也就是说,无论你将什么作为函数的参数传递,都会被复制到该函数中。例如,调用一个带有void foo(int)签名的函数并使用foo(x)作为参数,将会把x的值复制到foo的参数中。这可以在一个简单的例子中看到:

void foo(int param) { param++; }

int main()
{
  int x = 5;
  foo(x);
  printf("%d\n",x); // x == 5
}
x的值被复制到foo中,并且该副本被增加。在main中的x仍然保持其原始值。
正如您所知,对象可以是指针类型。例如,int* pp定义为指向int的指针。重要的是要注意,以下代码引入了两个对象:
int x = 5;
int* p = &x;

第一个变量的类型是int,其值为5。第二个变量的类型是int*,它的值是第一个对象的地址。

当向函数传递指针时,仍然按值传递。它所包含的地址被复制到函数中。在函数内部修改该指针不会改变函数外部的指针 - 但是,在函数内部修改它指向的对象将会改变函数外部的对象。但是,为什么呢?

由于具有相同值的两个指针总是指向相同的对象(它们包含相同的地址),因此可以通过两个指针访问和修改指向的对象。这赋予了通过引用传递指向对象的语义,尽管实际上根本没有引用 - 在C语言中根本不存在引用。看一下更改后的示例:

void foo(int* param) { (*param)++; }

int main()
{
  int x = 5;
  foo(&x);
  printf("%d\n",x); // x == 6
}

当我们把int*传递给函数时,可以说指向的int是“按引用传递”的,但实际上int根本没有被传递到任何地方-只有指针被复制到函数中。这给了我们“按值传递”和“按引用传递”的口语意义。

这种术语的使用得到了标准术语的支持。当您有指针类型时,它所指向的类型称为其“引用类型”。也就是说,int* 的引用类型是 int

指针类型可以来自函数类型、对象类型或不完全类型,称为“引用类型”。

虽然在标准中,一元运算符(如*p)被称为间接运算,但通常也被称为指针的解除引用。这进一步促进了在C中“按引用传递”的概念。

C ++

C ++从C采用了许多原始语言特性,其中包括指针,因此仍然可以使用这种口语化的“按引用传递”形式-*p仍然是解除引用p。但是,使用这个术语会很困惑,因为C ++引入了C没有的功能:真正传递引用的能力。

一个类型后面跟着一个&是一个引用类型2。例如,int&是对int的引用。当将参数传递给接受引用类型的函数时,对象确实通过引用传递。没有指针参与,没有对象复制,没有任何东西。函数内部的名称实际上指的是与传入的对象完全相同的对象。为了与上面的例子形成对比:

void foo(int& param) { param++; }

int main()
{
  int x = 5;
  foo(x);
  std::cout << x << std::endl; // x == 6
}
现在 foo 函数有一个参数,它是对一个 int 的引用。现在当传递 x 时,param 引用的是完全相同的对象。递增 paramx 的值产生了可见的改变,现在 x 的值为6。
在这个例子中,没有通过值传递任何东西。没有复制任何东西。与C中不同,其中按引用传递实际上只是通过值传递指针,在C++中,我们真正可以通过引用传递。
由于“按引用传递”一词存在潜在歧义,最好仅在使用引用类型时在C++上下文中使用它。如果您正在传递指针,则不是按引用传递,而是按值传递指针(当然,除非您正在传递指向指针的引用!例如:int*&)。但是,在使用指针时,您可能会遇到“按引用传递”的用法,但现在您至少知道实际发生了什么。
其他编程语言进一步复杂化了事情。在某些语言中,如Java,你拥有的每个变量都被称为一个对象的引用(不同于C++中的引用,更像指针),但这些引用是按值传递的。因此,即使您似乎正在通过引用传递到一个函数中,实际上您正在通过值将引用复制到函数中。当您将新对象分配给传入的引用时,会注意到这种微小差异与在C++中按引用传递时不同:
public void foo(Bar param) {
  param.something();
  param = new Bar();
}

如果你在Java中调用这个函数并传入类型为Bar的某个对象,那么对param.something()的调用将在你传入的同一对象上进行。这是因为你传递了一个指向你的对象的引用。然而,即使新的Bar被分配给param,函数外部的对象仍然是同一个旧对象。外部永远无法看到新对象,因为在foo内部的引用被重新分配到了一个新对象上。这种重新分配引用的行为在C++引用中是不可能实现的。


1“口语化”并不意味着C语言中的“按引用传递”的含义比C++含义更不准确,只是因为C++确实有引用类型,所以你真正地通过引用进行参数传递。C语言中的含义是在“按值传递”的抽象之上。

2当然,这些是左值引用,现在在C++11中还有右值引用。


7
“在C语言中,确实没有真正的按引用传递。”-- 这是错误的。你对按引用传递的定义偏向于C++。术语“按引用传递”比C++更早出现,它在不同的语言中有不同的实现方式。在C中,通过传递一个对象的指针,实际上是通过引用传递该对象。事实上,在C++中也是如此。只不过C++有两种按引用传递的方式。 - Benjamin Lindley
4
在某些编程语言中,比如Java,每个变量都是一个指向对象的指针(称为引用,但绝不是C++中引用的意思)。这与在C ++中传递一个指向对象的指针完全相同。在这里,你将自己搞混了,“传入Bar类型的某个对象”。在Java中,对象无法被传递,也不是值。 - newacct
所以基本上: C语言:通过值传递指针, C++语言:通过引用传递。 - bryanph
1
说传递引用不会复制可能是错误的,因为大多数编译器在内部实现引用作为常量指针,这些指针会自动解引用,所以当您将引用传递给函数时,它会进行复制。 - vinodsaluja
@vinodsaluja 请问您能解释一下如何才能在外部看到更改了的C++引用(allais)吗?这是否意味着编译器会将新值同步并复制到原始内存中?! - Shahaboddin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接