C++特性之引用
本章內(nèi)容:
1 引用的不同用例
1.1 引用變量
1.2 引用數(shù)據(jù)成員
1.3 引用參數(shù)
1.4 引用作為返回值
1.5 使用引用還是指針
1.6 右值引用
1 引用
- 在C++中,引用是變量的別名。所有對(duì)引用的修改都會(huì)改變被引用的變量的值??蓪⒁卯?dāng)作隱式指針,這個(gè)指針沒有取變量地址和解除引用的麻煩。
- 也可以將引用當(dāng)作原始變量的另一種名稱??梢詣?chuàng)建單獨(dú)的引用變量,在類中使用引用數(shù)據(jù)成員,將引用作為函數(shù)和方法的參數(shù),也可以讓函數(shù)或者方法返回引用。
1.1 引用變量
- 引用變量在創(chuàng)建時(shí)必須初始化,如下:
int ival = 3;
int &iRef = x;
- 在賦值后,iRef就是ival的另一個(gè)名稱。使用iRef就是使用ival的當(dāng)前值。對(duì)iRef賦值會(huì)改變ival的值。
無法在類外面聲明一個(gè)引用而不初始化它:
int &emptyRef; //編譯出錯(cuò)!
- 不能創(chuàng)建對(duì)未命名值(例如一個(gè)整數(shù)字面值)的引用,除非這個(gè)引用是一個(gè)const值。在下面的示例中,unnamedRef1將無法編譯,因?yàn)檫@是一個(gè)針對(duì)常量的non-const引用。
- 這條語句意味著可以改變常量5的值,而這樣做沒有意義。由于unnamedRef2是一個(gè)const引用,因此可以運(yùn)行,不能寫成"unnamedRef2=7"。
int &unnamedRef1 = 5; //編譯出錯(cuò)
const int &unnamedRef2 = 5; //正常運(yùn)行
(1) 修改引用
- 引用總是引用初始化的那個(gè)變量:引用一旦創(chuàng)建,就無法修改。這一規(guī)則導(dǎo)致了許多令人迷惑的語法。如果在聲明一個(gè)引用時(shí)用一個(gè)變量"賦值",那么這個(gè)引用就指向這個(gè)變量。然而,如果在此后使用變量對(duì)引用賦值,被引用變量的值就變?yōu)橘x值變量的值。引用不會(huì)更新為指向這個(gè)變量。示例代碼如下:
int x = 3,y = 4;
int &iRef = x;
iRef = y; //Changes value of x to 4. Doesn't make iRef refer to y.
- 如果試圖在賦值時(shí)取y的地址,以繞過這一限制:
int x = 3,y = 4;
int &iRef = x;
iRef = &y; //編譯出錯(cuò)
- 上面的代碼無法編譯。y的地址是一個(gè)指針,但iRef聲明為一個(gè)int的引用,而不是一個(gè)指針的引用。
- 如果將一個(gè)引用賦值給另一個(gè)引用時(shí),只是修改了其指向的值,而不是修改所指向的引用變量。(在初始化引用之后無法改變引用所指的變量;而只能改變該變量的值)
(2) 指針的引用和指向引用的指針
- 可以創(chuàng)建任何類型的引用,包括指針類型。下面給出一個(gè)指向int指針的引用例子:
int *pVal;
int *&ptrRef = pVal;
ptrRef = new int;
*ptrRef = 5;
- 這一語法有一點(diǎn)奇怪:你可能不習(xí)慣看到*和&彼此相鄰。然而,該語義上很簡單:ptrRef是pVal的引用,pVal是一個(gè)指向int的指針。修改ptrRef會(huì)更改pVal。指針的引用很少見,但是在某些場合下很有用,在1.3節(jié)中會(huì)討論這一內(nèi)容。
- 注意:
(i)對(duì)引用取地址的結(jié)果與被引用變量取地址的結(jié)果是相同的。
(ii)無法聲明引用的引用,或者指向引用的指針。
1.2 引用數(shù)據(jù)成員
- 類的數(shù)據(jù)成員可以引用,但是如果不是指向其他變量,引用就無法存在。因此,必須在構(gòu)造函數(shù)初始化器(constructor initializer)中初始化引用數(shù)據(jù)成員,而不是在構(gòu)造函數(shù)體內(nèi)。下面舉例說明:
class MyClass
{
public:
MyClass(int &iRef) : m_ref(iRef) {}
private:
int &m_ref;
};
1.3 引用參數(shù)
- C++程序員通常不會(huì)單獨(dú)使用引用變量或者引用數(shù)據(jù)成員。引用經(jīng)常用作函數(shù)或者方法的參數(shù)。默認(rèn)的參數(shù)傳遞機(jī)制是值傳遞:函數(shù)接收參數(shù)的副本。修改這些副本時(shí),原始的參數(shù)保持不變。引用允許指定另一種向函數(shù)傳遞參數(shù)的語義:按引用傳遞。當(dāng)使用引用參數(shù)時(shí),函數(shù)將引用作為參數(shù)。如果引用被修改,最初的參數(shù)變量也會(huì)修改。下面給出交換兩個(gè)數(shù)的例子來說明:
void swap(int &first, int &second)
{
int temp = first;
first = second;
second = temp;
}
- 可以采用下面的方式調(diào)用這個(gè)函數(shù):
int x = 5, y = 6;
swap(x, y);
- 當(dāng)使用x和y做參數(shù)調(diào)用函數(shù)swap()時(shí),first參數(shù)初始化為x的引用,second參數(shù)初始化為y的引用。當(dāng)swap()修改first和second時(shí),x和y實(shí)際上也被修改了。
就像無法使用常量初始化普通引用變量一樣,不能將常量作為參數(shù)傳遞給按引用傳遞參數(shù)的函數(shù):
swap(3, 4); //編譯出錯(cuò)
(1) 指針轉(zhuǎn)換為引用
- 某個(gè)函數(shù)或者方法需要一個(gè)引用做參數(shù),而你擁有一個(gè)指向被傳遞值的指針,這是一種常見的困境。在此情況下,可以對(duì)指針解除引用(dereferencing),將指針"轉(zhuǎn)換"為引用。這一行為會(huì)給出指針?biāo)傅闹?,隨后編譯器用這個(gè)值初始化引用參數(shù)。例如,可以這樣調(diào)用swap():
int x = 5, y = 6;
int *px = &x;
int *py = &y;
swap(*px, *py);
(2) 按引用傳遞與按值傳遞
- 如果要修改參數(shù),并修改傳遞給函數(shù)或者方法的變量,就需要使用按引用傳遞。然而,按引用傳遞的用途并不局限于此。按引用傳遞不需要將參數(shù)副本復(fù)制到函數(shù),在有些情況下會(huì)帶來兩面的好處:
(i)效率:復(fù)制較大的對(duì)象或者結(jié)構(gòu)需要較長的時(shí)間。按引用傳遞只是把指向?qū)ο蠡蛘呓Y(jié)構(gòu)的指針傳遞給函數(shù)。
(ii)正確性:并非所有對(duì)象都允許按值傳遞,即使允許按值傳遞的對(duì)象,也可能不支持正確的深度復(fù)制(deep copying)。(如果需要深度復(fù)制,動(dòng)態(tài)分配內(nèi)存的對(duì)象必須提供自定義復(fù)制構(gòu)造函數(shù)。) - 如果要利用好這些好處,但不想修改原始對(duì)象,可將參數(shù)標(biāo)記為const,從而實(shí)現(xiàn)按常理引用傳遞參數(shù)。按引用傳遞的這些優(yōu)點(diǎn)意味著,只有在參數(shù)是簡單的內(nèi)建類型(int或double),且不需要修改參數(shù)的情況下才應(yīng)該使用按值傳遞。其他情況下都應(yīng)該按引用傳遞。
1.4 引用作為返回值
- 可以讓函數(shù)或者方法返回一個(gè)引用,這樣做的主要作用是提高效率。返回對(duì)象的引用不是返回整個(gè)對(duì)象可以避免不必要的復(fù)制。當(dāng)然,只有涉及的對(duì)象在函數(shù)終止之后仍然存在的情況才能使用這一技巧。(如果變量的作用域局限于函數(shù)或者方法,例如:堆棧中分配的變量,在函數(shù)結(jié)束時(shí)會(huì)被銷毀。這個(gè)時(shí)候絕對(duì)不能返回這個(gè)變量的引用。)
- 返回引用的另一個(gè)原因是希望將返回值直接賦為左值(lvalue)(賦值語句在左邊)。一些重載的運(yùn)算符通常會(huì)返回引用。
1.5 使用引用還是指針
- 在C++中,引用有可能被認(rèn)為是多余的:幾乎所有使用引用可以完成的任務(wù)都可以用指針來代替完成。例如,可以這樣編寫swap()函數(shù):
void swap(int *first, int *second)
{
int temp = *first;
*first = *second;
*second = temp;
}
- 然而,這些代碼不如使用引用版本那么清晰:引用可以使程序整潔并易于理解。此外,引用比指針安全:不可能存在無效的引用,也不需要顯式地解除引用,因此不會(huì)遇到像指針那樣的解除引用問題。
- 大多數(shù)情況下,應(yīng)該使用引用而不是指針。對(duì)象的引用甚至可以像指向?qū)ο蟮闹羔樐菢又С侄鄳B(tài)性。只有在需要改變所指地址時(shí),才需要使用指針,因?yàn)闊o法改變引用所致的對(duì)像。例如,動(dòng)態(tài)分配內(nèi)存時(shí),應(yīng)該將結(jié)果存儲(chǔ)在指針而不是引用中。需要使用指針的第二種情況是可選參數(shù)。例如,指針參數(shù)可以定義為帶默認(rèn)值nullptr的可選參數(shù),而引用參數(shù)不能這樣定義。
- 還有一種方法可以判斷使用指針還是引用作為參數(shù)和返回類型:考慮誰擁有內(nèi)存。如果接受變量的代碼負(fù)責(zé)釋放相關(guān)對(duì)象的內(nèi)存,必須使用指向?qū)ο蟮闹羔?,最好是智能指針,這是傳遞擁有權(quán)的推薦方式。如果接受變量的代碼不需要釋放內(nèi)存,那么應(yīng)該使用引用。
- 注意:如果不需要改變所指的地址,就應(yīng)該使用引用而不是指針。
1.6 右值引用
- 在C++中,左值(lvalue)是可以獲取其地址的一個(gè)量,例如一個(gè)有名稱的變量。由于經(jīng)常出現(xiàn)在賦值語句的左邊,因此稱其為左值。另一方面,所有不是左值的量都是右值(rvalue),例如常量值,臨時(shí)對(duì)象或者臨時(shí)值。通常右值位于賦值運(yùn)算符的右邊。
- 右值引用是一個(gè)對(duì)右值(rvalue)的引用。特別地,這是一個(gè)當(dāng)右值是臨時(shí)對(duì)象時(shí)使用的概念。右值引用的目的是提供在涉及臨時(shí)對(duì)象時(shí)可以選用的特定方法。由于知道臨時(shí)對(duì)象會(huì)被銷毀,通過右值引用,某些涉及復(fù)制大量值的操作可以通過簡單的復(fù)制指向這些值的指針來實(shí)現(xiàn)。
- 函數(shù)可以將&&作為參數(shù)說明的一部分(例如 type&&name),來指定右值引用參數(shù)。通常,臨時(shí)對(duì)象被當(dāng)作const type&,但當(dāng)函數(shù)重載使用了右值引用時(shí),可以解析臨時(shí)對(duì)象,用于該重載。下面的示例說明了這一點(diǎn)。代碼首先定義了兩個(gè)incr()函數(shù),一個(gè)接受左值引用;另一個(gè)接受右值引用:
// Increment value using lvalue reference parameter.
void incr(int &value)
{
cout << "increment with lvalue reference" << endl;
++value;
}
// Increment value using rvalue reference parameter.
void incr(int &&value)
{
cout << "increment with rvalue reference" << endl;
++value;
}
- 可以使具有名稱的變量作為參數(shù)調(diào)用incr()函數(shù)。于是a是一個(gè)具有名稱的變量,因此調(diào)用接受左值引用的incr()函數(shù)。調(diào)用完incr()后,a的值將是11。
int a = 10, b = 20;
incr(a); //調(diào)用incr(int &value);
- 還可以用表達(dá)式作為參數(shù)來調(diào)用inrc()函數(shù)。此時(shí)無法使用接受左值引用作為參數(shù)的incr()函數(shù),因?yàn)楸磉_(dá)式a+b的結(jié)果是臨時(shí)的,這不是一個(gè)左值。在此情況下,會(huì)調(diào)用右值引用版本。由于參數(shù)是一個(gè)臨時(shí)值,當(dāng)incr()函數(shù)調(diào)用結(jié)束后,會(huì)丟失這個(gè)增加的值。
incr(a + b); //將調(diào)用incr(int &&value);
- 字面量也可以作為inrc()調(diào)用的參數(shù),此時(shí)同樣會(huì)調(diào)用右值引用版本,因?yàn)樽置媪坎荒茏鳛樽笾怠?/li>
incr(3); //將調(diào)用incr(int &&value);
- 如果刪除接受左值引用的incr()函數(shù),使用名稱的變量調(diào)用incr(),例如:incr(b),此時(shí)會(huì)導(dǎo)致編譯錯(cuò)誤,因?yàn)橛抑狄脜?shù)(int &&value)永遠(yuǎn)不會(huì)與左值(b)綁定。如下所示可以使用std::move()將左值轉(zhuǎn)換為右值,強(qiáng)迫編譯器調(diào)用incr()的右值版本。當(dāng)incr()調(diào)用結(jié)束后,b的值為21。
incr(std::move(b)); //將調(diào)用incr(int &&value);
- 右值引用并不局限于函數(shù)的參數(shù)??梢月暶饔抑狄妙愋偷淖兞?,并對(duì)其賦值,盡管這種用法并不常見。查下看如下代碼:
int &i = 2; //invalid:reference to a constant
int a = 2, b = 3;
int &j = a + b; //invalid:reference to a temporary
- 使用右值引用后,下面的代碼完全合法:
int &&i = 2;
int a = 2, b = 3;
int &&j = a + b;
- 前面示例中單獨(dú)使用右值引用的情況很少見。