C++ 引用离开其作用域会发生什么?

39
如果我正确地理解了 C++ 引用,它们就像指针一样,但具有保证数据完整性的特性(没有 NULL,没有(int*)0x12345)。但是当引用对象的作用域被离开时会发生什么呢?如果没有什么魔法(并且可能确实如此),那么引用对象将在幕后被销毁。
我编写了一段代码来验证这一点:
#include <iostream>
using namespace std;

class A {
public:
        A(int k) { _k = k; };
        int get() { return _k; };
        int _k;
};

class B {
public:
        B(A& a) : _a(a) {}
        void b() { cout << _a.get(); }
        A& _a;
};

B* f() {
        A a(10);
        return new B(a);
}

int main() {
        f()->b();
}

_k实例变量是用来检查堆栈帧是否存在的。

令人惊讶的是,它并没有发生段错误,而是正确地打印了“10”,尽管我认为A分配在堆栈上,并且f()的堆栈帧将被至少一个cout<<调用覆盖。


5
C++没有"链接",也许你指的是"引用(reference)"? - Evan Teran
3
你似乎意识到这会导致未定义的行为,那么为什么还要问呢?我的理解是A被分配在堆栈上,并且f()函数的堆栈帧将被至少cout<<调用覆盖。 - BlueRaja - Danny Pflughoeft
@Evan:没问题,只是翻译上的问题。 - whitequark
7
@BlueRaja:想知道事情是如何运作并没有什么害处。 - Evan Teran
3
认为引用是更安全的指针,从而认为引用不会失效的观点的人都是白痴。感谢您对这一说法进行思考而非盲目信任。 - Ben Voigt
2个回答

44

这是未定义的行为,你只是幸运地发现内存中的 a 还没有被用于其他任何事情。在更复杂的情况下,你几乎肯定会得到垃圾值。在我的机器上,使用这段代码会得到随机的垃圾值。对我来说,这很可能是因为我使用了一个使用寄存器调用约定的 64 位机器。寄存器比主内存更频繁地被重复使用(理想情况下...)。

所以回答你的问题"会发生什么"。在这种情况下,引用很可能不过是一个具有更友好语法的受限指针 :-)。底层存储了 a 的地址。稍后,a 对象超出作用域,但 B 对象对 a 的引用将不会 "自动地" 更新以反映这一点。因此,你现在有了一个无效的引用。

使用这个无效的引用将产生几乎任何东西,有时会崩溃,有时只是错误的数据。


编辑:感谢 Omnifarious,我一直在思考这个问题。在 C++ 中有一条规则,基本上是说如果你有一个对临时对象的 const 引用,那么这个临时对象的生命周期至少与 const 引用一样长。这引出了一个新问题。

编辑:移动到另一个问题(const reference to temporary oddity),供有兴趣的人参考。


我几乎确定了这一点,但还是问了一下,以防万一C++在某个地方有一个隐藏的引用计数器;) 无论如何,谢谢。 - whitequark
要明确地说,C ++ 在任何地方都没有内置的引用计数。你得到正好你所要求的,没有更多。但是有一些库实现了引用计数(例如 boost::shared_ptr 或者甚至 std::tr1::shared_ptr)。 - Evan Teran
4
编译器在一些有限的情况下会进行编译时的引用计数。如果您这样做 A &x = A(10);,则可以确保 x 引用的 A 对象直到 x 超出作用域之前不会被销毁。这仅在 x 具有块作用域时才有效。如果 x 是成员变量或者(我认为)全局或静态变量,则无法起作用。 - Omnifarious
1
@Omnifarious:说得很好,(尽管我认为它仅适用于**const**引用)。但这与引用计数并不完全相同。但是,无论如何都要注意到这一点。 - Evan Teran
为了通过附加一个const引用来延长临时对象的生命周期,应该直接将临时对象用作引用的初始化程序。你提供的所有示例都是无效的。通过构造函数参数传递临时对象并不能达到这个目的。 - AnT stands with Russia
显示剩余2条评论

12
在C++语言中,引用所绑定对象的生命周期与引用本身的生命周期没有任何关联,只有一种情况例外(见下文)。
如果对象在引用之前被销毁,那么引用就会变得无效(就像是悬挂指针)。任何试图访问该引用的行为都会导致未定义的行为。这就是你的示例中发生的情况。
如果引用的生命周期早于对象的结束,那么...好吧,什么事情也不会发生。引用消失了,对象依然存在。
我上面提到的唯一的例外是当一个const引用被立即临时对象初始化时。在这种情况下,临时对象的生命周期与引用的生命周期联系在一起。当引用消亡时,对象也随之消亡。
{
   const std::string& str = "Hello!"; 
   // A temporary `std::string` object is created here...
   ...
   // ... and it lives as long as the reference lives
   ...
} // ... and it dies here, together with the reference

顺便提一下,您错误地使用了术语scope范围是标识符的可见区域。 范围本身与对象生命周期无关。 当某些东西“离开范围”时,在一般情况下,这个东西并没有被销毁。 使用“离开范围”的措辞来指代对象的销毁点是一个普遍的误解。


2
感谢指出_scope_的误用——有时候试图将一个语言中的术语映射到另一个语言中会产生非常奇怪的结果——但是这整个问题都是关于当堆栈分配的对象在其标识符超出范围时被销毁时发生的事情。 (是的,内存只在leave时释放,但析构函数应该被调用,因为它已经被销毁了。) - whitequark
异常是否包含这种情况:const T&f(){T t; return t;} const T&tt = f();在这里使用tt?谢谢。 - jean

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