目錄
一、右值引用:
1、左值與右值:
2、左值引用和右值引用:
二、右值引用的使用場景:
1、左值引用的使用場景:
2、右值引用的使用場景:
移動構造
移動賦值
三、完美轉發:
1、萬能引用:
2、實際使用:
一、右值引用:
1、左值與右值:
在了解右值引用前,要先了解什么是左值引用(其實在之前已經使用過很多回了),那么要了解什么是左值引用,就先要了解什么是左值什么是右值
在等號左邊的值叫左值嗎?在等號右邊的值叫右值嗎??
顯然定義不會這么簡單的,但是左值可以出現賦值符號的左邊,右值不能出現在賦值符號左邊,也就是說在等號左邊的值一定不是右值,在等號右邊的可以是左值或右值
左值:
能夠進行取地址的操作,也可以被修改
如下的a,b,c都是左值
int main()
{int a = 10;const int b = 10;int* c = new int(0);return 0;
}
右值:
不能夠進行取地址的操作,一般不能夠被修改
如下,10,x,fmin(x,y)的返回值就是右值
int main()
{double x = 1.1, y = 2.2;//如下就是右值10;x + y;fmin(x, y);return 0;
}
理解:
1、右值的本質是一個臨時變量或者常量值
2、這些臨時變量是沒有被實際存儲起來的,所以無法對右值取地址????????
3、像上述的fmin的返回值,其實際上就是一份臨時拷貝,所以算作右值
2、左值引用和右值引用:
什么是左值引用:
左值引用就是給左值取別名
int main()
{int a = 10;const int b = 10;int* c = new int(0);//這就是左值引用int& pa = a;const int& pb = b;int*& pc = c;return 0;
}
什么是右值引用:
右值引用就是給右值取別名
int main()
{double x = 1.1, y = 2.2;//如下就是右值10;x + y;fmin(x, y);//這個就是右值引用int&& p1 = 10;int&& p2 = x + y;int&& p3 = fmin(x, y);return 0;
}
這里在給右值取別名后,右值會被存儲到特定的位置,此時就能夠取到該位置的地址了
左值引用可以引用右值嗎 ----- 可以
但是,左值引用不能直接引用右值,因為右值不能夠被修改,左值可以修改,如果直接引用的話權限會存在放大問題,所以如果想要左值引用右值就需要加上const修飾
像我們之前在函數參數傳參的時候經常寫const T& x,這就是保證既能夠傳左值,又能夠傳右值
template<class T>
void func(const T& val)
{cout << val << endl;
}
int main()
{string s("111");func(s); //s為左值func("222"); //"222"為右值return 0;
}
右值引用可以引用左值嗎 ----- 可以
但是,右值引用也不能直接引用左值,如果想要引用左值,就需要加上move后的左值
int main()
{int a = 10;//右值引用給左值取別名int&& pa = move(a);return 0;
}
為什么加上move后才能讓右值引用來引用左值呢?
我們首先要知道,左值引用或者右引用都是在給資源取別名,對于左值引用,就是直接指向原本的數據,對于右值引用,我們知道原本是沒有空間資源的,那么右值引用引用右值就是首先開辟一塊空間,然后將常量或者臨時變量轉移到開辟好的地方,然后在指向該地方
所以右值引用的本質是對右值進行資源的轉移
此時就有空間資源了,此時就能夠取地址了,并且能夠對其進行修改了
對于常量,臨時變量,表達式的結果這些右值,編譯器在右值引用的時候會直接將這些右值進行轉移資源,但是對于左值,編譯器不敢直接轉移,這個時候編譯器就為用戶提供了一個函數move,當進行move左值的時候,就能夠讓右值引用 引用左值了
二、右值引用的使用場景:
1、左值引用的使用場景:
左值引用既能夠引用左值,又能夠引用右值,但是還是存在短板,所以在C++11里面,引入了右值引用來彌補左值引用的短板
在左值引用中:
1、左值引用做參數,防止傳參是的拷貝
2、左值引用做返回值,防止返回時對返回對象進行拷貝
首先,寫一個自己的string類,在里面寫上部分cout來方便打印觀察
namespace ppr
{class string{public:typedef char* iterator;iterator begin(){return _str;//返回字符串中第一個字符的地址}iterator end(){return _str + _size;//返回字符串中最后一個字符的后一個字符的地址}//構造函數string(const char* str = ""){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);}//交換兩個對象的數據void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//拷貝構造函數(現代寫法)string(const string& s):_str(nullptr),_size(0),_capacity(0){cout << "string(const string& s) -- 深拷貝" << endl;string tmp;swap(tmp);}//移動構造函數(現代寫法)string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移動構造" << endl;swap(s);}//賦值運算符重載(現代寫法)string& operator=(const string& s){cout << "string& operator=(const string& s) -- 深拷貝" << endl;string tmp;swap(tmp);return *this;}//析構函數~string(){delete[] _str;_str = nullptr;_size = 0;_capacity = 0;}//[]運算符重載char& operator[](size_t i){assert(i < _size);return _str[i];}//改變容量,大小不變void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strncpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}}//尾插字符void push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_str[_size + 1] = '\0';_size++;}//+=運算符重載string operator+=(char ch){push_back(ch);string tmp(*this);return tmp;}//返回C類型的字符串const char* c_str()const{return _str;}private:char* _str;size_t _size;size_t _capacity;};
}
接著看看左值引用的使用場景
//首先,看看左值引用的使用場景
//值傳參
void func1(ppr::string s)
{cout << "void func1(ppr::string s)" << endl;
}
//左值引用傳參
void func2(const ppr::string& s)
{cout << "void func2(const ppr::string& s)" << endl;
}int main()
{ppr::string ret1("1111111111111");func1(ret1);//這里采用深拷貝func2(ret1);ret1 += '0';//里面return *this 的時候,也會進行拷貝構造return 0;
}
其中,在func1的時候,傳值傳參會進行一次拷貝構造,在+=那里,返回* this的時候,也會進行一次拷貝構造這樣的話會看到兩次深拷貝
左值引用短板:
左值引用能夠避免不必要的拷貝構造,但是并不能完全避免
左值引用做參數的時候,能夠完全避免傳參時的拷貝
左值引用做返回值的時候,不能完全避免拷貝
比如如果返回的是一個局部變量,在返回的時候局部變量被銷毀了,此時如果使用左值引用進行返回,就會返回的野指針,此時就不能夠用左值引用返回,需要老老實實地值拷貝
如下會進行兩次拷貝操作,然后將ret返回
如果在新一點的編譯器會進行優化,只需進行一次拷貝操作
如果是引用傳參,此時局部變量的局部空間,在出了函數作用域之后就會被釋放,此時就會出問題
所以,C++11為了解決這類問題,提出了右值引用來解決這種場景
2、右值引用的使用場景:
右值分為 純右值 和 將亡值
純右值:內置類型的右值
將亡值:自定義類型的右值
右值引用和移動語句解決上述問題的方式就是,給當前模擬實現的string類增加移動構造方法
移動構造
移動構造本質是將參數右值的資源竊取過來,占位已有,那么就不用做深拷貝了,所以它叫做移動構造,就是竊取別人的資源來構造自己
如果沒加上述的移動拷貝,就會出現深拷貝
如果加上移動構造,那么就會走移動構造函數,這樣就能更加減少拷貝
移動構造的本質就是將參數的右值竊取過來,占為己有,這樣它就不用再深度拷貝了,所以叫做移動構造
移動構造和拷貝構造的區別:
1、在沒有增加移動構造之前,由于拷貝構造采用的是const左值引用接收參數,因此無論拷貝構造對象時傳入的是左值還是右值,都會調用拷貝構造函數
2、增加移動構造之后,由于移動構造采用的是右值引用接收參數,因此如果拷貝構造對象時傳入的是右值,那么就會調用移動構造函數
3、string的拷貝構造函數做的是深拷貝,而移動構造函數中只需要調用swap函數進行資源的轉移,因此調用移動構造的代價比調用拷貝構造的代價小
左值引用:直接引用對象以減少拷貝
右值引用:間接減少拷貝,將臨時資源等將亡值的資源通過 移動構造 進行轉移,減少拷貝
移動賦值
移動賦值是一個賦值運算符重載函數,該函數的參數是右值引用類型的,移動賦值也是將傳入右值的資源竊取過來,占為己有,這樣就避免了深拷貝,之所以它叫移動賦值,就是竊取別人的資源來賦值給自己的意思
// 賦值重載
string& operator=(const string& s)
{cout << "string& operator=(string s) -- 深拷貝" << endl; string tmp(s); swap(tmp); return *this;
}
//移動賦值
string& operator=(string&& s)
{cout << "string& operator=(string && s) -- 移動拷貝" << endl; swap(s); return *this;
}
string& operator=(const string& s) 和string& operator=(string&& s) 的區別:
1、在沒有string& operator=(string&& s) 的時候,如果進行=操作,那么無論是左值還是右值傳參都會調用string& operator=(const string&?s) 這個函數
2、在增加移動賦值后,如果是左值就調用原來的函數,如果是右值就調用新加的移動賦值函數
3、移動賦值函數是通過swap函數進行資源的交換,而原來的operator=是通過深拷貝進行,因此,移動賦值的代價比原來的要小
三、完美轉發:
1、萬能引用:
template<class T>
void PerfectForward(T&& t)
{//...
}
這里函數中的參數并不是右值引用,如果傳的模板是左值,這里的參數就是左值引用,相反如果傳的模板是右值,那么這里的參數就是右值引用
void func(int& a)
{cout << "左值引用" << endl;
}
void func(const int& a)
{cout << "const 左值引用" << endl;
}
void func(int&& a)
{cout << "右值引用" << endl;
}
void func(const int&& a)
{cout << "const 右值引用" << endl;
}
template<class T>
void perfectForward(T&& val)
{func(val);
}int main()
{int a = 10;perfectForward(a); //左值const int b = 10; //const 左值perfectForward(b);perfectForward(move(a)); // 右值perfectForward(move(b)); //const 右值return 0;
}
如上,這就是通過func函數重載,來觀察編譯器會怎樣進行函數調用
如上,這是運行結果,為什么會這樣呢?難道是編譯器做的不對嗎,在實際調用中,4個函數沒有一個是進入了右值引用,均匹配的是左值引用版本,這是為什么呢?
當對右值進行引用后,會導致右值被存儲到特定的位置,此時就能夠對這個引用后的右值進行取地址了,這樣的話,這個右值就模版被識別成左值了
也就是說,在右值引用過一次后,會導致右值變成左值,但是如果想要繼續保證其右值的屬性,此時就需要用到完美轉發
如上,在對右值引用進行傳參的時候,在前面加上forward<T>,這樣經過完美轉發后,調用PerfectForward函數時傳入的是右值就會匹配到右值引用版本的Func函數,傳入的是const右值就會匹配到const右值引用版本的Func函數
forward是一個模板函數,需要指定模板參數類型T,確保能正確推導并傳遞
2、實際使用:
首先實現一個建議的list,在其中實現左值引用的push_back和insert函數
namespace ppr
{template<class T>struct ListNode{T _data;ListNode* _next = nullptr;ListNode* _prev = nullptr;};template<class T>class list{typedef ListNode<T> node;public://構造函數list(){_head = new node;_head->_next = _head;_head->_prev = _head;}//左值引用版本的push_backvoid push_back(const T& x){insert(_head, x);}//右值引用版本的push_backvoid push_back(T&& x){insert(_head, x);}//左值引用版本的insertvoid insert(node* pos, const T& x){node* prev = pos->_prev;node* newnode = new node;newnode->_data = x;prev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}//右值引用版本的insertvoid insert(node* pos, T&& x){node* prev = pos->_prev;node* newnode = new node;newnode->_data = x;prev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:node* _head; //指向鏈表頭結點的指針};
}
接著進行左值和右值的push_back版本的調用
int main()
{ppr::list<ppr::string> lt;ppr::string s1("111111111111111");//左值的push_backlt.push_back(s1);cout << endl << endl;ppr::string s2("111111111111111");//右值的push_backlt.push_back(move(s2));cout << endl << endl;lt.push_back("22222222222222222");//右值的push_backreturn 0;
}
但是會發現全部都是深拷貝,這和上述右值被引用后,就可以取地址了,就變成左值了,所以為了避免這種情況,就需要在右值版本的push_back和insert加上完美轉發,讓右值能夠保存右值屬性