从函数返回本地变量的const引用

45

我对从函数返回局部变量的引用有一些疑问:

class A {
public:
    A(int xx)
    : x(xx)
    {
        printf("A::A()\n");
    }
};

const A& getA1()
{
    A a(5);
    return a;
}

A& getA2()
{
    A a(5);
    return a;
}

A getA3()
{
    A a(5);
    return a;
}

int main()
{
    const A& newA1 = getA1(); //1
    A& newA2 = getA2(); //2
    A& newA3 = getA3(); //3
}

我的问题如下:

  1. getA1()的实现是否正确?我感觉它是错误的,因为它返回的是本地变量或临时变量的地址。

  2. main中哪个语句(1、2、3)会导致未定义的行为?

  3. const A& newA1 = getA1(); 中,标准是否保证一个被const引用绑定的临时对象在引用超出作用域前不会被销毁?


6
只是提一下,你的//3行代码不应该编译通过,按照标准,将临时对象绑定到非const引用是被禁止的。Visual Studio允许这样做,但是是错误的,gcc不会原谅你这样做。 - sbk
这是个好观点,@sbk。它在这个链接的Q2中有解释:http://herbsutter.wordpress.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/ - aJ.
4个回答

38

1. getA1() 的实现是否正确?我觉得它是错误的,因为它返回本地变量或临时变量的地址。

在您的程序中唯一正确的 getAx() 版本是 getA3()。其他两个无论如何使用都具有未定义行为。

2. main 函数中哪些语句将导致未定义行为?

从某种意义上来说,都没有。对于 1 和 2,未定义行为是由函数体引起的。对于最后一行,newA3 应该是编译错误,因为您无法将临时对象绑定到非 const 引用上。

3. 在 const A& newA1 = getA1(); 中,标准是否保证通过 const 引用绑定的临时对象在引用超出作用域之前不会被销毁?

不保证。以下是一个例子:

A const & newConstA3 = getA3 ();

这里,getA3() 返回一个临时对象,并且该临时对象的生命周期现在与 newConstA3 对象绑定。换句话说,只要 newConstA3 超出范围,临时对象就存在。


3
我认为最后一句话有些混淆。你的意思是说,只有当 newConstA3 超出其作用域时,才会销毁临时对象吗? - Naveen
好问题@Naveen!虽然我知道你现在一定已经过去了,但为了后人,请看看这篇好文章:https://dev59.com/-HM_5IYBdhLWcg3wPAfT - Matias

5

Q1: 是的,这是一个问题,请参考问题2的答案。

Q2: 1和2未定义,因为它们指的是getA1和getA2堆栈上的局部变量。那些变量超出了范围,并且不再可用,更糟糕的是,在堆栈不断变化时可能被覆盖。getA3有效,因为会创建并返回返回值的副本给调用者。

Q3: 没有这样的保证,请参见问题2的答案。


2
我认为主要问题在于您没有返回任何临时变量,您应该这样做:
return A(5);

相比于
A a(5);
return a;

否则你会返回局部变量地址,而不是临时变量。而且只有对于临时变量,才能将其转换为const引用。
我认为这里已经解释得很清楚了: 临时变量转换为const引用

5
我不认为这是正确的。临时对象的概念并没有在返回类型中传递。要考虑到你可能会在不同的翻译单元中调用函数,而调用者无法知道返回的表达式是临时对象还是其他对象。 - Richard Corden
2
@Arkaitz:我想我没有表达清楚,我的错。问题出在函数签名返回一个引用上。返回语句中的元素是临时的还是不是临时的并不影响问题代码的不正确性。 - David Rodríguez - dribeas
2
现在,根据您所做的更改:函数不再返回一个引用,而是返回一个值对象,那么代码就变得正确了。区别在于,在“调用”函数中,临时对象将与“调用”函数中的引用绑定,并且其生命周期得到延长,而在初始签名中,临时对象位于“被调用”函数中。想一想:在调用点,编译器无法确定返回的const引用是指向临时对象还是其他对象的引用。 - David Rodríguez - dribeas
2
...或者需要为未知的临时变量保留多少额外的堆栈空间,甚至不确定返回的对象是否是临时的。--回到你的例子,你正在返回一个变量,代码在函数内部创建了一个临时变量,将其复制到返回变量中,然后将其绑定到调用函数中的引用上。编译器可以省略复制(因此您只看到析构函数调用)并不意味着语义不是复制的。您可以通过将X的复制构造函数声明为私有来检查它。您的代码将无法编译。 - David Rodríguez - dribeas
2
@litb:实际上情况恰恰相反。不是允许复制临时对象,而是编译器允许省略复制操作。结果是一样的:不同的实现可能会复制或不复制,但编译器必须检查是否可以执行复制以满足语义要求,即使最终不执行复制操作。 - David Rodríguez - dribeas
显示剩余6条评论

0
如果您在VC6上编译此代码,将会收到以下警告:
******编译器警告(级别1)C4172 返回本地变量或临时对象的地址 函数返回本地变量或临时对象的地址。本地变量和临时对象在函数返回时被销毁,因此返回的地址无效。******
在测试此问题时,我发现了一个有趣的事情(给定的代码在VC6中可以工作):
 class MyClass
{
 public:
 MyClass()
 {
  objID=++cntr;
 }
MyClass& myFunc()
{
    MyClass obj;
    return obj;
}
 int objID;
 static int cntr;
};

int MyClass::cntr;

main()
{
 MyClass tseadf;
 cout<<(tseadf.myFunc()).objID<<endl;

}

尝试一下:MyClass& o = tseadf.myFunc(); cout << "Hello World\n"; cout << o.objID << endl; 顺便问一下,我们现在能否放弃VC6了?虽然老旧并不总是意味着不好,但在VC6的情况下却是这样。 - sbk
谢谢回复。关于VC6,我们受到管理人员的限制 :) - Satbir

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