c++函数返回对象的引用问题?

发布于 2022-09-02 10:35:11 字数 649 浏览 25 评论 0

string& Func(string& foo)
{
    return foo;
}

这个函数返回一个string的引用,我觉得调用它的时候会产生一个临时的引用变量,然后这个临时的引用变量绑定到foo上,对吗? 还是不会产生这个临时引用变量,直接返回foo?

比如下面这句赋值语句:

string foo;
string s = Func(foo);

等价于下面这两句:

string& temp = foo; //一个临时的引用变量绑定到foo上
string s = temp;

我理解的对吗? 返回引用其实还是会产生一个临时的引用变量吧(一个指针的大小),只不过用引用避免了对象的拷贝对吧? 不对的话求大大指出哪里理解的不对。。。

但如果返回一个临时的引用变量的话,又可以对它取地址这又怎么回事? 比如:

cout << &Func(foo);

这个临时变量应该是个右值才对? 难道对这个临时引用变量的所有操作都是对它引用的对象的操作?
或者说压根就不会生成这个临时引用变量,所有的操作都是在函数返回的对象上的操作? - - 晕了,真心求指出我的错误!!!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

柏林苍穹下 2022-09-09 10:35:11
string& Func(string& foo)
{
    return foo;
}

这个函数返回一个string的引用,我觉得调用它的时候会产生一个临时的引用变量,然后这个临时的引用变量绑定到foo上,对吗?

不对。

还是不会产生这个临时引用变量,直接返回foo?

对。

比如下面这句赋值语句:

string foo;
string s = Func(foo);

等价于下面这两句:

string& temp = foo; //一个临时的引用变量绑定到foo上
string s = temp;

我理解的对吗?

返回引用其实还是会产生一个临时的引用变量吧(一个指针的大小),只不过用引用避免了对象的拷贝对吧? 不对的话求大大指出哪里理解的不对。。。

但如果返回一个临时的引用变量的话,又可以对它取地址这又怎么回事? 比如:

cout << &Func(foo);

这个临时变量应该是个右值才对?

显然是左值,你的Func都返回string&了,那就是string&。只有返回string&&的时候才是右值。而且不管左值右值都是可以取地址的。

难道对这个临时引用变量的所有操作都是对它引用的对象的操作?

或者说压根就不会生成这个临时引用变量,所有的操作都是在函数返回的对象上的操作? - - 晕了,真心求指出我的错误!!!

题主的理解基本没有错误,除了上面取地址的那一条

满栀 2022-09-09 10:35:11

先引用一楼的

引用是指定义一个变量作为另一个变量的别名。通过引用访问是直接访问到所引用的变量。

这句话是很正确的,引用其实就是一种“别名”,任何使用引用的地方,都可以直接替换为该引用所引用的对象。比如说,

int i = 1;
int& r = i;
auto a = r;

那么a是什么类型呢?是int,而不是int&,可以理解为直接把右边的r换成i。这就是所谓的“别名”,用到该引用的地方,直接换成所引用的变量,大部分情况下如此。有一个例外,那就是c++中最为“诚实”的decltype,给它引用它就是引用,给它数组它就是数组,而不会变成相应的指针。

decltype(r) n = r;

n是什么类型呢?当然是int&啦。问题就来了,把一个int的引用绑定到一个int的引用上???当然不是,把等号右边的r换成i,这不就行了。
至于什么对引用取指针,把引用拿着一换,不就是对变量取指针了吗?
当然上面是一系列的语意分析,让你理解引用是什么东西。

那么再看看底层实现。
所谓的临时引用变量到底是不是个变量呢?这里的变量是指,它到底有没有占用内存空间呢?
很多的c++书上说引用在底层其实是通过指针来实现的,这个我还没有仔细的去研究过,如果要知道是不是真的是这样,恐怕还得反汇编。现在在外面,没电脑,就没法真正“做实验”了。
但是假如你是编译器的编写者,你会这么做?如果是我,那么我就会把所有用到r的地方,直接使用文本编辑器的“替换”功能,把它替换为i。(当然,肯定不是我描述的这么简单粗暴。。。)这样就压根不存在什么变量不变量的问题了,因为r已经“消失”了。。。
既然如此,你还用得着去纠结是否有所谓的引用变量吗?
那么那个函数的返回值怎么解释呢?题主想办法刁难编译器,非要让它给你弄一个临时引用变量出来不可!当然,编译器可是很聪明的,它才不会去创建一个临时的引用变量然后再进行初始化赋值,它直接把函数的返回值拿去初始化那个定义的引用变量,那个什么临时创建的引用类型的函数返回值,这一中转变量简直就是多此一举啊。(而且这个再一深究,就会涉及到函数返回值优化,以及c++11中右值引用的出现)
那么,真的有这么简单吗?当然不是这么简单,不然为什么说c++复杂呢。
考虑另外一个情景,那就是虚函数的调用。我们知道,对一个指向基类的指针调用其虚函数时,具体调用的是那个函数,由运行时指针指向的对象的真正类型所决定,这个过程叫动态绑定。要强调的一点就是,只有对指针进行这样的调用时,才会发生动态绑定,看下面这个例子。

DerivedClass object;
BaseClass* p = &object;
p -> virtualFunc();
BaseClass b = object;
b.virtualFunc();

前面的一次调用是动态绑定,会调用DerivedClass中的虚函数。而后者,第四行的赋值,发生了截断,第五行的调用,当然是不会发生动态绑定的,直接调用BaseClass中的虚函数,而且这个解析过程是发生在编译器。更本质的说,这里实际上发生了一次BaseClass的拷贝初始化,b其实是一个不同于object的另一个对象,压根就不存在什么动态绑定。
扯了这么远了,我究竟想说什么,题主如果留意的话,应该知道,引用也会导致动态绑定。只需要对上面的代码加一个符号:

BaseClass& c = object;
c.virtualFunc();

这个时候,虚函数的调用就又会进行动态绑定啦。这和指针也太像了吧,怎么做到的?还是所谓的直接“替换”吗?这里当然可以直接替换,因为我没有调用函数。如果我调用了一个接受基类引用的函数,并给它传了一个派生类的对象作参数呢,难道编译器跑到函数代码里面去改吗?那我分别要对一百个不同的派生类对象调用该函数呢?那编译器岂不是得创造一百个函数实例,并分别把其中的引用换成真正的变量。这个时候,编译器也知道自己没办法刷小聪明了,也只好乖乖的把引用变成指针了,只不过它悄悄的变,不告诉你,让你还是以为自己在传递引用。。。
说了这么多,我的意思是,编译器能优化的,它一般都帮你优化好了,能不创建临时变量,它一个也不多帮你创建。编译器不能优化的,就只能乖乖退一步用指针了,但是悄悄的,让你感觉自己还是在用引用

回到题主的问题上,前面的那个临时变量是否创建,或者说是否会吃掉一个指针的内存,这个得看编译器的具体实现和优化,可以创建一个指针,操作指针指向的对象,如果优化器分析得到位的话,应该是不用创建临时变量的,直接把所有的操作放到foo上,而不是间接地从指针指向的位置访问。当然,我这也是信口开河,具体怎么实现还真得反汇编。
至于后面那个左值右值的问题嘛。我个人的理解是,引用本身不是对象,甚至不是变量,不管编译器底层是怎么实现的,它对于我们来说不是一个变量,所以引用本身不存在左值右值的属性。而更多的是引用所绑定的对象的左值右值属性,左值引用绑定左值,右值引用和const左值引用绑定右值。所以最后所谓的引用是右值,确实没错,但是我们在操作引用的时候,实际上是在操作引用所绑定的对象,这里是绑定了左值的但本身是右值(临时创建的)的左值引用。引用本身的左值右值属性和它所绑定的对象的左值右值的属性是不同的,而且一般我们不去讨论引用本身的左右值属性,因为这涉及到引用的底层实现,而引用对于我们来说应该是一个抽象。所以最后对所谓的自身为右值的引用取地址,实际上是对该引用所绑定的左值string取地址。

最后要指出的一点就是,题主写c++的时候大可不必太纠结那所谓的一个引用变量的内存,你只需要知道,一般引用比拷贝要快就行了(基础数据类型则不是,一般指大一点的类对象),何必去想怎么把引用的overhead给做掉呢。。。

宣告ˉ结束 2022-09-09 10:35:11

个人粗浅理解:引用和指针是两回事。

  • 引用是指定义一个变量作为另一个变量的别名。通过引用访问是直接访问到所引用的变量。

  • 指针是先给自己开辟一个空间,然后把指向变量的地址放到这个开辟的空间里。通过指针访问是需要通过 dereference 操作间接访问所指的变量。


更新

C++ 标准只规定了引用是什么样子的(用法),但没有规定引用该怎么实现。你的问题其实关乎具体编译器对于引用的实现方式。通常情况下,引用是通过指针实现的,不过提供了一种比指针更容易理解的语法,同时在编译时会进行优化,引用就类似于常量指针。

1. 引用不一定占用空间

C++11 标准 § 8.3.2/4
It is unspecified whether or not a reference requires storage.

例如:

int i = 1;
int &ri = i;

编译器在编译时就会进行优化,ri不会单独占用空间,凡是编译遇到ri,直接替换成i就好了。

再例如题主的例子:

string& Func(string& foo)
{
    return foo;
}

这个函数参数foo没有办法在编译时优化掉,所以会分配空间。

2. 对引用的操作就是对被引用变量的操作

比如:

string foo;
string s = Func(foo);

调用的是被引用变量的operator=。跟题主理解的一样。

再比如:

string foo;
&Func(foo);  // 等价于 &foo

调用的是被引用变量的operator&

C++ 中不存在引用的引用。Func函数返回的是一个引用,对于引用变量,operator&会作用在它所引用的那个原始变量上,而原始变量foo的类型是stringstring没有重载operator&,所以&Func(foo)返回的是变量foo的地址。

C++11 标准 § 8.3.2/5
There shall be no references to references, no arrays of references, and no pointers to references. ...

参考

无尽的现实 2022-09-09 10:35:11

这期间不会有临时的string对象生成,有的只不过是引用的传递(实际上就是内存地址)

我们说引用实际上是地址(或者指针)是从语言实现的角度说的,但对于C++语义的角度来讲,引用属于原始对象的别名,所以取地址是原始对象的地址,或者换句话说

&Func(foo)

取的地址是foo的地址,(当然从实现上来讲就是这个引用存的那个地址值)

题主可以比较的看待下面的例子体会一下
int a = 0
int& r = a
&r == &a

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文