【C++11上手篇】壹、右值引用與移動語義

Devnan_ 2021-08-15 18:14:28 阅读数:946

本文一共[544]字,预计阅读时长:1分钟~
c++11 上手 引用

前言

前段時間換工作,技術棧漸漸從Android偏向到跨端開發,需要重拾C++,所以打算總結成一個系列。

這個系列會從實用的角度總結C++11的一些新特性,盡量做到簡單通俗易懂。

面對人群是有C++基礎想快速了解現代C++新特性的同學,感覺比較多,大學基本都是教傳統C++。

​廢話不多說,本文講一下右值引用的概念和以及它的應用場景移動語義。

右值引用的由來

在C++11之前,所有引用都是左值引用(lvalue reference),也就是對左值的引用。左值一般放在賦值錶達式左邊,是在堆或棧上分配的命名對象,它們有明確的內存地址。

而左值的另一比特朋友右值(rvalue),在賦值錶達式右邊,沒有可識別的內存地址。如果從硬件層面理解,右值只存在於臨時寄存器中。

比如下面這段代碼:

 int a = 1;
int& b = a;
複制代碼

很明顯,這裏 a 是左值,1 是右值,b 是一個左值引用,也就是a的別名。

再比如這段:

 int& a = 1;
複制代碼

g++編譯,會顯示錯誤如下:
non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
意思是非常量左值引用不能指向右值。

大家都不會犯這樣的錯,這裏想說的是,我們還可以使用常量左值引用來指向右值,像這樣:

 const int& a = 1; //常量左值引用
複制代碼

問題來了,常量左值引用為什麼可以指向右值?

因為const常量值不可修改,可以理解為內部產生了一個臨時量,可以取到地址。類似於以下:

 const int tmp = 1;
const int &a = tmp;
複制代碼

可以看到,const Type& 是 C++ 中一個常見的習慣,函數的參數使用常量引用 const Type& 接收,以避免創建不必要的臨時對象:

 void func(const std::string& a);
func("hello");
複制代碼

但是這種方式有個缺點,就是沒法修改這個const常量,有一定局限性。

C++11引入的這比特新朋友,右值引用,一定程度上解决了其中的這個問題。

右值引用,Type&&,用來指向右值,並且可以修改右值。

 void func(const std::string&& a){
a = "world"; //修改右值
}
func("hello");
複制代碼

OK,到這裏,我們簡單總結下

  • 左值可以尋址,右值不可以尋址,這是它們的關鍵區別;
  • 函數傳參使用左右值引用可以避免拷貝,但右值引用更為靈活。

那麼,右值引用的具體應用場景是什麼?

移動語義提昇性能

右值引用有一個非常重要的作用是支持移動語義。而相對於移動語義,拷貝語義可能比較好理解。

比如下面代碼,我們可以定義拷貝構造函數來實現對象的深拷貝,如果沒有定義,編譯器會有默認實現,是淺拷貝。

class Stack {
public:
Stack(int size = 100) : size_(size)
{
cout << "構造函數" << endl;
stack_ = new int[size];
}
Stack(const Stack &src):size_(src.size_)
{
cout << "拷貝構造函數" << endl;
stack_ = new int[src.size_];
//深拷貝
for (int i = 0; i < size_; ++i)
{
stack_[i] = src.stack_[i];
}
}
~Stack()
{
cout << "析構函數" << endl;
delete[] stack_;
stack_ = nullptr;
}
private:
int size_;
int *stack_;
};
int main() {
Stack stack(10);
Stack stack2 = stack;
}
複制代碼

運行輸出是:

構造函數
拷貝構造函數
析構函數
析構函數
複制代碼

除此之外,在某些場景,比如被拷貝者之後不再需要,我們其實可以使用 std::move 觸發移動語義,避免深拷貝,提昇性能。

所以在上面代碼中,我們可以加一個移動構造函數,這種方式在STL和自定義類廣泛應用。

Stack(Stack&& src):size_(src.size_) {
cout << "移動構造函數" << endl;
stack_ = src.stack_;
src.stack_ = nullptr;
}
int main(){
Stack stack(10);
//Stack stack2 = stack; //走拷貝構造
Stack stack2 = std::move(stack); //走移動構造
}
複制代碼

運行的輸出是:

構造函數
移動構造函數
析構函數
析構函數
複制代碼

這裏,std::move 的作用是把左值轉換為右值引用,而移動構造函數的作用是傳入對象的所有權轉讓給當前對象,然後掏空了傳入對象。

std::move真面目

大家可能以為std::move施展了什麼神奇的魔法,其實並沒有,僅僅做了類型轉換而已,真正的移動操作是在移動構造函數或者移動賦值操作符中發生的。可以瞧一瞧代碼

 template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
複制代碼

發現只是一個static_cast轉換,其中,remove_reference的作用去除T中的引用部分,無論T是左值還是右值,只獲取其中的類型。我們來簡化一下,當_TP是string時,這個函數其實就是

string&& move(string&& __t) {
return static_cast<string&&>(__t);
}
複制代碼

所以,不管傳參是左值右值,最後返回的一定是個右值引用。

實際上,std::move 運行期不做任何事情,因為編譯後不會生成可執行代碼,內部只是變量地址的透傳,完全可以被優化掉。有興趣的同學可以看看 通過匯編淺析 C++ 右值引用

最後,聰明的你可能也發現了,std::move 的函數參數&&看起來是一個右值引用類型,可以傳入左值嗎?

公眾號.png

版权声明:本文为[Devnan_]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815181418987z.html