c++11 - c++函数返回对象的引用问题?
迷茫
迷茫 2017-04-17 13:28:58
0
4
404
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);

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

迷茫
迷茫

业精于勤,荒于嬉;行成于思,毁于随。

全部回覆(4)
迷茫
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&&的時候才是右值。而且不管左值右值都是可以取位址的。

難道對這個臨時引用變數的所有操作都是對它所引用的物件的操作?

或者說壓根就不會產生這個臨時引用變量,所有的操作都是在函數傳回的物件上的操作? - - 暈了,真心求我的錯誤! ! !

題主的理解基本上沒有錯誤,除了上面取地址的那一條

PHPzhong

先引用一樓的

引用是指定義一個變數作為另一個變數的別名。透過引用存取是直接存取到所引用的變數。

這句話是很正確的,引用其實就是一種“別名”,任何使用引用的地方,都可以直接替換為該引用所引用的對象。比如說,

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給做掉呢。 。 。

巴扎黑

個人粗淺理解:引用和指標是兩回事。

  • 引用是指定義一個變數作為另一個變數的別名。透過引用存取是直接存取到所引用的變數。

  • 指標是先給自己開闢一個空間,然後把指向變數的位址放到這個開闢的空間。透過指標存取是需要透過 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. ...

參考

  • What is a reference?

  • How does a C++ reference look, memory-wise?

小葫芦

這段期間不會有臨時的string物件生成,有的只不過是引用的傳遞(其實就是記憶體位址)

我們說引用實際上是地址(或指標)是從語言實現的角度說的,但對於C++語義的角度來講,引用屬於原始對象的別名,所以取地址是原始對象的地址,或者換句話說

&Func(foo)

取的位址是foo的位址,(當然從實作上來講就是這個引用存的那個位址值)

題主可以比較的看待下面的例子體會一下
int a = 0
int& r = a
&r == &a

熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板