基本概念:
左值:在內(nèi)存中有可以訪問的地址,對象是一個左值。
右值:不可以取地址,整數(shù)10是個右值。
引用:對象的別名,沒有創(chuàng)建新的對象,僅僅給已經(jīng)存在的對象賦予了一個新的名字。
引用是對象的別名,對于引用的一切操作都是對對象的操作;
引用自身從概念上沒有大?。ɑ蛘呔褪菍ο蟮拇笮。坏迷趥鬟f或需要存儲時,其傳遞或存儲的大小為地址的大小。
引用必須初始化;
引用不可能重新綁定;
將指針所指向的對象綁定到一個引用時,需要確保指針非空。
任何引用類型的變量,都是左值。
四種類型引用:
| 類型 | 例子 | 備注 |
|---|---|---|
const lvalue refrence |
Foo foo(10); const Foo& ref = foo; | |
const rvalue refrence |
const Foo& ref = Foo(10); | |
non-const lvalue refrence |
Foo foo(10); Foo& ref = foo; | |
non-const rvalue refrence |
Foo&& ref=Foo(10); | C++11才開始有 |
move語義:
C++11 之前,只有 copy 語意,這對于極度關(guān)注性能的語言而言是一個重大的缺失。
對move 語意的急迫需求,到了 C++11 終于被引入。其直接的驅(qū)動力很簡單:在構(gòu)造或者賦值時,如果等號右側(cè)是一個中間臨時對象,應(yīng)直接將其占用的資源直接 move 過來(對方就沒有了)。
但問題是,如何讓一個構(gòu)造函數(shù),或者賦值操作重載函數(shù)能夠識別出來這是一個臨時變量?
/////////////hello.cpp/////////////////
#include <iostream>
using namespace std;
struct Foo
{
Foo(){ cout << "Foo()" << endl; }
Foo(const Foo&ref){ cout << "Foo(const Foo&)" << endl; } // copy ctor
Foo(Foo&& ref){ cout << "Foo(Foo&&)" << endl; } // move ctor
Foo& operator=(const Foo& rhs){cout << "Foo& operator=(const Foo& rhs)" << endl; } // copy assignment
Foo& operator=(Foo&& rhs){cout << "Foo& operator=(Foo&& rhs)" << endl; } // move assignment
~Foo(){ cout << "~Foo()" << endl; }
};
int main(int argc, char* argv[])
{
cout<<"=========="<<endl;
Foo foo1 = Foo();
cout<<"=========="<<endl;
foo1 = Foo();
cout<<"=========="<<endl;
Foo foo2 = foo1;
cout<<"=========="<<endl;
foo2 = foo1;
getchar();
return 1;
}
實參類型為non-const lvalue reference、const lvalue reference、 const rvalue reference可以匹配到copy ctor和copy assignment。
實參類型為non-const rvalue reference 才能匹配到 move ctor和 move assignment 。
通過這樣的方式,讓 Foo foo1 = Foo()或 foo1 = Foo()這樣的表達式,都可以匹配到 move 語意的版本。
與此同時,讓 Foo foo2 = foo1 或 foo2 = foo1 這樣的表達式,依然使用 copy 語意的版本。
達到以上效果需要編譯時加上
-fno-elide-constructors,以此關(guān)閉編譯器省略創(chuàng)建一個只是為了初始化另一個同類型對象的臨時對象的優(yōu)化。root@ubuntu-Vostro-3268:/mnt/zpp# g++ hello.cpp -fno-elide-constructors root@ubuntu-Vostro-3268:/mnt/zpp# root@ubuntu-Vostro-3268:/mnt/zpp# root@ubuntu-Vostro-3268:/mnt/zpp# ./a.out ========== Foo() Foo(Foo&&) ~Foo() ========== Foo() Foo& operator=(Foo&& rhs) ~Foo() ========== Foo(const Foo&) ========== Foo& operator=(const Foo& rhs)使用編譯器優(yōu)化時:
root@ubuntu-Vostro-3268:/mnt/zpp# g++ hello.cpp root@ubuntu-Vostro-3268:/mnt/zpp# ./a.out ========== Foo() ========== Foo() Foo& operator=(Foo&& rhs) ~Foo() ========== Foo(const Foo&) ========== Foo& operator=(const Foo& rhs)
練習:
struct Foo
{
Foo(int a) :a(a){}
int a;
};
void test1(Foo&& f)
{
// 對于任何類型為 右值引用的變量(當然也包括函數(shù)參數(shù)),只能由右值來初始化;
}
void test2(Foo& f)
{
// 一個右值,不能被 T& 類型的參數(shù)匹配;畢竟這種可以修改的承諾。而修改一個調(diào)用后即消失的臨時
// 對象上,沒有任何意義,反而會導(dǎo)致程序員犯下潛在的錯誤,因而還是禁止了最好
}
void test3(const Foo& f)
{
}
Foo f1(1);
test1(f1); // wrong cannot bind ‘Foo’ lvalue to ‘Foo&&’ 不能將一個左值綁定到右值引用
test2(f1); // ok
test3(f1); // ok
test1(Foo{1}); // ok Foo{1}是右值
test2(Foo{1}); // wrong 這種做法無意義,invalid initialization of non-const reference of type ‘Foo&’ from an rvalue of type ‘Foo’
test3(Foo{ 1 }); // ok
// ref是左值雖然其類型是右值引用;
// 一個類型為 右值引用的變量,一旦被初始化之后,臨時對象的生命將被擴展,會在其被創(chuàng)建的 scope 內(nèi)始終有效。
// 看似 ref 被定義的類型為 右值引用,但這僅僅約束它的初始化:只能從一個 右值進行初始化。
// 但一旦初始化完成,它就和一個 左值引用再也沒有任何差別:都是一個已存在對象的 標識。
Foo&& ref = Foo{1};
test1(ref); // wrong ref是左值,test1的形參為右值引用,右值引用的變量只能由右值來初始化 cannot bind ‘Foo’ lvalue to ‘Foo&&’
test2(ref); // ok
test3(ref); // ok
速亡值:
只有右值臨時對象可以初始化右值引用變量,從而也只有右值臨時變量能夠匹配參數(shù)類型為 右值引用(T&&)的函數(shù),包括 move 構(gòu)造函數(shù)。
如果程序員就是想把一個左值 move 給另外一個對象,該怎么辦?最簡單的選擇是通過 static_cast 進行類型轉(zhuǎn)換:
Foo foo{10}; // foo為左值
Foo&& ref = Foo{10}; // ref為左值 類型為右值引用
Foo obj1 = static_cast<Foo&&>(foo); // move 構(gòu) 造
Foo obj2 = static_cast<Foo&&>(ref); // move 構(gòu) 造
我們之前說過,只有 右值,才可以用來初始化一個 右值引用類型的變量,因而也只有 右值才能匹配 move構(gòu)造。
所以,static_cast<Foo&&>(foo) 表達式,肯定是一個 右值。
但同時,它返回的類型又非常明確的是一個 引用,而這一點又不符合 右值的定義。
因為,所有的右值,都必須是一個 具體類型,不能是不完備類型,也不能是抽象類型,但 引用,無論左值引用,還是右值引用,都可以是不完備類型的引用或抽象類型的引用。這是 左值才有的特征。
對于這種既有左值特征,又和右值臨時對象一樣,可以用來初始化右值引用類型的變量的表達式,只能將其歸為新的類別。C++11 給這個新類別命名為 速亡值 (eXpiring value,簡稱 xvalue)。
而將原來的 右值,重新命名為 純右值。而 速亡值和 純右值合在一起,稱為 右值,其代表的含義是,所有可以直接用來初始化 右值引用類型變量的表達式。
同時,由于 速亡值又具備左值特征:可以是不完備類型,可以是抽象類型,可以進行運行時多態(tài)。所以,速亡值又和 左值一起被歸類為 泛左值(generalized lvalue, 簡稱 glvalue)。
? 類型為 右值引用的變量,只能由 右值表達式初始化;
? 右值包括 純右值和 速亡值,其中 速亡值的類型是 右值引用;
? 類型為 右值引用的變量,是一個 左值,因而不能賦值給其它類型為 右值引用的變量,當然也不能匹配參數(shù)類型為 右值引用的函數(shù)。