按值/引用传递定义的实现还是行为明智

Is pass-by-value/reference defined implementation or behavior wise?

本文关键字:实现 引用 定义 按值      更新时间:2023-10-16

问:按值/引用传递是否严格由C++的行为或实现明智地定义,你能提供权威的引用吗?

我和一位朋友在C++中进行了关于按值/引用传递的转换。我们在按值/引用传递的定义上产生了分歧。我知道将指针传递给函数仍然是按值传递的,因为指针的值是复制的,并且此副本用于函数中。随后,取消引用函数中的指针并改变它将修改原始变量。这就是分歧出现的地方。

他的立场是:仅仅因为指针值被复制并传递给函数,对取消引用的指针执行操作就能够影响原始变量,因此它具有按引用传递的行为,将指针传递给函数。

我的立场:将指针传递给函数确实复制了指针的值,函数中的操作可能会影响原始变量;但是,仅仅因为它可能会影响原始变量,这种行为并不构成通过引用传递,因为它是定义这些术语的语言的实现, 按值/引用传递。

引用这里投票最高的答案给出的定义:语言不可知论

按引用传递

通过引用传递参数时,调用方和被调用方对参数使用相同的变量。如果被调用方修改参数变量,则效果对调用方的变量可见。

按值传递

当参数按值传递时,调用方和被调用方有两个具有相同值的独立变量。如果被调用方修改参数变量,则效果对调用方不可见。

看完这些,我还是有种暧昧的感觉。例如,按值/引用传递可以支持我们的任一声明。谁能澄清这些定义是否源于行为或实现并提供引用?谢谢!

编辑:我应该更加小心我的词汇。让我澄清一下我的问题。当质疑通过引用时,我的意思不是纯粹谈论引用的C++实现,而是理论。C++,是不是 &传递引用是真正的 PBR,因为它不仅可以修改原始值,还可以修改值的内存地址。这导致了这个,带有指针的例子也算作PBR?

void foo(int ** bar){
*bar = *bar+(sizeof(int*));
cout<<"Inside:"<<*bar<<endl;
}
int main(){
int a = 42;
int* ptrA = &a;
cout<<"Before"<<ptrA<<endl;
foo(&ptrA);
cout<<"After:"<<ptrA<<endl;
}

输出是After ptrA等于Inside,这意味着函数不仅可以修改a,而且可以修改ptrA。因此,这是否将引用调用定义为一种理论:不仅能够修改值,而且能够修改值的内存地址。对不起,这个令人费解的例子。

你在这里谈论了很多关于指针的问题,它们确实大部分时间都是按值传递的,但你没有提到实际的C++引用,这是实际的引用。

int a{};
int& b = a;
// Prints true
std::cout << std::boolalpha << (&b == &a) << std::endl;

在这里,如您所见,两个变量具有相同的地址。简而言之,特别是在这种情况下,引用充当变量的另一个名称。

C++中的参考文献很特别。它们不是对象,不像指针。不能有引用数组,因为它要求引用具有大小。参考根本不需要有存储。

那么通过引用实际传递变量呢?

看看这段代码:

void foo(int& i) {
i++;
}
int main() {
int i{};
foo(i);
// prints 1
std::cout << i << std::endl;
}

在这种特殊情况下,编译器必须有一种方法来发送引用绑定到哪个变量。事实上,引用不需要有任何存储,但它们也不需要没有存储。在这种情况下,如果禁用优化,则编译器很可能使用指针实现引用的行为。

当然,如果启用了优化,它可能会跳过传递并完全内联函数。在这种情况下,引用不存在,或者没有任何存储,因为原始变量将直接使用。

其他类似的优化也发生在指针上,但这不是重点:重点是,实现引用的方式是实现定义的。它们很可能是按照指针实现的,但它们不是被迫实现的,并且引用的实现方式可能因情况而异。引用的行为由标准定义,并且实际上是按引用传递的。


指针呢?它们算不算通过参考?

我会说不。指针是对象,就像intstd::string一样。您甚至可以传递对指针的引用,从而允许您更改原始指针。

但是,指针确实具有引用语义。它们确实不是引用,就像std::reference_wrapper也不是引用一样,但它们具有引用语义。我不会将传递指针称为"按引用传递",因为您没有实际的引用,但您确实具有引用语义。

很多东西都有引用语义,指针,std::reference_wrapper,资源的句柄,甚至GLuint,都是opengl对象的句柄,都有引用语义,但它们不是引用。您没有对实际对象的引用,但可以通过这些句柄更改指向的对象。

您还可以阅读其他好文章和答案。它们都非常提供有关值和引用语义的信息。

  • isocpp.org:引用和值语义
  • Andrzej的C++博客:价值语义
  • 堆栈溢出:C++中的指针变量和引用变量有什么区别?

按值/引用传递(您忘记了使用指针将地址传递到内存中的位置)是C++实现的一部分。

还有另一种方法可以将变量传递给函数,那就是通过地址。按地址传递参数涉及传递参数变量的地址(使用指针)而不是参数变量本身。由于参数是地址,因此函数参数必须是指针。然后,该函数可以取消引用指针以访问或更改指向的值。

看看我一直认为是权威的来源:按地址传递参数。

关于按值传递时复制的值是正确的。这是C++中的默认行为。将值传递到函数中的优点是,当值传入函数时,函数无法更改原始值,这可以防止在更改参数值时出现任何不需要的错误和/或副作用。

传递 Value 的问题在于,如果您多次将整个结构或类传递到函数中,您将遭受巨大的性能损失,因为您将传递您尝试传递的值的完整副本,并且在类中的突变器方法的情况下,您将无法更改原始值,因此最终将创建数据的多个副本您正在尝试修改,因为您将被迫从函数本身返回新值,而不是从数据结构所在的内存位置返回新值。这完全是低效的。

仅当不必更改参数的值时,才希望按值传递。

这是关于按值传递参数主题的一个很好的来源。

现在,当您确实需要更改数组、类或结构的参数值时,您将需要使用"按引用传递"行为。通过将引用传递到内存中数据结构驻留在函数中的位置来更改数据结构的值会更有效。这样做的好处是,您不必从函数返回新值,而是函数可以直接更改您提供给它的引用的值,它位于内存中的位置。

查看此处以了解有关通过引用传递参数的更多信息。

编辑:关于在使用指针时是否通过引用或值传递非常量的问题,在我看来,答案很清楚。当使用指向非常量的指针时,它两者都不是。当将指针作为参数传递给函数时,您实际上是将 ADDRESS 的值"传递到函数中,并且由于它是内存中非常量所在位置的 ADDRESS 的副本,因此您可以更改该位置的数据值,而不是指针本身的值。如果您不想更改位于指针所指向的地址处的数据值,该指针由值作为参数传递到函数中,则最好将指向参数的指针设为常量,因为函数不会更改数据本身的值。

希望这是有道理的。

引用不同于指针。引入引用的主要原因是支持运算符重载。C++ 派生自 C,在此过程中,指针继承自 C。正如斯特劳斯特鲁普所说:C++从 C 继承的指针,所以我无法在不导致严重兼容性问题的情况下删除它们。

因此,实际上有三种不同的参数传递方式:

  • 按值传递
  • 通过引用传递 &
  • 通过指针传递。

现在,按指针传递与按引用传递具有相同的效果。那么如何决定你想使用什么呢?回到斯特劳斯特鲁普所说的话:

这取决于您要实现的目标: 如果要更改传递的对象,请通过引用调用或使用指针;例如空隙 f(X&);或无效 f(X*); 如果您不想更改传递的对象并且它很大,请通过 const 引用调用;例如无效 f(常量 X&);否则,按值调用;例如无效 f(X);

参考: http://www.stroustrup.com/bs_faq2.html#pointers-and-references

这些术语是关于传递的变量,在本例中为指针。如果将指针传递给函数,则传递的变量是指向对象的指针(保存对象的地址),而不是指向的对象。

如果按值传递指针,则在函数中追逐它指向的对象不会影响传递给函数的指针。

如果通过引用传递指针,则可以在指针指向的函数中进行更改,这将修改传递给此函数的指针。

这就是它的定义。否则,您可能会争辩说,如果您有一个全局std::map<int,SomeObject>并且您将int作为对象的键传递,那么也将是通过引用传递,因为您可以修改该全局映射中的对象,并且调用者将看到这些更改。 因为这个int也只是一个指向对象的指针。