C++ 右值引用 | 左值、右值、move、移動語義、引用限定符

文章目錄

  • C++11為什么引入右值?
  • 區分左值引用、右值引用
  • move
  • 移動語義
    • 移動構造函數
    • 移動賦值運算符
    • 合成的移動操作
    • 小結
  • 引用限定符
    • 規定this是左值or右值
    • 引用限定符與重載


C++11為什么引入右值?

C++11引入了一個擴展內存的方法——移動而非拷貝,移動較之拷貝有兩個優點:

  1. 效率更高: 在此之前,當數據結構申請的內存用盡時,一般是申請一塊更大的內存,然后將舊內存中存儲的元素拷貝到新內存中。但很多情況下,為了方便拷貝操作而建立的臨時對象在拷貝完成后就被銷毀了,因此不如直接將舊內存中的元素移動到新內存中,即省空間(臨時對象也是要占內存的),還省時間(不用建立臨時對象了)。
  2. IOunique_ptr 這樣的類都包含不可被共享的資源(如指針或IO緩沖),因此,這些類不支持拷貝,僅支持移動。

PS:STLshared_ptr 既支持移動也支持拷貝。

而為了支持移動操作,就誕生了一種新的引用類型——右值引用(rvalue reference)

為了與左值引用進行劃分,使用 & 時則代表是左值引用,而使用 && 則代表右值引用。

右值引用有一個重要的特性——只能綁定到一個將要銷毀的對象。


區分左值引用、右值引用

左值

生成左值: 返回引用的函數、賦值、取下標、解引用、前置遞增/遞減運算符。

我們可以將一個 左值引用 綁定到這類表達式的結果上。

右值

生成右值: 返回非引用類型的函數、算術、關系、位、后置遞增/遞減運算符。

我們可以將一個 const的左值引用 或者一個 右值引用 綁定到這類表達式上。

舉一些例子:

int i = 42;
int &r = i; // 正確:左值引用綁定變量
int &&rr = i; // 錯誤:不能將右值引用綁定到左值上
int &r2 = i * 42; // 錯誤:i*42是右值,不能將左值引用綁定到右值上
const int &r3 = i * 42; // 正確:可以將const左值引用綁定到右值上
int &&rr2 = i * 42; // 正確:右值引用可以綁定到算術結果上

詳細來講:

  1. 普通類型的變量,因為有名字,可以取地址,都認為是左值。
  2. const修飾的常量,不可修改,只讀類型的,理論應該按照右值對待,但因為其可以取地址,C++11認為其是左值。(const類型常量初始化時,編譯器不給其開辟空間,當對該常量取地址時,編譯器才為其開辟空間。)
  3. 如果表達式的運行結果是一個臨時變量或者對象,認為是右值。
  4. 如果表達式運行結果或單個變量是一個引用,認為是左值。

總的來講,即為:左值持久、右值短暫,左值有持久的狀態,而右值要么是字面常量、要么是在表達式求值過程中創建的臨時對象。

由于右值引用只能綁定到臨時對象,我們得知:

  • 所引用的對象將要被銷毀
  • 該對象沒有其他用戶

這兩個特性意味著:可以自由地接管右值引用綁定的資源,而不必擔心發生錯誤。

有趣的是,右值引用本身是一個變量,因此它是一個左值,也就是說,不能將右值引用綁定到一個右值引用類型的變量上:

int &&rr1 = 42; // 正確:字面常量是右值
int &&rr2 = rr1; // 錯誤:表達式rr1是左值

move

按照語法來說,右值引用應該只能引用右值,但我們可以通過move函數顯式地將一個左值轉換為對應的右值引用類型

#include<utility> //move的頭文件
int &&rr1 = 42; // 右值引用
int &&rr2 = std::move(rr1); // rr1是左值,綁定到右值引rr2上

調用move就意味著:可以銷毀一個移后源對象(rr1),也可以賦予它新值,但不能使用一個移后源對象(rr1)的值。

與大多數標準庫名字的使用不同,對 move 我們不提供 using聲明。換言之,我們直接調用 std::move 而不是 move。因為 STL 還有另一個 move,那個的作用就是將一個范圍中的元素搬移到另一個位置。


移動語義

移動構造函數

  • 類似拷貝構造函數,移動構造函數的第一個參數是該類類型的引用,任何額外的參數都必須有默認實參。
  • 不同于拷貝構造函數的是,這個引用參數在移動構造函數中是一個右值引用

除了完成資源移動,移動構造函數還必須確保移后源對象是可銷毀的。 一旦資源完成移動,源對象必須不再指向被移動的資源——這些資源的所有權已經歸屬新創建的對象。

作為一個例子,我們為 IntVec類 定義移動構造函數,實現從一個 IntVec 到另一個 IntVec 的元素移動而非拷貝:
在這里插入圖片描述

class IntVec // IntVec是對標準庫vector類的模仿,僅存儲int元素
{int *begin; // 指向已分配的內存中的首元素int *end; // 指向最后一個實際元素之后的位置int *cap; // 指向分配的內存末尾之后的位置
public:IntVec(IntVec &&a) noexcept // noexcept通知標準庫不拋出任何異常: begin(a.begin), end(a.end), cap(a.cap) // 成員初始化器接管a中的資源{a.begin = a.end = a.cap =nullptr;// 令a進入可銷毀狀態,確保對其運行析構函數是安全的。}
}; 

工作流程:

  1. 移動構造函數不分配任何新內存,而是接管給定的 IntVec 中的內存。
  2. 接管之后,將給定對象中的指針都置為 nullptr
  3. 函數體執行完畢自動調用析構函數銷毀移后源對象。

在第三點中,如果我們沒有進行第二點,此時移后源對象仍指向被接管的內存,此時調用析構函數會釋放掉剛剛移動的內存,因此三步一步都不能少。

關于 noexcept

  • 由于移動操作不分配任何資源,因此不會拋出異常,我們可以通知標準庫,這樣他就不會因為需要等待處理異常而浪費資源。
  • noexcept 是通知標準庫的方式之一,出現在參數列表和初始化列表開始的冒號之間。

為什么移動操作不會拋出異常?

首先明確一定,是允許移動操作拋出異常的,但是這么做反而有壞處。

vectorpush_back 操作來講,當執行尾插操作但是內存空間已經滿了,需要重新分配內存空間,此時:

  • 如果重新分配過程使用了移動構造函數,且在移動了部分元素后拋出了一個異常,就會產生問題——舊空間中的移動源元素已經被改變了,而新空間中移動源元素尚未構造好。在此情況下,vector 將丟失自身的部分元素。
  • 如果 vector 使用了拷貝構造函數,當在新內存中構造元素時,舊內存中的元素保持不變。如果此時發生了異常,vector 可以釋放新分配的(但還未成功構造的)內存并返回。vector 原有的元素仍然存在。

因此,對于移動操作來講,不拋出異常反而能保證數據的完整性。


移動賦值運算符

和移動構造函數一樣——不拋出異常,但仍要注意處理所有賦值運算符逃不過的劫難——自賦值問題。

IntVec& IntVec::operator=(IntVec &&rhs) noexcept{if(this != &rhs){ // 處理非自賦值free(); // 釋放已有資源begin = rhs.begin; // 從 rhs 接管資源end = rhs.end;cap = rhs.cap;// 將 rhs 置于可析構狀態rhs.begin = rhs.end = rhs.cap = nullptr;}return *this;
}

這種寫法其實是最常用也最簡單的自賦值處理方法,像之前講的 用臨時量存右側運算對象swap實現自賦值 。巧妙則巧妙,但是寫起來一定要很小心,遠不如直接 if-else 來的方便。


合成的移動操作

如果我們不聲明自己的拷貝構造函數或拷貝賦值運算符,編譯器總會為我們合成這些操作。但與拷貝操作不同,如果一個類定義了自己的拷貝構造函數、拷貝賦值運算符或者析構函數,編譯器就不會為它合成移動構造函數和移動賦值運算符了。如果一個類沒有移動操作,通過正常的函數匹配,類會使用對應的拷貝操作來代替移動操作

只有當一個類沒有定義任何自己版本的拷貝控制成員,且類的每個 非static數據成員 都可以移動時,編譯器才會為它合成移動操作。

編譯器可以移動內置類型的成員。如果一個成員是類類型,且該類有對應的移動操作,編譯器也能移動這個成員:
在這里插入圖片描述


小結

在移動操作之后,移后源對象必須保持有效的、可析構的狀態。

移后源對象仍然保持有效

我們可以對它執行諸如 emptysize 這些操作。但是,我們不知道將會得到什么結果。我們可能猜測一個移后源對象是空的,但結果并不一定如我們猜測的那樣。換言之,我們可以重新用它,但是我們不知道用之前它是什么狀態。


同時存在拷貝控制操作和移動操作時的匹配規則

  • 拷貝構造函數接受一個 const 類型名& 的左值引用類型;
  • 移動構造函數接受一個 類型名&& 右值引用類型。

因此,左值只能匹配拷貝構造函數,但是右值卻都可以匹配,只是調用拷貝操作時需要進行一次到 const 的轉換,而移動操作精確匹配,因此,右值會使用移動操作。


swap實現一個賦值運算符既是拷貝操作也是移動操作

  • 移動賦值運算符接受一個 類型名&& 右值引用類型;
  • 拷貝賦值運算符接受一個 const 類型名& 的左值引用類型。

因此,我們可以在已經定義好移動構造函數的基礎上,借助 swap函數 實現一個形參為 類型名 的賦值運算符:

class IntVec
{
public:IntVec(IntVec &&a) noexcept: begin(a.begin), end(a.end), cap(a.cap){a.begin = a.end = a.cap =nullptr;}IntVec& operator=(IntVec a){swap(*this, a);return *this;}
}; 

具體思想我們在上一篇博客的swap實現自賦值中講過一次,這里簡單再提一下。

  • 首先 swap函數 是類自己重載的,而不是標準庫中的 swap函數,目的是避免浪費內存。
  • 一定要確保類已經定義好了移動構造函數,否則,像我們之前說過的那樣,在有拷貝操作的情況下,類不會合成移動操作,則該賦值運算符只實現了拷貝操作而沒有實現移動操作。
  • 該賦值運算符最終實現的操作由傳入的實參類型決定:左值拷貝、右值移動

舉個例子:

// 假定 v1、v2 都是 IntVec 對象
v1 = v2; // v2是左值,拷貝構造函數來拷貝v2
v1 = std::move(v2); // 移動構造函數移動v2

匹配詳情就不多說了,在上文的匹配規則中講的很詳細了,這里主要想體現的是:不管使用的是拷貝構造函數還是移動構造函數,賦值運算符都可以將他們的結果作為實參來執行。換言之,配合上 swap函數賦值運算符 同時支持 移動操作拷貝操作


為什么拷貝操作的形參通常是 const X& 而不是 X&?移動操作的形參通常是 X&& 而不是 const X&&?

  • 當我們希望使用 將亡值 時,通常傳遞一個右值引用。為了在移動后釋放源對象持有的資源,實參不能是 const 的。
  • 從一個對象進行拷貝的操作不應該改變該對象。因此,通常不需要定義一個接受一個 (普通的)X& 參數的版本。

引用限定符

規定this是左值or右值

有時會看到這樣的代碼:

string s1 = "hello", s2 = "world";
s1 + s2 = "!";

此處我們對兩個 string 的連接結果——一個右值,進行了賦值。

在舊標準中,我們沒有辦法阻止這種使用方式。為了維持向后兼容性,新標準庫類仍然允許向右值賦值。但是,我們有時需要阻止這種用法。在此情況下,我們希望強制左側運算對象(即,this指向的對象)是一個左值。

我們指出 this 的左值/右值屬性的方式與定義 const 成員函數相同,即,在參數列表后放置一個引用限定符(reference qualifier)

class IntVec
{
public:IntVec& operator=(IntVec a) & // 只能向可修改的左值賦值{swap(*this, a);return *this;}
}; 

引用限定符可以是 &&&,分別指出 this 可以指向一個左值或右值。類似 const 限定符,引用限定符只能用于(非static)成員函數,且必須同時出現在函數的聲明和定義中。

一個函數可以同時用 const引用限定。在此情況下,引用限定符必須跟隨在const限定符之后:

class IntVec
{
public:IntVec& operator=(IntVec a) const &}; 

引用限定符與重載

就像一個成員函數可以根據是否有 const 來區分其重載版本一樣,引用限定符也可以區分重載版本。

舉個例子:
在這里插入圖片描述

編譯器會根據調用 sorted 的對象的左值/右值屬性來確定使用哪個 sorted 版本:
在這里插入圖片描述

  • 當我們定義 const成員函數 時,可以定義兩個版本,唯一的差別是一個版本有 const限定 而另一個沒有。
  • 引用限定的函數則不一樣。如果我們定義兩個或兩個以上具有相同名字和相同參數列表的成員函數,就必須對所有函數都加上引用限定符,或者所有都不加。

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/443739.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/443739.shtml
英文地址,請注明出處:http://en.pswp.cn/news/443739.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

且談關于最近軟件測試的面試

前段時間有新的產品需要招人&#xff0c;安排和參加了好幾次面試&#xff0c;下面就談談具體的面試問題&#xff0c;在面試他人的同時也面試自己。 面試問題是參與面試同事各自設計的&#xff0c;我也不清楚其他同事的題目&#xff0c;就談談自己設計的其中2道題。 過去面試總是…

C++ 多態 | 虛函數、抽象類、虛函數表

文章目錄多態虛函數重寫重定義&#xff08;參數不同&#xff09;協變&#xff08;返回值不同&#xff09;析構函數重寫&#xff08;函數名不同&#xff09;final和override重載、重寫、重定義抽象類多態的原理虛函數常見問題解析虛函數表多態 一種事物&#xff0c;多種形態。換…

C++ 運算符重載(一) | 輸入/輸出,相等/不等,復合賦值,下標,自增/自減,成員訪問運算符

文章目錄輸出運算符<<輸入運算符>>相等/不等運算符復合賦值運算符下標運算符自增/自減運算符成員訪問運算符輸出運算符<< 通常情況下&#xff0c;輸出運算符的第一個形參是一個 非常量ostream對象的引用 。之所以 ostream 是非常量是因為向流寫入內容會改變…

C++ 重載函數調用運算符 | 再探lambda,函數對象,可調用對象

文章目錄重載函數調用運算符lambdalambda等價于函數對象lambda等價于類標準庫函數對象可調用對象與function可調用對象function函數重載與function重載函數調用運算符 函數調用運算符必須是成員函數。 一個類可以定義多個不同版本的調用運算符&#xff0c;互相之間應該在參數數…

C++ 運算符重載(二) | 類型轉換運算符,二義性問題

文章目錄類型轉換運算符概念避免過度使用類型轉換函數解決上述問題的方法轉換為 bool顯式的類型轉換運算符類型轉換二義性重載函數與類型轉換結合導致的二義性重載運算符與類型轉換結合導致的二義性類型轉換運算符 概念 類型轉換運算符&#xff08;conversion operator&#…

Tomcat中JVM內存溢出及合理配置

Tomcat本身不能直接在計算機上運行&#xff0c;需要依賴于硬件基礎之上的操作系統和一個Java虛擬機。Tomcat的內存溢出本質就是JVM內存溢出&#xff0c;所以在本文開始時&#xff0c;應該先對Java JVM有關內存方面的知識進行詳細介紹。 一、Java JVM內存介紹 JVM管理兩種類型的…

俄羅斯農民乘法 | 快速乘

文章目錄概念概念 俄羅斯農民乘法經常被用于兩數相乘取模的場景&#xff0c;如果兩數相乘已經超過數據范圍&#xff0c;但取模后不會超過&#xff0c;我們就可以利用這個方法來拆位取模計算貢獻&#xff0c;保證每次運算都在數據范圍內。 A 和 B 兩數相乘的時候我們如何利用加…

Linux網絡編程 | socket選項設定 及 網絡信息API

文章目錄讀取和設置 socket 選項SO_REUSEADDRSO_RCVBUF 和 SO_SNDBUFSO_RCVLOWAT 和 SO_SNDLOWATSO_LINGER 選項網絡信息APIgethostbyname 和 gethostbyaddrgetservbyname 和 getservbyportgetaddrinfogetnameinfo讀取和設置 socket 選項 正如 fcntl 系統調用是控制文件描述符…

Linux | 高級I/O函數

文章目錄創建文件描述符的函數pipe函數dup函數、dup2函數讀取或寫入數據readv函數、writev函數零拷貝sendfile函數splice函數tee函數進程間通信——共享內存mmap函數 和 munmap函數控制文件描述符fcntl函數創建文件描述符的函數 pipe函數 不再贅述&#xff0c;詳情見我的另一…

分布式理論:CAP、BASE | 分布式存儲與一致性哈希

文章目錄分布式理論CAP定理BASE理論分布式存儲與一致性哈希簡單哈希一致性哈希虛擬節點分布式理論 CAP定理 一致性&#xff08;Consistency&#xff09;&#xff1a; 在分布式系統中的所有數據副本&#xff0c;在同一時刻是否一致&#xff08;所有節點訪問同一份最新的數據副…

Tomcat服務器性能優化

一、概述 本文檔主要介紹了Tomcat的性能調優的原理和方法。可作為公司技術人員為客戶Tomcat系統調優的技術指南&#xff0c;也可以提供給客戶的技術人員作為他們性能調優的指導手冊。 二、調優分類 由于Tomcat的運行依賴于JVM&#xff0c;從虛擬機的角度我們把Tomcat的調整分為…

分布式系統概念 | 分布式事務:2PC、3PC、本地消息表

文章目錄分布式事務2PC&#xff08;二階段提交協議&#xff09;執行流程優缺點3PC&#xff08;三階段提交協議&#xff09;執行流程優缺點本地消息表&#xff08;異步確保&#xff09;分布式事務 分布式事務就是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分…

數據結構算法 | 單調棧

文章目錄算法概述題目下一個更大的元素 I思路代碼下一個更大元素 II思路代碼132 模式思路代碼接雨水思路算法概述 當題目出現 「找到最近一個比其大的元素」 的字眼時&#xff0c;自然會想到 「單調棧」 。——三葉姐 單調棧以嚴格遞增or遞減的規則將無序的數列進行選擇性排序…

最長下降子序列

文章目錄題目解法DP暴搜思路代碼實現貪心二分思路代碼實現題目 給出一組數據 nums&#xff0c;求出其最長下降子序列&#xff08;子序列允許不連續&#xff09;的長度。&#xff08;類似于lc的最長遞增子序列&#xff09; 示例&#xff1a; 輸入&#xff1a; 6 // 數組元素個…

Linux 服務器程序規范、服務器日志、用戶、進程間的關系

文章目錄服務器程序規范日志rsyslogd 守護進程syslog函數openlog函數setlogmask函數closelog函數用戶進程間的關系進程組會話系統資源限制改變工作目錄和根目錄服務器程序后臺化服務器程序規范 Linux 服務器程序一般以后臺進程&#xff08;守護進程[daemon]&#xff09;形式運…

IO模型 :阻塞IO、非阻塞IO、信號驅動IO、異步IO、多路復用IO

文章目錄IO模型阻塞IO非阻塞IO信號驅動IO多路復用IO異步IOIO模型 根據各自的特性不同&#xff0c;IO模型被分為阻塞IO、非阻塞IO、信號驅動IO、異步IO、多路復用IO五類。 最主要的兩個區別就是阻塞與非阻塞&#xff0c;同步與異步。 阻塞與非阻塞 阻塞與非阻塞最主要的區別就…

Tomcat服務器集群與負載均衡實現

一、前言 在單一的服務器上執行WEB應用程序有一些重大的問題&#xff0c;當網站成功建成并開始接受大量請求時&#xff0c;單一服務器終究無法滿足需要處理的負荷量&#xff0c;所以就有點顯得有點力不從心了。另外一個常見的問題是會產生單點故障&#xff0c;如果該服務器壞掉…

Linux服務器 | 事件處理模式:Reactor模式、Proactor模式

文章目錄Reactor模式Proactor模式同步I/O模型模擬Proactor模式兩者的優缺點ReactorProactor同步I/O模型通常用于實現 Reactor 模式&#xff0c;異步I/O模型通常用于實現 Proactor 模式。&#xff08;不是絕對的&#xff0c;同步I/O也可模擬出 Proactor 模式&#xff09; React…

Linux服務器 | 服務器模型與三個模塊、兩種并發模式:半同步/半異步、領導者/追隨者

文章目錄兩種服務器模型及三個模塊C/S模型P2P模型I/O處理單元、邏輯單元、存儲單元并發同步與異步半同步/半異步模式變體&#xff1a;半同步/半反應堆模式改進&#xff1a;高效的半同步/半異步模式領導者/追隨者模式組件 &#xff1a;句柄集、線程集、事件處理器工作流程兩種服…

香農信息熵之可憐的小豬

文章目錄題目解析香農熵公式樣例具體分析代碼題目 有 n 桶液體&#xff0c;其其中 正好 有一桶含有毒藥&#xff0c;其裝的都是水。它們從外觀看起來都一樣。為了弄清楚哪只水桶含有毒藥&#xff0c;你可以喂一些豬喝&#xff0c;通過觀察豬是否會死進行判斷&#xff0c;實驗對…