6
#include<iostream>
using namespace std;

int &fun()
{
  static int x = 10;
  return x;
}
int main()
{
   fun() = 30;
   cout << fun();
   return 0;
}

函数 fun() 通过引用返回值,但在 main() 方法中,我将一些 int 分配给函数。理想情况下,编译器应该显示像 lvalue required 这样的错误,但在上述情况下,程序可以正常工作。为什么会这样?

4

7 回答 7

7

说“函数返回某些东西”是松散而草率的语言。如果您知道如何使用它,它可以作为速记,但在这种情况下您会感到困惑。

考虑它的更正确方法是评估函数调用表达式。这样做会给你一个价值。值是右值或左值(取模细节)。

T是一个对象类型并且你评估一个返回类型的函数时T,你会得到一个类型的值,T它是一个右值。另一方面,如果函数有返回类型T &,你会得到T一个左值类型的值(并且该值是绑定到return语句中引用的东西)。

于 2013-09-23T17:02:54.937 回答
4

返回参考非常有用。

例如,它是做什么的std::map::operator[]。我希望你喜欢写作的可能性my_map[key] = new_value;

如果一个常规(非操作员)函数返回一个引用,那么可以分配给它,我看不出有任何理由应该禁止这样做。

如果你真的想要,你可以通过返回 aconst X&或返回来阻止分配。X

于 2013-09-23T17:11:18.483 回答
3

您可以使用指针重写代码,这可能更容易理解:

#include<iostream> 
using namespace std; 

int *fun() //fun defined to return pointer to int
{ 
    static int x = 10; 
    return &x; // returning address of static int
} 
int main() 
{ 
    *fun() = 30; //execute fun(), take its return value and dereference it,
                 //yielding an lvalue, which you can assign to. 
    cout << *fun();  //you also need to dereference here
    return 0; 
}

从语法的角度来看,引用可能非常令人困惑,因为底层“指针”的取消引用是由编译器为您隐式完成的。指针版本看起来更复杂,但它的符号更清晰或更明确。

PS:在有人反对我认为引用是一种指针之前,两个代码版本的反汇编是 100% 相同的。

PPS:当然,这种方法是对封装的一种非常隐蔽的破坏。正如其他人指出的那样,这种技术有一些用途,但如果没有非常充分的理由,你永远不应该这样做。

于 2018-09-14T11:24:28.840 回答
2

我也被类似的代码迷住了——一拳。这是“为什么我要为函数调用赋值,为什么编译器对此感到满意?” 我问自己。但是,当您查看“背后”发生的事情时,这确实是有道理的。


正如cpp和其他人指出的那样,左值是具有地址的“内存位置”,我们可以为它们赋值。您可以在 Internet 上找到有关左值和右值主题的更多信息。

当我们查看函数时:

int& fun()
{
    static int x = 10;
    return x;
}

我将 & 移到了类型上,所以很明显我们正在返回对 int 的引用。
我们看到我们有x,它是左值 - 它有地址,我们可以分配给它。它也是静态的,这使它变得特别 - 如果它不是静态的,变量的生命周期(范围)将以离开函数时堆栈展开而结束,然后引用可以指向宇宙中存在的任何黑洞。然而,由于x是静态的,即使在我们离开函数之后(以及当我们再次回到函数时)它也会存在,并且我们可以在函数之外访问它。

我们正在返回对 int 的引用,并且由于我们返回x,它是对x的引用。然后我们可以使用引用来改变函数外部的x 。所以:

int main()
{
    fun();

我们只是调用函数。变量x(在 fun 函数的范围内)被创建,它被赋值为 10。即使在函数离开后,它的地址和值仍然存在——但我们不能使用它的值,因为我们没有它的地址。

    fun() = 30;

我们调用该函数,然后更改x的值。x值通过函数返回的引用进行更改。注意:函数首先被调用,并且只有在函数调用完成后才会发生分配。

    int& reference_to_x = fun(); // note the &

现在我们(最终)保留对函数返回的x的引用。现在我们可以在不先调用函数的情况下更改x 。reference_to_x可能与 fun 函数中的x具有相同的地址)

    int copy_of_x = fun(); // no & this time

这次我们创建了新的 int 并且我们只是复制了x的值(通过引用)。这个新的 int 有自己的地址,它不像reference_to_x那样指向x

    reference_to_x = 5;

我们通过引用将x赋值为 5,我们甚至没有调用该函数。copy_of_x没有改变。

    copy_of_x = 15;

我们将新的 int 更改为值15。x没有更改,因为copy_of_x有自己的地址。

}


正如6502和其他人指出的那样,我们使用类似的方法,通过容器和自定义覆盖返回大量引用。

std::map<std::string, std::string> map = {};

map["hello"] = "Ahoj";
// is equal to
map.operator[]("hello") = "Ahoj"; // returns reference to std::string
// could be done also this way
std::string& reference_to_string_in_map = map.operator[]("hello");
reference_to_string_in_map = "Ahoj";

我们使用的 map 函数可以有这样的声明:

std::string& map::operator[]( const std::string& key ); // returns reference

我们没有指向我们“存储”在 map 中的字符串的地址,所以我们调用 map 的这个重写函数,将 key 传递给它,以便 map 知道我们想要访问哪个字符串,并返回对该字符串的引用,这我们可以用来改变值。注意:再次调用该函数,并且只有在它完成后(map 找到正确的字符串并返回对它的引用)分配才会发生。就像 fun() = 10 一样,只是更漂亮......

希望这可以帮助那些即使在阅读其他答案后仍然无法理解所有内容的人......

于 2014-08-17T14:14:12.797 回答
2

它之所以有效,是因为该函数的结果一个左值。引用是左值。基本上,从函数返回非常量引用的全部目的是能够分配给它(或对引用对象执行其他修改)。

于 2013-09-23T17:12:25.360 回答
2

除了其他答案,请考虑以下代码:

SomeClass& func() { ... }

func().memberFunctionOfSomeClass(value);

这是一件非常自然的事情,如果您希望编译器在这方面给您一个错误,我会感到非常惊讶。

现在,当您编写some_obj = value;幕后真正发生的事情时,您调用some_obj.operator =(value);. 并且operator =()只是您的类的另一个成员函数,与memberFunctionOfSomeClass().

总而言之,归结为:

func() = value;
// equivalent to
func().operator =(value);
// equivalent to
func().memberFunctionOfSomeClass(value);

当然,这过于简单了,并且这种表示法不适用于内置类型int(但使用相同的机制)。

希望这将帮助您更好地理解其他人已经在lvalue方面解释过的内容。

于 2013-09-23T17:20:33.403 回答
0

L-value 是一个locator-value。这意味着它有地址。参考文献显然有一个地址。如果从 fun() 按值返回,则可以获得所需的左值:

#include<iostream>
using namespace std;

int fun()
{
  static int x = 10;
  return x;
}
int main()
{
   fun() = 30;
   cout << fun();
   return 0;
}
于 2013-09-23T17:14:50.480 回答