網站首頁 編程語言 正文
一、左值與右值
顧名思義,左值就是只能放在等號左邊的值,右值是只能放在等號右邊的值。
在C++Prime一書中,對左值和右值的劃分為,左值是一個表示數據的表達式,右值是一個即將銷毀的值(通常稱為將亡值)。比如我們定義的一個變量就是一個左值,而字面常量,表達式返回值,傳值返回函數的返回值就是右值。
10;//右值
int a = 10;//a是左值
add(2, 3);//右值
x+y;//右值
const int a;//左值
注意,const類型的變量是不能放在等號左側來為它賦值的,但是他是一個左值。
這里給出一個區分兩者的方式:可以取地址的就是左值,不能取地址的就是右值!
二、左值引用與右值引用
我們之前所寫的引用都是左值引用符號是&,左值引用的底層是使用指針,它的作用是為對象取一個別名。
而右值引用就是給右值取別名,它的符號是&&,右值引用開辟了空間,得到的一個對象是左值。
int a = 10;
int& d = a;//左值引用
int&& e = 10;//右值引用
int&& f = a + 1;
int&& c = add(2, 3);
左值引用不能給右值取別名,右值引用也不能給左值取別名。但是如果對左值進行move(),對左值引用加上const是可以這樣進行的。
move的意思就是保證除了賦值和銷毀之外,不再使用該左值,即將a的屬性轉移到了e中,對左值move后是一共右值。
int&& c = a;//右值引用不能給左值取別名
int& d = add(3, 4);//左值引用不能給右值取別名
int&& e = move(a);//當對左值加move的時候可以
const int& f = add(3, 4);//當對引用加const后可以取別名
同時右值引用不像左值引用一樣具有傳遞性:
int&& a = 10;
a=20;
cout<<&a<<endl;
//int&& b = a;//錯誤
這是因為a是一個左值,我們可以打印a的地址,右值經過引用后得到的對象是一個左值。因此我們是可以對a進行賦值的。
三、右值引用應用
1.移動構造與移動賦值
移動構造與移動賦值在C++11中已經加入了STL容器的函數中:
string(string&& str) //移動構造
string& operator=(string&& str)//移動賦值
移動構造與移動賦值都是向函數中傳入右值引用,它們的本質與右值基本相同,就是將一個將亡值的數據轉移給另一個值。
我們可以在函數string中模擬實現一下移動構造和移動賦值,它們的本質就是調用swap函數完成賦值,而不是使用strcpy創建一個新對象。
1.模擬實現的string
為了方便觀察,我們使用自己模擬實現的string來進行說明:
namespace my_string
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
//構造函數
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷貝構造
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 深拷貝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移動構造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 資源轉移" << endl;
this->swap(s);
}
// 移動賦值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 轉移資源" << endl;
swap(s);
return *this;
}
//賦值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷貝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
//下標訪問
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//調換順序
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
//插入
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string operator+(char ch)
{
string tmp(*this);
push_back(ch);
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做標識的\0
};
my_string::string to_string(int value)
{
my_string::string str;
while (value)
{
int val = value % 10;
str += ('0' + val);
value /= 10;
}
reverse(str.begin(), str.end());
return str;
}
}
2.移動構造
當我們調用to_string的時候:
my_string::string ret = my_string::to_string(1234);
當我們不添加移動構造的時候,可以發現最終進行的是一次深拷貝和一次淺拷貝:
這里發現只調用了一次拷貝構造,這是因為編譯器做了優化,如果不優化的話,str拷貝構造臨時對象,然后臨時對象作為to_string的返回值再拷貝構造給ret。其實是發生了兩次拷貝構造。
但是編譯器做了優化之后,在to_string函數快結束時,返回前直接用str構造ret。
當我們加入拷貝構造之后,會發現只發生了一次移動構造就可以了:
其實在這一過程中編譯器也做了優化,str先拷貝構造形成一個臨時對象,再由臨時對象進行移動構造賦值給ret。
編譯器做了優化之后,將str直接當成左值(相當于move了一下),然后進行移動構造生成ret。
通過觀察打印結果可以發現,顯然移動構造沒有再開辟空間,而是直接將數據進行轉移,節省了空間,由臨時變量進行拷貝構造給ret還會創建一個新的對象,消耗空間。
3.移動賦值
my_string::string ret;
ret = my_string::to_string(1234);//調用移動賦值
當不使用移動賦值的時候,以上代碼是兩段深拷貝實現的:
首先str會調用移動構造,生成臨時對象,然后臨時對象再調用賦值拷貝構造(深拷貝),定義ret。
當引入移動賦值之后,這個過程就變成了str調用移動構造生成臨時對象,臨時對象再通過移動運算符重載生成ret,整個過程中沒有一次深拷貝。
C++11中,所有STL容器中,都會提供一個右值引用的版本。
四、默認移動構造和移動賦值重載函數
與六大成員函數一樣,編譯器在一定的條件下,也會生成自己的默認移動構造函數,只不過生成的條件更加復雜:
1.如果你自己沒有實現移動構造函數,并且沒有實現析構函數,拷貝構造,拷貝賦值構造中的任意一個。那么編譯器會自動生成一個默認構造函數。默認生成的移動構造函數,對內置類型進行直接拷貝,對于自定義類型,如果有對應的移動構造函數就調用其對應的移動構造函數,如果沒有那么調用拷貝構造。
2.如果你沒自己實現移動賦值重載函數,且沒有實現析構函數,拷貝構造,拷貝賦值重載中的任何一個,編譯器會自動生成一個移動賦值重載函數。默認生成的移動賦值重載函數,對內置類型直接進行賦值,對于自定義類型,如果有對應的移動賦值重載函數就調用其對應的移動賦值重載函數,如果沒有則調用拷貝賦值。
3.如果你提供了移動賦值構造或者移動賦值重載函數,那么編譯器就不會自動生成。
五、完美轉發
1.萬能引用
在模板中,&&表示的不是右值引用,而是萬能引用,即既可以接收左值,又可以接收右值。
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
此時傳入的t既可以是左值,也可以是右值。
2.完美轉發
運行以下程序,發現最終識別的都是左值引用。
void Func(int&& x)
{
cout << "rvalue" << endl;
}
void Func(int& x)
{
cout << "lvalue" << endl;
}
template<class T>
void PerfectForward(T&& t)
{
Func(t);
}
int main()
{
PerfectForward(10);//左值
int a;
PerfectForward(a);//左值
PerfectForward(move(a));//左值
}
這是因為右值引用一旦引用了,就變成了左值,如果我們還希望保持該右值引用的特性的話,需要使用forward函數來對其進行封裝:
Func(forward<T>(t));
forward(t)來進行封裝的意義在于,保持t原來的屬性,如果它原來是左值那么封裝之后還是左值,如果它是右值的引用,則將其還原成右值。該函數的作用稱為完美轉發,由于這一性質,STL容器的插入也可以使用右值引用來實現。
即支持:
vector<int> v;v.push_back(111);
在該右值引用版本的插入中,調用的就是forward(val)。
原文鏈接:https://blog.csdn.net/qq_51492202/article/details/126956110
相關推薦
- 2022-09-23 Python文件目錄操作常用模塊的使用詳解_python
- 2022-07-19 Python數據分析?Pandas?Series對象操作_python
- 2022-10-03 Objective-C之Category實現分類示例詳解_IOS
- 2023-03-13 Python實現柵欄密碼的加密解密方法詳解_python
- 2022-06-20 深入淺析C#?11?對?ref?和?struct?的改進_C#教程
- 2022-09-22 哈希思想的經典應用(位圖,哈希切割)
- 2022-04-17 算法時間復雜度和空間復雜度
- 2022-05-01 c++隱式類型轉換存在的問題解析_C 語言
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支