左值和左值引用、右值和右值引用


1. 左值和右值

  • 左值(L-value):能用“取地址&”運算符獲得對象的內存地址,表達式結束后依然存在的持久化對象。左值可以出現在等號左邊也能夠出現在等號右邊。
  • 右值(R-value):不能用“取地址&”運算符獲得對象的內存地址,表達式結束后就不再存在的臨時對象。只能出現在等號右邊。

   - 可以做出以下三點理解:

     1)當一個對象被用作右值的時候,用的是對象的值(內容);而被用作左值的時候,用的是對象的身份(在內存中的位置)。總之:左值看地址,右值看內容。

     2)所有的具名變量或者對象都是左值,而右值不具名,如常見的右值有非引用返回的臨時變量、運算表達式產生的臨時變量、原始字面量和lambda表達式等。

        很難得到左值和右值的真正定義,但是有一個可以區分左值和右值的便捷方法:看能不能對表達式取地址,如果能,則為左值,否則為右值。

     3)右值要么是字面常量,要么是在表達式求值過程中創建的對象。

     特例:因為可以用&取得字符串字面值常量的地址,雖然它不能被賦值,但它是一個左值。

int main()
{
    char *p = "1234";
    printf("%d\n", p);
    printf("%d\n", &"1234");
}

   - 為什么右值不能用&取地址呢?

     1)對於臨時對象,它可以存儲於寄存器中,所以沒辦法用“取地址&”運算符;

     2)對於(非字符串)常量,它可能被編碼到機器指令的“立即數”中,所以沒辦法用“取地址&”運算符。

 

2. 左值引用和右值引用

   使用引用的目的就在於減少不必要的拷貝。

  • 左值引用:對左值的引用,就是給左值取別名。其基本語法如下:
Type &引用名 = 左值表達式;

   - 變量名實質上是一段連續存儲空間的別名,是一個標號(門牌號),通過變量的名字可以使用存儲空間。

   - 對一段連續的內存空間只能取一個別名嗎?

     在C++中新增加了引用的概念,引用可以看作一個已定義變量的別名,於是我們就可以通過引用為一個內存空間取多個別名。

int main()
{
    int a = 0;
    int &b = a;
    b = 11;
    return 0;
}

   - 普通引用在聲明時必須用其它的變量進行初始化,引用作為函數參數聲明時不進行初始化。

struct Teacher
{
    char name[64];
    int age;
};

void printfT(Teacher *pT) { cout << pT->age << endl; }

/*
 * pT是t1的別名, 相當於修改了t1
 */
void printfT2(Teacher &pT) { pT.age = 33; }

/*
 * pT和t1的是兩個不同的變量,t1 copy一份數據給pT, 只會修改pT變量 ,不會修改t1變量
 */
void printfT3(Teacher pT) { pT.age = 45; }

int main()
{
    Teacher t1;
    t1.age = 35;
    printfT(&t1);
    printfT2(t1);
    printf("t1.age:%d\n", t1.age)   // 33
    printfT3(t1);
    printf("t1.age:%d\n", t1.age);  //35
    return 0;
}

   - 對於引用語法,C++編譯器背后做了什么工作呢?

     首先我們知道引用單獨定義時,必須初始化,說明它很像一個常量。又因為引用是一個內存空間的別名所以它可以取地址。

     故我們可以得到引用的本質:

     1)引用在C++中的內部實現是一個常指針Type& name <=> Type* const name

     2) C++編譯器在編譯過程中使用常指針作為引用的內部實現,因此引用所占用的空間大小與指針相同。

     3) 從使用的角度,引用會讓人誤會其只是一個別名,沒有自己的存儲空間。這是C++為了實用性而做出的細節隱藏。

   - 函數返回值是引用(引用當左值)

     當函數返回值為引用時,若返回棧變量,不能成為其它引用的初始值,不能作為左值使用。若返回靜態變量或全局變量,

     可以成為其他引用的初始值,即可作為右值使用,也可作為左值使用。

     對於引用的理解可以直接看成指針,因為棧變量在函數結束后,內存空間就被釋放了,所以這個指針指向的內容就不對了。

   - 對指針的引用

struct Teacher
{
    char name[64];
    int age;
};

// 指針的引用
int getTe(Teacher* &myp)
{
    myp = (Teacher *)malloc(sizeof(Teacher));
    myp->age = 34;
    return 0;
}

int main()
{
    Teacher *p = NULL;
    getTe(p);
    printf("age:%d\n", p->age);
    return 0;
}

   - 常引用(const T &)

int main()
{
    int a = 10;
    int &b = a;        //普通引用
    const int &c = a;  //常量引用:只能通過c讀取a的內存空間

    // 常量引用初始化分為兩種
    // 1. 變量 初始化 常量引用
    int x = 20;
    const int& y = x;
    printf("y:%d\n", y);

    // 2. 常量 初始化 常量引用
    // int &m = 10; // 引用是內存空間的別名 字面量10沒有內存空間 沒有方法做引用
    const int &m = 10; 

    return 0;
}

  const引用結論

    1)Const & int e  相當於 const int * const e

    2)普通引用相當於 int *const e

    3)當使用常量(字面量)這類右值對const引用進行初始化時,C++編譯器會為常量值分配空間,並將引用名作為這段空間的別名

       初始化后,將生成一個只讀變量。只有常引用才可以用右值表達式初始化,這一點很重要,因為如果不加const,那么這個

       臨時的對象是無法進行傳遞給左值引用的,比如

MyString s = MyString("hello")    // 這個臨時對象本身就存在於內存空間,所以無需為這個右值分配空間

     因為MyString("hello")是一個臨時對象,即右值,所以MyString實現的拷貝構造函數參數不加const就會報錯。

 

  • 右值引用:對右值的引用,就是給右值取別名。其基本語法如下:
Type &&引用名 = 右值表達式;   // 如果是左值表達式,綁定就會出錯。這里雖然是個右值引用,但左側的具名變量本身是個左值

    - 開始介紹右值引用之前,先得了解到底啥是臨時對象?

      在C++中創建對象是一個費時、廢空間的一個操作,有些固然必不可少,但還有一些對象卻在我們不知道的情況下創建了。

      1)以值的方式給函數傳參

         給函數傳參有兩種方式----按值傳遞和按引用傳遞。按值傳遞時,首先將需要傳給函數的參數,調用拷貝構造函數創建

         一個副本,所有在函數里的操作都是針對這個副本的,也正是因為這個原因,在函數體里對該副本進行任何操作,都不會影響原參數。

class Test
{
public:
    int a, b;

public:
    Test(Test& t) : a(t.a), b(t.b) { printf("Copy function!\n"); }
    Test(int m = 0,int n = 0) : a(m), b(n) { printf("Construct function!\n"); }
    virtual ~Test() {}

public:
    int GetSum(Test ts)
    {
        int tmp = ts.a + ts.b;
        ts.a = 1000;           //此時修改的是tm的一個副本
        return tmp;
    }
};

int main()
{
    Test tm(10,20);
    printf("Sum = %d \n",tm.GetSum(tm));
    printf("tm.a = %d \n",tm.a);
    return 0;
}

         當函數執行結束后,這個臨時的對象就會被銷毀了。可以將 int GetSum(Test ts)改成 int GetSum(Test &ts) 來避免產生這個拷貝了。

      2)類型轉換生成的臨時對象

int main()
{
    Test tm(10,20), sum;
    sum = 1000;  // 調用 Test(int m = 0,int n = 0) 構造函數,還會調用一次賦值運算符
    printf("Sum = %d \n",tm.GetSum(sum));
}

      3)函數返回一個對象

         當函數需要返回一個對象,他會在棧中創建一個臨時對象或也叫匿名對象(如果是類對象,則會調用拷貝構造函數),存儲函數的返回值。

         這個臨時對象在表達式 sum = Double(tm) 結束后就自動銷毀了,這個臨時對象就是右值。

         按理說下面這個例子中Double函數返回時會觸發拷貝構造函數,但實際運行后卻沒有,猜想是被編譯器優化了,可以在編譯時設置編譯

         選項-fno-elide-constructors用來關閉返回值優化效果。

class Test
{
public:
    int a;

public:
    Test(Test& t) : a(t.a) { printf("Copy Construct!\n"); }
    Test(int m = 0) : a(m) { printf("Construct!\n"); }
    virtual ~Test() {};

public:
    Test& operator=(const Test& t)
    {
        a = t.a;
        printf("Assignment Operator!\n");
        return *this;
    }
};

Test Double(Test& ts)
{
    Test tmp;
    tmp.a = ts.a * 2;
    return tmp;
}

int main()
{
    Test tm(10), sum;
    sum = Double(tm);
    printf("sum.a = %d\n",sum.a);
    return 0; 
}

    - 引入右值引用的目的:右值引用是C++11中新增加的一個很重要的特性,它主要用來解決以下問題。

      1)函數返回臨時對象造成不必要的拷貝操作通過使用右值引用,右值不會在表達式結束之后就銷毀了,而是會被“續命”,

         的生命周期將會通過右值引用得以延續,和變量的聲明周期一樣長。

int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;
 
class Test
{
public:
    Test() { cout << "construct: " << ++g_constructCount << endl; }
    Test(const Test& a) { cout << "copy construct: " << ++g_copyConstructCount << endl; }
    ~Test() { cout << "destruct: " << ++g_destructCount << endl; }
};
 
Test GetTestObj() { return Test(); }
 
int main() 
{
    Test a = GetTestObj();
    return 0;
}

// 上面代碼關掉返回值優化后輸出:
construct: 1          // return Test()
copy construct: 1     // 臨時對象構造
destruct: 1           // return Test()對象銷毀
copy construct: 2     // a對象構造
destruct: 2           // 臨時對象銷毀
destruct: 3           // a對象銷毀

//-------------------------------------------------------------------------------------------------

// 但是如果使用右值引用來接收返回值呢?
int main() 
{
    Test &&a = GetTestObj();
    return 0;
}

// 輸出如下
construct: 1          // return Test()
copy construct: 1     // 臨時對象構造
destruct: 1           // return Test()對象銷毀
destruct: 2           // a這個對象其實就是那個臨時對象了,main結束后才銷毀

         通過右值引用,比之前少了一次拷貝構造和一次析構,原因在於右值引用綁定了右值,讓臨時右值的生命周期延長了。

         我們可以利用這個特點做一些性能優化,即避免臨時對象的拷貝構造和析構。

      2)通過右值引用傳遞臨時參數:使用字面值(如1、3.15f、true),或者表達式等臨時變量作為函數實參傳遞時,按左值引用傳遞參數會被編譯器阻止。

         而進行值傳遞時,將產生一個和參數同等大小的副本。C++11提供了右值引用傳遞參數,不申請局部變量,也不會產生參數副本。

static float  global = 1.111f;

void offset(float &&f) { global += f; }   // 通過右值引用傳遞參數
void offset(float& f)  { global -= f; }   // 重載了offset函數,而且是左值傳遞
float getFloat() { return 4.444f; }

int main()
{
    float u = 10.000f;
    cout << "global:" << global << endl;

    offset(3.333f);   // 這里會調用右值引用參數的函數
    cout << "global:" << global << endl;

    offset(getFloat() + 2.222);
    cout << "global:" << global << endl;

    offset(u);        // 執行的是按左值引用的offset函數,右值引用無法初始化為左值.
    cout << "global:" << global << endl;
    return 0;
}

       對於非模板函數,函數參數有確定的類型,右值引用只能與右值綁定,只接收右值實參,可以將它看作是臨時變量的別名,不會將臨時

         變量再復制1次,和按值傳遞相比提高了效率。這一點同3)進行區別。

      3)模板函數中如何按照參數的實際類型進行轉發:當右值引用和模板結合的時候,就復雜了。T&&並不一定表示右值引用,它可能是個左值

         引用又可能是個右值引用。如果函數模板表示的是右值引用的話,肯定是不能傳遞左值的,但是事實卻是可以。這里的&&是一個未定義的引用類型,

         稱為universal references,它必須被初始化,它是左值引用還是右值引用卻決於它的初始化,如果它被一個左值初始化,它就是一個左值引用;

         如果被一個右值初始化,它就是一個右值引用。

         注意:只有當發生自動類型推斷時(如函數模板的類型自動推導,或auto關鍵字),&&才是一個universal references

// Test是一個特定的類型,不需要類型推導,所以&&表示右值引用  
template<typename T>
class Test 
{
  Test(Test&& rhs);
};

// 右值引用
void f1(Test&& param);

// 在調用這個f之前,這個vector<T>中的推斷類型已經確定了,所以調用f函數的時候沒有類型推斷了,所以是右值引用
template<typename T>
void f2(std::vector<T>&& param); 

// universal references僅僅發生在 T&& 下面,任何一點附加條件都會使之失效, 所以是右值引用
template<typename T>
void f3(const T&& param);

// 這里T的類型需要推導,所以 && 是一個 universal references
template<typename T>
void f(T&& param);

int main()
{
    int x = 1;
    int && a = 2;
    string str = "hello";
    f(1);               // 參數是右值 T 推導成了int, 所以是int&& param, 右值引用
    f(x);               // 參數是左值 T 推導成了int&, 所以是int&&& param, 折疊成 int&, 左值引用
    f(a);               // 雖然 a 是右值引用,但它還是一個左值,T推導成了int&
    f(str);             // 參數是左值, T 推導成了string&
    f(string("hello")); // 參數是右值, T 推導成了string
    f(std::move(str));  // 參數是右值, T 推導成了string
}

          所以最終還是要看T被推導成什么類型,如果T被推導成了string,那么T&&就是string&&,是個右值引用,如果T被推導為string&

          就會發生類似string& &&的情況,對於這種情況,c++11增加了引用折疊的規則,本質如下:

              所有的引用折疊最終都代表一個引用,要么是左值引用,要么是右值引用。規則就是:

              如果任一引用為左值引用,則結果為左值引用。否則(即兩個都是右值引用),結果為右值引用。

          引用折疊存在四種情形,根據上面的規則我們可以知道:

              1)左值-左值 T& &     <=>   int &

              2)左值-右值 T& &&    <=>   int &

              3)右值-左值 T&& &    <=>   int &

              4)右值-右值 T&& &&   <=>   int &&

          因為1,2,3中都存在一個左值引用。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM