&
如果你在網上看到 c++ 的幾種傳參方式,肯定就分成兩種,“值傳遞”和“引用傳遞”。值傳遞很簡單,複製一份就是了;“引用傳遞”就說的馬馬虎虎了。“傳遞的是實參的本身”,說起來很輕鬆,實際上很有問題。最簡單的一個問題就是:“實參”本身不是一個東西怎麼辦?例如:
void f_ck(int & i) {
i++;
}
...
f_ck(1); // 編譯不通過,VS答曰:非常量的引用值必須是左值。
...
當然我們後來也明白,&
是左值引用,所謂左值就是實際在運行時內存中存在的值,而不是 1
這種寫死在運行代碼中的值。
生命週期問題
有時候即使我們賦予了左值,依然會出問題,比如下面這段代碼:
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
class Good {
public:
string goodname;
Good() {
goodname = "not_good";
cout << "Good constructed" << endl;
}
~Good() {
cout << "Good destoyed" << endl;
}
};
class Warehouse {
public:
Good & good;
Warehouse(Good & good) : good(good)
{
}
static Warehouse* WarehouseBuilder() {
Good good;
return new Warehouse(good);
}
};
int main()
{
Warehouse * warehouse = Warehouse::WarehouseBuilder();
cout << warehouse->good.goodname << endl;
getchar();
return 0;
}
這段代碼很簡單,調用了一個 Builder
函數,返回了一個類。其中 WarehouseBuilder
將棧中的 good
的引用賦值給了 Warehouse
的 good
成員。然而,這裏要注意,我們的 WarehouseBuilder
運行時的 good
生命週期僅僅爲 WarehouseBuilder
調用時,而 Warehouse
的 good
引用生命週期要長,因此 Warehouse
的 good
引用會引用到一個已經不存在的 Good
,這時我們運行的 cout << warehouse->good.goodname << endl;
就會報錯。
以下是輸出:
Good constructed
Good destoyed
報錯信息爲:
0x0F8F69E6 (msvcp140d.dll)處(位於 ConsoleApplication2.exe 中)引發的異常: 0xC0000005: 讀取位置 0xCCCCCCCC 時發生訪問衝突。
可以看到我們這裏有一個 0xCCCCCCCC
的填充內存,這是調試模式下 good
中的 string
析構時留下的印記,如果我們改成 Release
呢?
這時並沒有任何報錯。因爲 VS
沒有爲我們進行析構時的填充。
那麼怎麼解決這個問題呢?很顯然,你需要 new
一個。
&&
&&,就是右值引用了。根據某些博客的定義,右值是可以出現在等式右邊的值,但實際上呢?
一個合理的猜測是:&& 和 & 作爲右值時,有類似的行爲,只是我們對這個值的權限不一樣。右值 && 顯然不能放在等號左邊。下面用 VS 驗證一下。
結果就打臉了。
class Whore {
public:
string nickname;
};
class Brothel {
public:
Whore && whore_;
Brothel(Whore && whore): whore_(whore) { //編譯不通過,無法將右值引用綁定到左值,原因之後解釋
}
};
如果我們換個寫法:
class Whore {
public:
string nickname;
};
class Brothel {
public:
Whore && whore_;
Brothel(Whore && whore) { // 編譯不通過,“Brothel::whore_”: 必須初始化引用
whore_ = whore;
}
};
再換種寫法:
class Whore {
public:
string nickname;
};
class Brothel {
public:
Whore & whore_;
Brothel(Whore && whore): whore_(whore) {
}
};
這裏,將成員 whore_
變成了 左值引用。
然後我們再看,能否“正常地”初始化 Brothel
...
Whore && whore = Whore();
Brothel(whore); // 編譯不通過
...
爲啥這樣不行呢?這不是幻覺,按理說,Whore
也是右值引用,它憑什麼不能傳遞給 Brothel
的構造函數作爲參數呢?
根據我的理解,原理是這樣的:
當進行
Whore && whore = Whore();
時,這一操作的實質是 Whore()
本來是個匿名的實體,它 本來不被需要,就像 int a, b; b = a + 1;
中的 a + 1
一樣,是轉瞬即逝的值,算完就扔掉了(複製給 b
了,結果就不再被需要了),但是我們用 Whore && whore = Whore();
其實就是告訴編譯器,我們給它起了個名字,叫做 whore
。它被固定下來了。
我們可以這樣操作:
whore.nickname = "Crystal";
完美!就像 Whore whore = Whore()
一樣。
Whore constructed
Crystal
所以 Whore && whore = Whore();
執行以後,whore
就變成了實打實的 Whore()
(literally,這一行的Whore()
)的一個別名,它被捕獲了,whore
的語義發生了變化,它就是 Whore()
。
照理來講,如果我們認爲 Whore()
是一個右值,一個表達式裏算完就可以扔掉不要的值,那麼 我們執行
Whore && whore = Whore();
似乎是在延長 Whore()
的生命週期。
到這裏,我們就可以填上剛纔那個 “原因之後解釋” 的坑了。
因爲傳遞參數進來以後,whore
已經變成了實體,它有了名字,自然不能再綁定給另一個右值了。
這很奇怪。這不符合 c++ 一貫的風格。那麼我們看看如果 Whore && whore
和 Whore()
生命週期不一樣會發生什麼。(如果你有心看我囉囉嗦嗦寫到這裏,並且在用 VS 驗證,記得把 Release 調成 Debug )
Whore && F_ckWhore() {
return Whore(); // 返回值捕獲了匿名值 Whore(),但是我們接下來可以看到 Whore() 去世了,引用無效了。
}
int main()
{
Whore && whore2 = F_ckWhore();
cout << whore2.nickname << endl; // 0x5D6669E6 (msvcp140d.dll)處(位於 ConsoleApplication2.exe 中)引發的異常: 0xC0000005: 讀取位置 0xFFFFFFFF 時發生訪問衝突
getchar();
return 0;
}
可以看到,這裏確實 Whore()
作爲在 F_ckWhore()
的棧上生存的玩意,確實在函數返回後被析構了。
我們可以這樣認爲,用 type && r
去引用一個匿名的右值(右值當然是匿名的),確實延長了其生命週期,但是這有個限度,就是不能超過棧的生命週期。它的內部原理可能是強制中間變量存儲在棧上,而不允許其被優化掉。
總結
總結一下,c++ 中的左值就像指針,它可以捕獲實實在在的實體,但是我們要注意被捕獲值的生命週期。不要隨便把生命週期和棧同步的實體傳給了它;
右值其實也是指針,但是它功能是專門捕獲匿名的實體(可以理解爲編譯產生的中間變量)。同時我們要注意,右值在定義時捕獲了實體以後,右值的名字就變成了被捕獲的實體。更加明確一些,就是(這是我猜的)
//你的程序
int && b = a + 1;
------
//編譯過程
t1 = a + 1;
b 和 t1 綁定到同一地址
當然以上都是我的推斷,通過編譯器的行爲反推標準,非常的譚浩強,千萬不要被誤導了