徹底搞懂 c++ 函數參數的 & 和 &&

&

如果你在網上看到 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 的引用賦值給了 Warehousegood 成員。然而,這裏要注意,我們的 WarehouseBuilder 運行時的 good 生命週期僅僅爲 WarehouseBuilder 調用時,而 Warehousegood 引用生命週期要長,因此 Warehousegood 引用會引用到一個已經不存在的 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 && whoreWhore() 生命週期不一樣會發生什麼。(如果你有心看我囉囉嗦嗦寫到這裏,並且在用 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 綁定到同一地址

當然以上都是我的推斷,通過編譯器的行爲反推標準,非常的譚浩強,千萬不要被誤導了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章