C++返回值是類名和返回值是引用的區(qū)別及說明
返回值是類名和返回值是引用的區(qū)別
返回非引用類型
函數(shù)的返回值用于初始化在調(diào)用函數(shù)時創(chuàng)建的臨時對象(temporary object),如果返回類型不是引用,在調(diào)用函數(shù)的地方會將函數(shù)返回值復(fù)制給臨時對象。
在求解表達(dá)式的時候,如果需要一個地方存儲其運算結(jié)果,編譯器會創(chuàng)建一個沒命名的對象,這就是臨時對象。C++程序員通常用temporary這個術(shù)語來代替temporary object。
用函數(shù)返回值初始化臨時對象與用實參初始化形參的方法是一樣的。
當(dāng)函數(shù)返回非引用類型時,其返回值既可以是局部對象,也可以是求解表達(dá)式的結(jié)果。
返回引用類型
當(dāng)函數(shù)返回引用類型時,沒有復(fù)制返回值,相反,返回的是對象本身。
千萬不要返回局部對象的引用!千萬不要返回指向局部對象的指針!
當(dāng)函數(shù)執(zhí)行完畢時,將釋放分配給局部對象的存儲空間。此時對局部對象的引用就會指向不確定的內(nèi)存!返回指向局部對象的指針也是一樣的,當(dāng)函數(shù)結(jié)束時,局部對象被釋放,返回的指針就變成了不再存在的對象的懸垂指針。
返回引用時,要求在函數(shù)的參數(shù)中,包含有以引用方式或指針方式存在的,需要被返回的參數(shù)。
如果返回對象,最后多執(zhí)行一次拷貝構(gòu)造函數(shù),如果返回引用,直接返回現(xiàn)存對象
#include <iostream> using namespace std; class Timer { public: ? ? Timer(); ? ? Timer(int, int, int); ? ? friend Timer ?&operator+(Timer&, Timer&); ? ? friend Timer operator-(Timer&, Timer&); ? ? friend ostream& operator<<(ostream &out, Timer &t); ? ? friend istream& operator>>(istream &in, Timer &t); private: ? ? int hour, minute, second; }; Timer::Timer() { ? ? hour = 0; ? ? minute = 0; ? ? second = 0; } Timer::Timer(int hour, int minute, int second) { ? ? this->hour = hour; ? ? this->minute = minute; ? ? this->second = second; } Timer & operator+(Timer& a, Timer &b) { ? ? a.second = a.second + b.second; ? ? a.minute = a.minute + b.minute; ? ? a.hour = a.hour + b.hour; ? ? if (a.second >= 60) ? ? { ? ? ? ? a.second = a.second - 60; ? ? ? ? a.minute++; ? ? } ? ? if (a.minute >= 60) ? ? { ? ? ? ? a.minute = a.minute - 60; ? ? ? ? a.hour++; ? ? } ? ? return a; } Timer operator-(Timer &a, Timer &b) { ? ? Timer c; ? ? c.hour = a.hour - b.hour; ? ? c.minute = a.minute - b.minute; ? ? c.second = a.second - b.second; ? ? if (c.second < 0) ? ? { ? ? ? ? c.second += 60; ? ? ? ? c.minute--; ? ? } ? ? if (c.minute < 0) ? ? { ? ? ? ? c.minute += 60; ? ? ? ? c.hour--; ? ? } ? ? return c; } ostream& operator<<(ostream &out, Timer &t) { ? ? out << t.hour << ":" << t.minute << ":" << t.second << endl; ? ? return out; } istream& operator>>(istream&in, Timer &t) { ? ? cout << "Input hours and minutes." << endl; ? ? in >> t.hour >> t.minute >> t.second; ? ? return in; } int main() { ? ? Timer t1, t2, t3, t4; ? ? cin >> t1 >> t2; ? ? cout << "t1=" << t1; ? ? cout << "t2=" << t2; ? ? t3 = t1 + t2; ? ? cout << "t3=t1+t2=" << t3; ? ? t4 = t1 - t2; ? ? cout << "t4=t1-t2=" << t4; ? ? return 0; }
剛開始我將函數(shù)聲明為:
friend Timer&operator+(Timer&,Timer&); friend Timer&operator-(Timer&,Timer&);
對于<<,>>的重載聲明的和長代碼一樣。但是這樣做之后我發(fā)現(xiàn)出現(xiàn)異常。
在Time &operator+(Timer&t1,Timer&t2)里我聲明了一個Timer的局部變量,而當(dāng)函數(shù)調(diào)用結(jié)束后,該指針變成了懸掛指針。
所以,一定謹(jǐn)記不要在返回值為引用的函數(shù)中返回局部變量。
C++函數(shù)返回值和返回引用問題
C++函數(shù)的返回過程基本可以分為兩個階段,返回階段和綁定階段,根據(jù)兩個階段中需要返回的值的類型不同(返回值和引用),和要綁定的值的類型(綁定值和引用)會產(chǎn)生不同的情況。
最基本的規(guī)則是先返回,再綁定,返回和綁定的時候,都有可能發(fā)生移動或者拷貝構(gòu)造函數(shù)的調(diào)用來創(chuàng)建臨時對象,并且只會發(fā)生一次。更具體的,當(dāng)返回值的時候,函數(shù)返回之前,會調(diào)用一次拷貝構(gòu)造函數(shù)或移動構(gòu)造函數(shù),函數(shù)在綁定到值的時候,會發(fā)生一次拷貝構(gòu)造函數(shù)或移動構(gòu)造函數(shù),如果返回時已經(jīng)調(diào)用了構(gòu)造函數(shù),則綁定時不會再調(diào)用,而是直接綁定。返回引用或者綁定到引用,在各自階段,不會產(chǎn)生構(gòu)造函數(shù)的調(diào)用。
下面舉例闡述各種可能的情況(例子中很多情況我們是不該這樣寫的,本文只討論如果這樣做了會怎樣)。
關(guān)于右值引用和移動構(gòu)造函數(shù)的解釋,后面會寫篇東西做個總結(jié)。這里僅需要一條規(guī)則,那就是在需要調(diào)用移動構(gòu)造函數(shù)的情況,如果類沒有定義移動構(gòu)造函數(shù),則調(diào)用它的拷貝構(gòu)造函數(shù),后面對未定義移動構(gòu)造函數(shù)的情況,不再贅述。
為敘述方便,定義綁定值和返回值:
綁定值就是函數(shù)賦給的變量,返回值就是函數(shù)的返回值,最終的調(diào)用形式類似:
返回值 fun() { ?? ?..... ?? ?return 返回值; } 綁定值 = func();
定義一個類:
class myClass { public: ? ? //構(gòu)造函數(shù) ?? ?myClass() ?? ?{ ?? ??? ?cout << "construct" << endl; ?? ?} ? ? //拷貝構(gòu)造函數(shù) ?? ?myClass(const myClass& rhs) ?? ?{ ?? ??? ?cout << "copy construct" << endl; ?? ?} ? ? //移動構(gòu)造函數(shù) ?? ?myClass(myClass&& rhs) ?? ?{ ?? ??? ?cout << "move consturct" << endl; ?? ?} ? ? //析構(gòu)函數(shù) ?? ?~myClass() ?? ?{ ?? ??? ?cout << "desctruct" << endl; ?? ?} };
1.綁定值類型為值類型
返回值類型為值類型,返回的是局部變量:
在函數(shù)返回階段,調(diào)用類的移動構(gòu)造函數(shù)創(chuàng)建返回值,并綁定到綁定值上。移動構(gòu)造函數(shù)的調(diào)用發(fā)生在函數(shù)返回階段。
例子:
myClass fun() { ?? ?myClass mc; ?? ?return mc; } ? int main() { ?? ?myClass result = fun(); }
輸出:
construct
move consturct
desctruct
desctruct
2.綁定值類型為值類型
返回值類型為值類型,返回的是局部變量的引用:
首先,這不是一個好的做法,企圖返回一個局部變量的引用結(jié)果通常并不會令人滿意。
函數(shù)調(diào)用結(jié)束后,函數(shù)幀被回收(實際上只是設(shè)置了棧頂?shù)牡刂罚?,函?shù)棧中的臨時變量不再有意義。
但是這么做通常也不會造成什么副作用,因為在函數(shù)返回階段,會調(diào)用拷貝構(gòu)造函數(shù)(注意,即使定義了移動構(gòu)造函數(shù)也不會調(diào)用),將生成的新的臨時對象,綁定到綁定值上。
例子:
myClass fun() { ?? ?myClass mc; ?? ?myClass& mcRef = mc; ? ? ? ? //如果是想返回這個局部變量的引用,并且在函數(shù)外部對其進(jìn)行修改,那必然要失望了。 ?? ?return mcRef; } ? int main() { ?? ?myClass result = fun(); }
輸出:
construct
copy construct
desctruct
desctruct
3.綁定值類型為值類型
返回值類型為值類型,返回的是全局變量的引用:
和2中的情形類似,但是由于全局變量的生命周期超過當(dāng)前函數(shù),所以即使函數(shù)返回,變量仍然存活。在函數(shù)返回階段,仍然會調(diào)用拷貝構(gòu)造函數(shù),將產(chǎn)生得臨時對象綁定到綁定值上,卻依然不能返回全局對象。
例子:
通常,返回作為參數(shù)而傳遞進(jìn)來的引用的情況更加常見,此時的參數(shù)對于函數(shù)來說,和全局變量類似,即函數(shù)返回,變量的生命周期也不會結(jié)束。
myClass fun(myClass& param) { ? ? ? ? //最終只能得到param的拷貝 ?? ?return param; } ? int main() { ?? ?myClass mc; ?? ?myClass result = fun(mc); }
輸出:
construct
copy construct
desctruct
desctruct
4.綁定值類型為值類型
返回值類型為引用類型,返回的是局部變量或局部變量的引用:
和情況2情形類似的是,希望以引用的方式返回局部變量或者局部變量的引用,通常也不能取得令人滿意的結(jié)果;但與情況2的情形不同的是,情況2基本不會產(chǎn)生什么副作用,情況2中在函數(shù)返回階段對對象進(jìn)行拷貝,此時對象完整;但是在本情況中,就會產(chǎn)生嚴(yán)重的副作用。
由于返回值類型為引用類型,在函數(shù)返回階段,并不會調(diào)用拷貝構(gòu)造函數(shù),而在綁定階段,綁定值是值類型,才會產(chǎn)生拷貝構(gòu)造函數(shù)的調(diào)用,而此時函數(shù)已經(jīng)返回,拷貝函數(shù)拷貝的是函數(shù)中的臨時變量,此時已經(jīng)析構(gòu)了,再進(jìn)行拷貝,只會得到一個被析構(gòu)對象的拷貝,通常,這會產(chǎn)生嚴(yán)重的錯誤。
簡單的說,這種情況下,會先析構(gòu)臨時對象,在拷貝這個臨時對象。
例子:
myClass& fun() { ?? ?myClass mc; ?? ?//或者myClass mcRef = mc; return mcRef ?? ?return mc; } ? int main() { ?? ?myClass result = fun(); }
輸出:
construct
desctruct
copy construct
desctruct
5.綁定值類型為值類型
返回值類型為引用類型,返回的是全局變量的引用:
基本的情況和2是類似的,即并不能真的的到全局變量的引用,但是也不會產(chǎn)生什么副作用(除非調(diào)用者就是希望或者這個全局變量,并進(jìn)行修改)。由于全局變量在函數(shù)結(jié)束之后并不會被析構(gòu),所以對它調(diào)用拷貝構(gòu)造函數(shù)是安全的。和情況4類似,拷貝函數(shù)的調(diào)用也不是發(fā)生在函數(shù)返回階段,而是發(fā)生在綁定階段。
例子:
同樣使用參數(shù)為引用類型的函數(shù)舉例:
myClass& fun(myClass& param) { ?? ?return param; } ? int main() { ?? ?myClass mc; ?? ?myClass result = fun(mc); }
輸出:
construct
copy construct
desctruct
desctruct
上述5中情況是將返回值綁定到值類型的情形,所有情形中,均會出現(xiàn)拷貝或移動構(gòu)造函數(shù)的調(diào)用,最終綁定之后,都不可能得到原來的對象,企圖通過這種方式來修改原對象,必然會失敗。
如果綁定值類型為引用類型,返回值類型為值類型,返回的是局部變量,局部變量的引用或者全局變量會怎么樣呢?
這種情況編譯器會報錯,由于函數(shù)返回的是值類型,必然會產(chǎn)生移動構(gòu)造函數(shù)或者拷貝構(gòu)造函數(shù),產(chǎn)生一個臨時對象,而臨時對象是一個右值,我們常說的引用,其實是左值引用,而一個右值對象是不能夠綁定到一個左值引用的,所以編譯器會報錯。
我們可以將函數(shù)的返回值版綁定到右值引用變量(使用myClass&&),我們下面討論返回右值引用的情況。但是,有時候返回一個右值引用并且使用它,可能并不能產(chǎn)生想要的結(jié)果(如本文最后例子)。
6.綁定值類型為右值引用
返回值類型為值類型,返回的是局部變量或局部變量的引用或全局變量的引用:
函數(shù)返回階段,由于是返回值類型,所以會調(diào)用移動構(gòu)造函數(shù)或者拷貝構(gòu)造函數(shù),創(chuàng)建一個不具名的臨時變量(其實它就是一個右值),可以把它綁定到一個右值引用上。
但是對它的修改是沒有什么意義的,本來函數(shù)創(chuàng)建的這個不具名臨時變量,會在函數(shù)返回之后被析構(gòu),由于我們增加了一個指向它的引用,所以這個臨時變量的生命周期被延長了,直到指向它的引用離開作用域,它才會析構(gòu)。
例子:
myClass fun() { ?? ?myClass mc; ?? ?return mc; } int main() { ?? ?myClass&& result = func(); }
輸出:
construct
move consturct
desctruct
desctruct
myClass fun() { ?? ?myClass mc; ?? ?myClass& mcRef = mc; ?? ?return mcRef; } int main() { ?? ?myClass&& result = fun(); }
輸出:
construct
copy construct
desctruct
desctruct
7.綁定值類型為為引用類型
返回值類型為引用類型,返回局部變量或局部變量的引用:
由于函數(shù)返回階段和綁定階段都是引用類型,所以不會產(chǎn)生任何拷貝或移動構(gòu)造函數(shù)的調(diào)用,最終綁定值類就是函數(shù)的局部變量,但是由于函數(shù)返回后,局部變量已經(jīng)被析構(gòu),在保存這個局部變量的引用沒有意義,往往會引起一些詭異的錯誤。
例子:
myClass& fun() { ?? ?myClass mc; ?? ?//或者myClass& mcRef = mc; return mcRef; ?? ?return mc; } ? int main() { ? ? ? ? //在函數(shù)返回之后,我們甚至還能使用這個result,但是僅僅是編譯器將原來result的位置繼續(xù)解釋成result變量, ? ? ? ? //很有可能很快這個位置會被新的棧幀覆蓋,繼續(xù)使用result,可能會出現(xiàn)“詭異”的問題 ?? ?myClass& result = fun(); }
輸出:
construct
desctruct
8.綁定值類型為引用類型
返回值類型為引用類型,返回全局變量的引用:
通常,當(dāng)我們這是我們的真實意圖,函數(shù)在返回階段和綁定階段均不存在構(gòu)造函數(shù)的調(diào)用,最終綁定值就是需要返回的全局變量,對最終綁定值的任何修改,均會反映到全局變量上。
例子:
依然使用參數(shù)為引用類型的函數(shù)舉例。
myClass& fun(myClass& mc) { ?? ?return mc; } int main() { ?? ?myClass mc; ?? ?myClass& result = fun(mc); }
輸出:
construct
desctruct
9.一個思考
右值是不能綁定到左值引用的,但是當(dāng)把右值付給左值會怎樣呢?
如下
int main() { ?? ?myClass mc; ?? ?myClass result = move(mc); }
答案是會調(diào)用移動構(gòu)造函數(shù)創(chuàng)建一個新的對象,并且綁定到綁定值。
輸出:
construct
move consturct
desctruct
desctruct
另一個非常類似的例子:
int main() { ?? ?myClass mc; ?? ?myClass&& mcRightRef = move(mc); ?? ?myClass result = mcRightRef; }
和第一個非常類似,但是result的生成是調(diào)用拷貝構(gòu)造函數(shù)而非移動構(gòu)造函數(shù)完成的。
輸出:
construct
copy construct
desctruct
desctruct
這就涉及到了移動構(gòu)造函數(shù)調(diào)用的時機,更多關(guān)于右值,右值引用和移動構(gòu)造函數(shù)詳細(xì)的研究,待續(xù)。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
c語言數(shù)據(jù)結(jié)構(gòu)與算法之順序表的定義實現(xiàn)詳解
這篇文章主要介紹了c語言數(shù)據(jù)結(jié)構(gòu)與算法之順序表的定義實現(xiàn)詳解,用順序存儲的方式實現(xiàn)線性表順序存儲,把邏輯上相鄰的元素存儲在物理位置上也相鄰的存儲單元中,元素之間的關(guān)系由存儲單元的鄰接關(guān)系來體現(xiàn),需要的朋友可以參考下2023-08-08C++11中的時間庫std::chrono(引發(fā)關(guān)于時間的思考)
這篇文章主要介紹了C++11中的時間庫std::chrono(引發(fā)關(guān)于時間的思考),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04