前言:
- 本期,我們將要的介紹有關(guān) C++右值引用 的相關(guān)知識。對于本期知識內(nèi)容,大家是必須要能夠掌握的,在面試中是屬于重點(diǎn)考察對象。
目錄
(一)左值引用和右值引用
1、什么是左值?什么是左值引用?
2、什么是右值?什么是右值引用?
(二)左值引用與右值引用比較
(三)右值引用使用場景和意義
(四)完美轉(zhuǎn)發(fā)?
1、概念
2、模板中的&& 萬能引用
3、std::forward
總結(jié)
(一)左值引用和右值引用
傳統(tǒng)的C++語法中就有引用的語法,而C++11中新增了的右值引用語法特性,所以從現(xiàn)在開始我們之前學(xué)習(xí)的引用就叫做左值引用。無論左值引用還是右值引用,都是給對象取別名。
1、什么是左值?什么是左值引用?
C++98/03 標(biāo)準(zhǔn)中就有引用,使用 "&" 表示。但此種引用方式有一個缺陷,即正常情況下只能操作 C++中的左值,無法對右值添加引用。舉個例子:
?
int main()
{
int num = 10;
int& b = num; //正確
int& c = 10; //錯誤
return 0;
}
輸出展示:
?【解釋說明】
- 如上所示,編譯器允許我們?yōu)?num 左值建立一個引用,但不可以為 10 這個右值建立引用。因此,C++98/03 標(biāo)準(zhǔn)中的引用又稱為左值引用。
?
那么到底什么是左值?什么是左值引用呢?
- 左值是一個表示數(shù)據(jù)的表達(dá)式(如變量名或解引用的指針),我們可以獲取它的地址+可以對它賦值,左值可以出現(xiàn)賦值符號的左邊,右值不能出現(xiàn)在賦值符號左邊;
- 定義時const修飾符后的左值,不能給他賦值,但是可以取它的地址。左值引用就是給左值的引用,給左值取別名。
?
注意:雖然 C++98/03 標(biāo)準(zhǔn)不支持為右值建立非常量左值引用,但允許使用常量左值引用操作右值。也就是說,常量左值引用既可以操作左值,也可以操作右值,例如:
?
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下幾個是對上面左值的左值引用
int*& rp = p;
int& rb = b;
//左值引用給右值取別名
const int& rc = c;
int& pvalue = *p;
return 0;
}
2、什么是右值?什么是右值引用? ?
我們知道,右值往往是沒有名稱的,因此要使用它只能借助引用的方式。這就產(chǎn)生一個問題,實際開發(fā)中我們可能需要對右值進(jìn)行修改(實現(xiàn)移動語義時就需要),顯然左值引用的方式是行不通的。
為此,C++11 標(biāo)準(zhǔn)新引入了另一種引用方式,稱為右值引用,用 "&&" 表示:
- 右值也是一個表示數(shù)據(jù)的表達(dá)式,如:字面常量、表達(dá)式返回值,函數(shù)返回值(這個不能是左值引用返回)等等;
- 右值可以出現(xiàn)在賦值符號的右邊,但是不能出現(xiàn)出現(xiàn)在賦值符號的左邊,右值不能取地址;
- 右值引用就是對右值的引用,給右值取別名。
int main()
{
double x = 1.1, y = 2.2;
// 以下幾個都是常見的右值
10;
x + y;
fmin(x, y);
// 以下幾個都是對右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
return 0;
}
輸出展示:
?但是如果是下面這幾個表達(dá)式,就會發(fā)生報錯現(xiàn)象:
10 = 1;
x + y = 1;
fmin(x, y) = 1;
輸出顯示:
需要注意的是右值是不能取地址的,但是給右值取別名后,會導(dǎo)致右值被存儲到特定位置,且可
以取到該位置的地址。例如下面代碼所示:
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
double&& rr2 = x + y;
cout << rr1 << " " << rr2 << " " << endl;
rr1 = 20;
rr2 = 5.5;
cout << rr1 << " " << rr2 << " " << endl;
return 0;
}
輸出展示:
?當(dāng)我們不想被修改時,我們可以加上 【const】關(guān)鍵字:
【解釋說明】
- 不能取字面量10的地址,但是rr1引用后,可以對rr1取地址,也可以修改rr1;
- 如果不想rr1被修改,可以用const int&& rr1 去引用;
- 是不是感覺很神奇這個了解一下實際中右值引用的使用場景并不在于此,這個特性也不重要
?
(二)左值引用與右值引用比較
左值引用總結(jié):
- 1. 左值引用只能引用左值,不能引用右值。
- 2. 但是const左值引用既可引用左值,也可引用右值
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra為a的別名
return 0;
}
輸出展示:
?又例如以下示例:
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra2 = 10; // 編譯失敗,因為10是右值
return 0;
}
輸出展示:
?左值引用只能引用左值,不能引用右值。但是當(dāng)我們加上?const 時,此時左值引用可以給右值取別名:
int main()
{
int a = 10;
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
輸出展示:
【解釋說明】
值得一提的是,雖然C++ 語法上是支持定義常量右值引用的,但這種定義出來的右值引用并無實際用處:
const int& ra3 = 10;
- 一方面,右值引用主要用于移動語義和完美轉(zhuǎn)發(fā),其中前者需要有修改右值的權(quán)限;
- 其次,常量右值引用的作用就是引用一個不可修改的右值,這項工作完全可以交給常量左值引用完成。
?
右值引用總結(jié):
- 1. 右值引用只能右值,不能引用左值。
- 2. 但是右值引用可以move以后的左值。
?
代碼展示:
?引用左值會發(fā)生報錯行為:
通過 move?可以支持將左值轉(zhuǎn)換為右值引用?
?在C++中,move
是一個函數(shù)模板,可以將給定的對象轉(zhuǎn)換為對應(yīng)的右值引用。它并不執(zhí)行實際的內(nèi)存移動操作,而是將對象標(biāo)記為可以進(jìn)行移動操作的右值。這樣,用戶可以利用該標(biāo)記來實現(xiàn)更高效的移動語義。
(三)右值引用使用場景和意義
前面我們可以看到左值引用既可以引用左值和又可以引用右值,那為什么C++11還要提出右值引用呢?是不是化蛇添足呢?下面我們來看看左值引用的短板,右值引用是如何補(bǔ)齊這個短板的!
現(xiàn)有以下代碼:?
?【解釋說明】
首先,對于上述代碼中的 res1 和 res2 ,它們分別為左值和右值;
緊接著大家想想,我們對左值和對右值拷貝有沒有什么區(qū)別呢?
- 如果是內(nèi)置類型,他們其實區(qū)別不是很大,但是對于自定類型他們的區(qū)別可就很大了,
- 因為自定義類型的右值,一般很多地方又把它叫做將亡值。通常都是一些表達(dá)式的返回值、一個函數(shù)調(diào)用等;
- 而對于右值又分為 純右值(一般來說是內(nèi)置類型)和將亡值(一般來說是自定義類型)
對于上述的 res1,它作為一個左值,我們不能對其進(jìn)行操作,只能去做深拷貝。因為雖然看起來這里是一個賦值,其實應(yīng)該是拷貝構(gòu)造;
而對于 res2 來說,它本身是右值,假如是自定義類型作為一個將亡值,我們就沒有必要去對其進(jìn)行拷貝操作。此時就引出了關(guān)于右值引用實現(xiàn)引動構(gòu)造的概念。
例如現(xiàn)在有這樣一個我們手寫模擬的 string :
namespace zp
{
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);
}
// 拷貝構(gòu)造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷貝" << endl;
string tmp(s._str);
swap(tmp);
}
// 賦值重載
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);
tmp += ch;
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做標(biāo)識的\0
};
}
當(dāng)我們再這樣的場景在來觀察上述的 res1 和res2:
?我們可以發(fā)現(xiàn)此處的右值發(fā)生的是相應(yīng)的深拷貝,這樣顯然是會造成不必要的浪費(fèi)的。為了解決上述這樣的問題,我們就可以引入 “移動構(gòu)造” 這樣的概念:
// 移動構(gòu)造
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移動拷貝" << endl;
swap(s);
}
緊接著,再次運(yùn)行上述代碼,我們可以發(fā)現(xiàn)編譯器會去自動識別:
此時,當(dāng)我們就是想把 s1 轉(zhuǎn)為右值可以怎么做呢?其實很簡單(這里就體現(xiàn)了move):
?輸出展示:
我們通過調(diào)試也可以發(fā)現(xiàn)此時確實達(dá)到了預(yù)期的效果:
?【小結(jié)】
通過上述我們可以發(fā)現(xiàn)左值引用的好處就是直接減少拷貝
左值引用的使用場景可以分為以下兩個部分:
- 做參數(shù)和做返回值都可以提高效率
左值引用的短板:
- 但是當(dāng)函數(shù)返回對象是一個局部變量,出了函數(shù)作用域就不存在了,就不能使用左值引用返回,只能傳值返回。
例如現(xiàn)在有以下這樣的代碼:?
zp::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
zp::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
【說明】
- zp::string to_string(int value)函數(shù)中可以看到,這里只能使用傳值返回,傳值返回會導(dǎo)致至少1次拷貝構(gòu)造(如果是一些舊一點(diǎn)的編譯器可能是兩次拷貝構(gòu)造)。
?緊接著,我們?nèi)ゴ蛴】唇Y(jié)果是什么:
?此時,傳值返回帶來的代價得到了極大的解決:
?右值引用和移動語義解決上述問題:
- 在zp::string中增加移動構(gòu)造,移動構(gòu)造本質(zhì)是將參數(shù)右值的資源竊取過來,占位已有,那么就不用做深拷貝了,所以它叫做移動構(gòu)造,就是竊取別人的資源來構(gòu)造自己;
再運(yùn)行上面zp::to_string的兩個調(diào)用,我們會發(fā)現(xiàn),這里沒有調(diào)用深拷貝的拷貝構(gòu)造,而是調(diào)用了移動構(gòu)造,移動構(gòu)造中沒有新開空間,拷貝數(shù)據(jù),所以效率提高了。
c++11 不僅僅有移動構(gòu)造,還有移動賦值:
在zp::string類中增加移動賦值函數(shù),再去調(diào)用zp::to_string(1234),不過這次是zp::to_string(1234)返回的右值對象賦值給ret1對象,這時調(diào)用的是移動構(gòu)造。
輸出展示:
?【解釋說明】
- 這里運(yùn)行后,我們看到調(diào)用了一次移動構(gòu)造和一次移動賦值。因為如果是用一個已經(jīng)存在的對象接收,編譯器就沒辦法優(yōu)化了。zp::to_string函數(shù)中會先用str生成構(gòu)造生成一個臨時對象,但是我們可以看到,編譯器很聰明的在這里把str識別成了右值,調(diào)用了移動構(gòu)造。然后在把這個臨時對象做為 zp::to_string 函數(shù)調(diào)用的返回值賦值給ret1,這里調(diào)用的移動賦值。
(四)完美轉(zhuǎn)發(fā)?
1、概念
- 完美轉(zhuǎn)發(fā)(perfect forwarding)是C++11引入的一項特性,旨在實現(xiàn)在函數(shù)模板中對參數(shù)類型進(jìn)行精確傳遞的能力;
- 它主要用于保留傳遞到函數(shù)模板的實參的值類別,并將其轉(zhuǎn)發(fā)到內(nèi)部調(diào)用的函數(shù),從而實現(xiàn)類型和值類別的完全保持;
舉個例子:
template<typename T>
void PerfectForward(T t)
{
Fun(t);
}
【解釋說明】
- 如上所示,PerfectForward() 函數(shù)模板中調(diào)用了 Func() 函數(shù);
- 在此基礎(chǔ)上,完美轉(zhuǎn)發(fā)指的是:如果 PerfectForward() 函數(shù)接收到的參數(shù) t 為左值,那么該函數(shù)傳遞給 Func() 的參數(shù) t 也是左值;
- 反之如果 function() 函數(shù)接收到的參數(shù) t 為右值,那么傳遞給 Func() 函數(shù)的參數(shù) t 也必須為右值。
使用任何一種引用形式,可以實現(xiàn)轉(zhuǎn)發(fā),但無法保證完美。因此如果使用 C++ 98/03 標(biāo)準(zhǔn)下的 C++ 語言,我們可以采用函數(shù)模板重載的方式實現(xiàn)完美轉(zhuǎn)發(fā),例如:
template<typename T>
void Func(T& arg)
{
cout << "左值引用:" << arg << endl;
}
template<typename T>
void Func(T&& arg)
{
cout << "右值引用:" << arg << endl;
}
template<typename T>
void PerfectForward(T&& arg)
{
Func(arg); // 利用重載的process函數(shù)進(jìn)行處理
}
int main()
{
int value = 42;
PerfectForward(value); // 傳遞左值
PerfectForward(123); // 傳遞右值
return 0;
}
輸出展示:
?【解釋說明】
- 在上述示例中,我們定義了兩個重載的函數(shù)模板?
Func
,一個接收左值引用參數(shù)T& arg
,另一個接收轉(zhuǎn)發(fā)引用參數(shù)T&& arg;
- 然后,我們再定義一個模板函數(shù)
PerfectForward
,其參數(shù)也是轉(zhuǎn)發(fā)引用T&& arg
。在PerfectForward
函數(shù)內(nèi)部,我們通過調(diào)用Func
函數(shù)來處理傳遞的參數(shù); - 通過函數(shù)重載的機(jī)制,傳遞的左值參數(shù)將匹配到接收左值引用的
Func
函數(shù),傳遞的右值參數(shù)則匹配到接收轉(zhuǎn)發(fā)引用的Func
函數(shù),從而正確地進(jìn)行區(qū)分和處理; - 通過函數(shù)模板的重載,我們可以根據(jù)參數(shù)類型將左值和右值區(qū)分開來,并分別處理,實現(xiàn)了針對不同值類別的精確匹配和操作。
2、模板中的&& 萬能引用
顯然,上述使用重載的模板函數(shù)實現(xiàn)完美轉(zhuǎn)發(fā)也是有弊端的,此實現(xiàn)方式僅適用于模板函數(shù)僅有少量參數(shù)的情況,否則就需要編寫大量的重載函數(shù)模板,造成代碼的冗余。為了方便用戶更快速地實現(xiàn)完美轉(zhuǎn)發(fā),C++ 11 標(biāo)準(zhǔn)中允許在函數(shù)模板中使用右值引用來實現(xiàn)完美轉(zhuǎn)發(fā)。
還是以 PerfectForward() 函數(shù)為例,在 C++11 標(biāo)準(zhǔn)中實現(xiàn)完美轉(zhuǎn)發(fā),只需要編寫如下一個模板函數(shù)即可:
//模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
以如下代碼為例:
void Fun(int& x)
{
cout << "左值引用" << endl;
}
void Fun(const int& x)
{
cout << "const 左值引用" << endl;
}
void Fun(int&& x)
{
cout << "右值引用" << endl;
}
void Fun(const int&& x)
{
cout << "const 右值引用" << endl;
}
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
輸出展示:
?【解釋說明】
- 模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。
- 模板的萬能引用只是提供了能夠接收同時接收左值引用和右值引用的能力,
- 但是引用類型的唯一作用就是限制了接收的類型,后續(xù)使用中都退化成了左值,
- 我們希望能夠在傳遞過程中保持它的左值或者右值的屬性, 就需要用我們下面學(xué)習(xí)的完美轉(zhuǎn)發(fā)
?
3、std::forward
C++11 標(biāo)準(zhǔn)的開發(fā)者已經(jīng)幫我們想好的解決方案,該新標(biāo)準(zhǔn)還引入了一個模板函數(shù) forword<T>(),我們只需要調(diào)用該函數(shù),就可以很方便地解決此問題。
- 完美轉(zhuǎn)發(fā)通常與轉(zhuǎn)發(fā)引用(forwarding reference)和 std::forward 函數(shù)一起使用;
- 轉(zhuǎn)發(fā)引用是一種特殊的引用類型,使用
&&
語法進(jìn)行聲明,用于在函數(shù)模板中捕獲傳遞的實參; - std::forward 是一個模板函數(shù),用于在函數(shù)模板內(nèi)部將轉(zhuǎn)發(fā)引用作為右值或左值引用進(jìn)行轉(zhuǎn)發(fā)。
如下演示了該函數(shù)模板的用法:
void Fun(int& x)
{
cout << "左值引用" << endl;
}
void Fun(const int& x)
{
cout << "const 左值引用" << endl;
}
void Fun(int&& x)
{
cout << "右值引用" << endl;
}
void Fun(const int&& x)
{
cout << "const 右值引用" << endl;
}
template<typename T>
void PerfectForward(T&& t)
{
// forward<T>(t)在傳參的過程中保持了t的原生類型屬性。
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(move(b)); // const 右值
return 0;
}
程序執(zhí)行結(jié)果為:
通過完美轉(zhuǎn)發(fā),我們可以在函數(shù)模板中正確處理傳遞實參的值類別,并將其轉(zhuǎn)發(fā)到內(nèi)部函數(shù),以達(dá)到類型和值類別的完全保持,提高代碼的靈活性和效率。
總結(jié)
學(xué)到這里,一些讀者可能無法記清楚左值引用和右值引用各自可以引用左值還是右值,這里給大家一張表格,方便大家記憶:
?
- ?表中,Y 表示支持,N 表示不支持。
以上便是關(guān)于左值引用和右值引用的全部知識講解!感謝大家的觀看與支持?。。?mark hidden color="red">文章來源:http://www.zghlxwxcb.cn/news/detail-670938.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-670938.html
到了這里,關(guān)于【C++】—— C++11新特性之 “右值引用和移動語義”的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!