了解C++如何返回引用并绑定到引用


#include <iostream>
class A {
public:
int x = 0;
};
A& a_returner() {
A a;
return a;
}
int main()
{
A& a = a_returner();
std::cout << a.x << std::endl;
}

在此代码中,a_returner()返回对堆栈中变量a的引用。如果我们这样打电话:

A a = a_returner();

这不是问题,因为我们只是复制返回的A对象。但是,我们都在a_returner中返回引用并绑定到引用。

问题是:为什么会这样?A生活在a_returner堆里.

编译器确实给出了

Warning: reference to local variable `a` returned

但是该程序成功地打印了0.我认为从a_returner返回的a会访问无效数据。

你对"有效"的含义有一个错误的心理模型。

您在问题中的代码不起作用。当您尝试时它没有崩溃,结果是您所期望的,但这并不意味着(C++(一切都很好。

您刚刚发现的是"未定义行为"最糟糕的一面,即当您在C++中犯错误(例如返回对堆栈上对象的引用(时,任何事情都可能发生,包括什么都没有,代码显然可以按照您的意图工作。

代码仍然不起作用,并且在开发周期的后期会出现可怕的错误。

C++的主要哲学基础是程序员不犯错误,因此不是捕捉错误的"运行时错误角度",而是获得"未定义的行为守护进程",如果发生错误,程序可以做任何它想做的事情,包括暂时隐藏问题以便以后更努力地反咬。因此,在其他语言中,例如当您超出数组的边界时,您会得到运行时错误,而在C++中,您会得到未定义的行为,原因是假设C++程序员永远不会超出数组的边界,并且浪费时间检查这一点是没有意义的。

回顾一下:C++如果程序崩溃,您就知道代码有问题......如果程序没有崩溃并且结果是您所期望的,那么您仍然无法确定代码是否正确(即使是您作为输入提供的单个情况也是如此(。

许多C++学习者和一些自封C++"专家"认为崩溃是敌人,你应该防止崩溃(或者更糟糕的是吸收并吞下它们以继续运行(。实际上恰恰相反,崩溃是你的朋友,程序错误是敌人。问题是C++崩溃的频率不够高,有时应该崩溃的错误代码没有崩溃(如您的示例(。

在您的特定情况下,程序员有责任确保如果有引用,则引用的对象仍然处于活动状态,即存在引用的事实并不能保证该对象也存在,如果不是(如您的示例(,则代码是未定义的行为。 奇怪的是,C++有一个非常特殊的规则,在某些情况下,绑定引用的事实将"延长"应该立即丢弃的对象的寿命......例如:

int foo() {
return 42;
}
int bar() {
const int& x = foo(); // Note... a const reference
return x;
}

这段代码是有效且正确的,但这是一种意外,因为一个奇怪的规则说,如果将 const 引用绑定到临时引用,则临时生存期将延长到引用的生存期。 还要注意,这是一个非常微妙和精确的规则(例如,你必须理解法律术语,对于C++编译器来说什么是"临时的"(,除非你100%学习和理解该规则,否则你很有可能认为它适用,即使不适用。

只是不要那样做。

这是未定义的行为,一切皆有可能,即使它看起来运行良好。

顺便说一句:A a = a_returner();也有未定义的行为。从a_returner()返回的引用始终处于悬空状态,并且尝试从悬空引用初始化a

这在您的情况下(尽管是未定义的行为(有效,因为引用使用内存和别名的映射。(有关更多信息,请访问:C++指针和参考(。在您的情况下,函数a_returner()中的对象a正在使用的内存不会自动释放,因为操作系统可能不需要它。但是,正如@6502所提到的,该内存一直悬而未决。因此,该程序似乎有效。然而,正如@songyuanyao所解释的那样,这就是人们所说的未定义的行为。如果你仔细看看拆卸,你会发现所需的解释。

最新更新