C++學習:六個月從基礎到就業——C++11/14:右值引用與移動語義

C++學習:六個月從基礎到就業——C++11/14:右值引用與移動語義

本文是我C++學習之旅系列的第三十九篇技術文章,也是第三階段"現代C++特性"的第一篇,主要介紹C++11/14中引入的右值引用和移動語義。查看完整系列目錄了解更多內容。

引言

C++11引入的右值引用和移動語義是現代C++最重要的特性之一,它解決了傳統C++中昂貴的深拷貝問題,顯著提高了程序性能,尤其是在處理大型對象和臨時對象時。本文將深入探討右值引用和移動語義的概念、實現方式以及實際應用,幫助你理解和掌握這一強大特性。

左值與右值的基本概念

在深入理解右值引用之前,我們需要先清楚左值(lvalue)和右值(rvalue)的概念。

傳統的左值與右值

最初的定義非常直觀:

  • 左值:可以出現在賦值表達式左側的表達式
  • 右值:只能出現在賦值表達式右側的表達式

但這個定義在現代C++中已經不夠精確了。更現代的定義是:

  • 左值:有身份(可以取地址)且可以被移動的表達式
  • 右值:有身份或可以被移動,但不同時滿足這兩個條件的表達式

左值和右值示例

int x = 10;      // x是左值,10是右值
int y = x;       // x是左值,用于初始化另一個左值y
int& ref = x;    // 左值引用必須綁定到左值上
int&& rref = 20; // 右值引用綁定到右值20上// 函數返回的臨時值是右值
int getVal() { return 42; }
// int& r = getVal(); // 錯誤:不能將左值引用綁定到右值
int&& rr = getVal(); // 正確:右值引用可以綁定到右值

左值引用與右值引用

  • 左值引用:使用單&符號,只能綁定到左值
  • 右值引用:使用雙&&符號,只能綁定到右值
  • 常量左值引用:是個特例,可以綁定到左值或右值
int x = 10;
int& ref1 = x;            // 正確:左值引用綁定到左值
// int& ref2 = 10;        // 錯誤:左值引用不能綁定到右值
const int& ref3 = 10;     // 正確:const左值引用可以綁定到右值
int&& rref1 = 10;         // 正確:右值引用綁定到右值
// int&& rref2 = x;       // 錯誤:右值引用不能綁定到左值
int&& rref3 = std::move(x); // 正確:std::move將x轉換為右值

右值引用詳解

右值引用的語法與特性

右值引用使用雙&&符號聲明,主要用于綁定臨時對象(右值):

// 右值引用基本語法
int&& rref = 42;  // 綁定到字面量(右值)
int&& rref2 = getVal();  // 綁定到函數返回的臨時值(右值)

右值引用的關鍵特性:

  1. 延長臨時對象的生命周期
  2. 允許修改被引用的臨時對象
  3. 為移動語義提供基礎

引用折疊規則

在模板和auto推導中,涉及到右值引用的引用(如 T&& &&)時,C++使用引用折疊規則:

  • T& & 折疊為 T&
  • T& && 折疊為 T&
  • T&& & 折疊為 T&
  • T&& && 折疊為 T&&

簡單記憶:只要有一個是左值引用(單&),結果就是左值引用。

完美轉發

完美轉發是指在函數模板中,將參數按照其原始類型(保持左值/右值屬性)轉發給另一個函數:

template<typename T>
void perfectForward(T&& arg) {// std::forward保持arg的值類別(左值或右值)processArg(std::forward<T>(arg));
}int main() {int x = 10;perfectForward(x);        // x作為左值傳遞perfectForward(42);       // 42作為右值傳遞return 0;
}

std::forward的作用是:如果傳入的是左值,則作為左值轉發;如果傳入的是右值,則作為右值轉發。

移動語義

移動語義的基本概念

移動語義允許將資源(如動態分配的內存)從一個對象"偷"到另一個對象,而不是進行昂貴的復制。它特別適用于:

  • 臨時對象被用于初始化另一個對象
  • 對象即將被銷毀(如函數返回值)
  • 明確不再需要對象的原始狀態

std::move的作用

std::move是一個用于將左值轉換為右值引用的函數模板,它本身不移動任何東西,只是允許移動操作發生:

template <typename T>
typename std::remove_reference<T>::type&& move(T&& arg) noexcept {return static_cast<typename std::remove_reference<T>::type&&>(arg);
}

注意:調用std::move后,被移動對象進入"有效但未指定"的狀態,不應再使用它的值(除非重新賦值)。

移動構造函數與移動賦值運算符

移動構造函數和移動賦值運算符是支持移動語義的關鍵組件:

class MyString {
private:char* data;size_t size;public:// 移動構造函數MyString(MyString&& other) noexcept: data(other.data), size(other.size) {// 將源對象置于有效但可預測的狀態other.data = nullptr;other.size = 0;}// 移動賦值運算符MyString& operator=(MyString&& other) noexcept {if (this != &other) {delete[] data;  // 釋放自身資源// 從other"竊取"資源data = other.data;size = other.size;// 將other置于有效但可預測的狀態other.data = nullptr;other.size = 0;}return *this;}// 其他成員函數...
};

移動操作應該:

  1. 標記為noexcept(提高標準庫容器性能)
  2. 檢查自賦值(雖然移動自身很少見)
  3. 確保被移動對象保持在有效但可預測的狀態

實際應用示例

避免不必要的深拷貝

#include <iostream>
#include <vector>
#include <string>
#include <chrono>// 測量函數執行時間的輔助函數
template <typename Func>
long long measureTime(Func func) {auto start = std::chrono::high_resolution_clock::now();func();auto end = std::chrono::high_resolution_clock::now();return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}int main() {// 準備一個大字符串std::string largeString(1000000, 'x');// 使用拷貝long long copyTime = measureTime([&largeString]() {std::vector<std::string> vec;for (int i = 0; i < 100; ++i) {vec.push_back(largeString); // 創建largeString的副本}});// 使用移動long long moveTime = measureTime([&largeString]() {std::vector<std::string> vec;for (int i = 0; i < 100; ++i) {std::string temp = largeString; // 先創建副本vec.push_back(std::move(temp)); // 移動而非復制}});std::cout << "Copy time: " << copyTime << " microseconds" << std::endl;std::cout << "Move time: " << moveTime << " microseconds" << std::endl;std::cout << "Performance improvement: " << (copyTime - moveTime) * 100.0 / copyTime << "%" << std::endl;return 0;
}

實現高效的swap

通過移動語義,可以實現零拷貝的swap操作:

template<typename T>
void swap(T& a, T& b) {T temp = std::move(a);  // 移動而非復制a = std::move(b);       // 移動而非復制b = std::move(temp);    // 移動而非復制
}

高效實現類的移動語義

下面是一個完整的示例,展示如何為一個管理動態資源的類實現移動語義:

#include <iostream>
#include <utility>  // 為std::moveclass DynamicArray {
private:int* data;size_t size;public:// 構造函數DynamicArray(size_t size) : size(size), data(new int[size]) {std::cout << "Constructor called. Size: " << size << std::endl;for (size_t i = 0; i < size; ++i) {data[i] = 0;}}// 析構函數~DynamicArray() {std::cout << "Destructor called. Data: " << data << std::endl;delete[] data;}// 拷貝構造函數 - 深拷貝DynamicArray(const DynamicArray& other) : size(other.size), data(new int[other.size]) {std::cout << "Copy constructor called" << std::endl;for (size_t i = 0; i < size; ++i) {data[i] = other.data[i];}}// 拷貝賦值運算符 - 深拷貝DynamicArray& operator=(const DynamicArray& other) {std::cout << "Copy assignment operator called" << std::endl;if (this != &other) {delete[] data;size = other.size;data = new int[size];for (size_t i = 0; i < size; ++i) {data[i] = other.data[i];}}return *this;}// 移動構造函數DynamicArray(DynamicArray&& other) noexcept : data(other.data), size(other.size) {std::cout << "Move constructor called" << std::endl;other.data = nullptr;other.size = 0;}// 移動賦值運算符DynamicArray& operator=(DynamicArray&& other) noexcept {std::cout << "Move assignment operator called" << std::endl;if (this != &other) {delete[] data;data = other.data;size = other.size;other.data = nullptr;other.size = 0;}return *this;}// 輔助方法size_t getSize() const { return size; }void setValue(size_t index, int value) {if (index < size) {data[index] = value;}}int getValue(size_t index) const {if (index < size) {return data[index];}return -1;}// 打印數組內容void print() const {std::cout << "Array at " << data << " with size " << size << ": ";for (size_t i = 0; i < size && i < 5; ++i) {std::cout << data[i] << " ";}if (size > 5) std::cout << "...";std::cout << std::endl;}
};// 返回一個臨時DynamicArray對象
DynamicArray createArray(size_t size) {DynamicArray arr(size);for (size_t i = 0; i < size; ++i) {arr.setValue(i, i * 10);}return arr;  // 返回時會發生移動,而非拷貝
}int main() {std::cout << "=== Testing move semantics ===" << std::endl;std::cout << "\n1. Basic constructor:" << std::endl;DynamicArray arr1(5);arr1.print();std::cout << "\n2. Copy constructor:" << std::endl;DynamicArray arr2 = arr1;  // 調用拷貝構造函數arr2.print();std::cout << "\n3. Move constructor with temporary:" << std::endl;DynamicArray arr3 = createArray(3);  // 使用函數返回的臨時對象arr3.print();std::cout << "\n4. Move constructor with std::move:" << std::endl;DynamicArray arr4 = std::move(arr1);  // 顯式移動arr4.print();// arr1現在處于"有效但未指定"的狀態,其數據成員被移走了std::cout << "arr1 after move: ";arr1.print();  // 應該顯示空或默認值std::cout << "\n5. Move assignment:" << std::endl;DynamicArray arr5(2);arr5 = std::move(arr2);  // 移動賦值arr5.print();// arr2現在處于"有效但未指定"的狀態std::cout << "arr2 after move: ";arr2.print();std::cout << "\n=== End of scope, destructors will be called ===" << std::endl;return 0;
}

常見陷阱與最佳實踐

移動語義的陷阱

  1. 使用移動后的對象

    std::string s1 = "Hello";
    std::string s2 = std::move(s1);
    std::cout << s1 << std::endl;  // 危險:使用已移動的對象
    
  2. 在不適當的場景使用std::move

    // 不要在返回局部變量時使用std::move
    std::string badFunction() {std::string result = "value";return std::move(result);  // 反而阻止了RVO優化!
    }// 正確寫法
    std::string goodFunction() {std::string result = "value";return result;  // 編譯器會自動應用RVO/NRVO
    }
    
  3. 在條件表達式中使用std::move

    std::string s = condition ? std::move(a) : std::move(b);
    // 注意:無論選擇哪個分支,a和b都會被std::move轉換為右值!
    

最佳實踐

  1. 總是標記移動操作為noexcept

    MyClass(MyClass&& other) noexcept;
    MyClass& operator=(MyClass&& other) noexcept;
    
  2. 確保移動后的對象處于有效狀態

    // 在移動操作后
    other.data = nullptr;  // 防止原對象的析構函數釋放內存
    other.size = 0;        // 將對象重置為空
    
  3. 實現"大五"法則
    如果定義了任何一個拷貝構造、拷貝賦值、移動構造、移動賦值或析構函數,就應該考慮定義所有五個。

  4. 考慮顯式禁用不需要的操作

    class OnlyMovable {
    public:OnlyMovable(OnlyMovable&&) = default;OnlyMovable& operator=(OnlyMovable&&) = default;// 禁用拷貝OnlyMovable(const OnlyMovable&) = delete;OnlyMovable& operator=(const OnlyMovable&) = delete;
    };
    
  5. 使用RAII和智能指針簡化資源管理

    class ModernResource {
    private:std::unique_ptr<int[]> data;size_t size;public:// 使用unique_ptr自動處理移動語義ModernResource(size_t s) : data(std::make_unique<int[]>(s)), size(s) {}// 移動構造和賦值由編譯器自動生成且正確處理
    };
    

性能考量

移動語義的性能優勢在處理大型對象時尤為明顯。考慮以下情況:

// 假設每個字符串大小為1MB
std::vector<std::string> createAndFill(size_t n) {std::vector<std::string> result;std::string largeString(1024*1024, 'x');for (size_t i = 0; i < n; ++i) {// 在C++11前:這里會導致深拷貝// 在C++11后:push_back可以使用移動語義result.push_back(largeString);}return result;  // 返回值優化 + 移動語義
}

在這個例子中,如果沒有移動語義,每次push_back都會創建一個1MB字符串的完整副本。而有了移動語義,我們可以避免大部分的內存分配和復制操作。

總結

右值引用和移動語義是現代C++中最重要的優化技術之一,它們通過減少不必要的對象復制,大幅提高了程序的性能,特別是在處理大型數據結構時。主要優勢包括:

  1. 提高性能:通過"竊取"資源而不是復制,減少內存分配和數據復制
  2. 更高效的標準庫:標準容器和算法通過移動語義獲得顯著性能提升
  3. 表達能力增強:能夠明確區分對象的"移動"和"復制"語義

要充分利用右值引用和移動語義,建議:

  • 為管理資源的類實現移動操作
  • 理解并正確使用std::movestd::forward
  • 遵循移動語義的最佳實踐
  • 使用智能指針和標準庫容器自動受益于移動語義

在下一篇文章中,我們將探討C++11/14中另一個重要特性:lambda表達式,它如何簡化函數對象的創建和使用。


這是我C++學習之旅系列的第三十九篇技術文章。查看完整系列目錄了解更多內容。

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

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

相關文章

基于Qlearning強化學習的電梯群控系統高效調度策略matlab仿真

目錄 1.算法仿真效果 2.算法涉及理論知識概要 2.1 Q-learning強化學習原理 2.2 基于Q-learning的電梯群控系統建模 3.MATLAB核心程序 4.完整算法代碼文件獲得 1.算法仿真效果 matlab2022a仿真結果如下&#xff08;完整代碼運行后無水印&#xff09;&#xff1a; 仿真操作…

31.軟件時序控制方式抗干擾

軟件時序控制方式扛干擾 1. 軟件時序控制抗干擾的時間邏輯2. 應用案例 1. 軟件時序控制抗干擾的時間邏輯 &#xff08;1&#xff09;將受軟件控制的功能或軟件檢測到的狀態一一羅列&#xff1b; &#xff08;2&#xff09;將其中的潛在干擾和敏感信號分開&#xff1b; &#x…

Ubuntu環境下使用uWSGI服務器【以flask應用部署為例】

0、前置內容說明 首先要知道WSGI是什么&#xff0c;關于WSGI服務器的介紹看這篇&#xff1a;WSGI&#xff08;Web Server Gateway Interface&#xff09;服務器 由于從Python 3.11開始限制了在系統級 Python 環境中使用 pip 安裝第三方包&#xff0c;以避免與系統包管理器&am…

d3_v7繪制折線圖

<!DOCTYPE html> <html><head><meta charsetutf-8><title>需求</title><script src"https://d3js.org/d3.v7.min.js"></script><style>* {margin: 0;padding: 0;}html, body {width: 100%;height: 100%;displ…

Hotspot分析(1):單細胞轉錄組識別信息基因(和基因模塊)

這一期我們介紹一個常見的&#xff0c;高分文章引用很高的一個單細胞轉錄組分析工具Hotspot&#xff0c;它可針對單細胞轉錄組數據識別有意義基因或者基因module&#xff0c;類似于聚類模塊。所謂的”informative "的基因是那些在給定度量中相鄰的細胞之間以相似的方式表達…

爬蟲準備前工作

1.Pycham的下載 網址&#xff1a;PyCharm: The only Python IDE you need 2.Python的下載 網址&#xff1a;python.org&#xff08;python3.9版本之后都可以&#xff09; 3.node.js的下載 網址&#xff1a;Node.js — 在任何地方運行 JavaScript&#xff08;版本使用18就可…

基于Springboot旅游網站系統【附源碼】

基于Springboot旅游網站系統 效果如下&#xff1a; 系統登陸頁面 系統主頁面 景點信息推薦頁面 路線詳情頁面 景點詳情頁面 確認下單頁面 景點信息管理頁面 旅游路線管理頁面 研究背景 隨著互聯網技術普及與在線旅游消費習慣的深化&#xff0c;傳統旅游服務模式面臨效率低、…

利用KMP找出模式串在目標串中所有匹配位置的起始下標

問題關鍵&#xff1a;完成首次匹配之后需要繼續進行模式匹配。 到這一步后&#xff0c;我們不能直接將j 0然后開始下一輪匹配&#xff0c;因為已經匹配過的部分&#xff08;藍色部分&#xff09;中仍然可能存在與模式串重疊的子串&#xff1a; 解決辦法&#xff1a; 找到藍…

RR(Repeatable Read)級別如何防止幻讀

在 MySQL 數據庫事務隔離級別中&#xff0c;RR&#xff08;可重復讀&#xff09; 通過 MVCC&#xff08;多版本并發控制&#xff09; 和 鎖機制 的組合策略來避免幻讀問題。 一、MVCC機制&#xff1a;快照讀與版本控制 快照讀&#xff08;Snapshot Read&#xff09; 每個事務啟…

Android運行時ART加載類和方法的過程分析

目錄 一,概述 二,ART運行時的入口 一,概述 既然ART運行時執行的都是翻譯DEX字節碼后得到的本地機器指令了&#xff0c;為什么還需要在OAT文件中包含DEX文件&#xff0c;并且將它加載到內存去呢&#xff1f;這是因為ART運行時提供了Java虛擬機接口&#xff0c;而要實現Java虛…

Javase 基礎加強 —— 02 泛型

本系列為筆者學習Javase的課堂筆記&#xff0c;視頻資源為B站黑馬程序員出品的《黑馬程序員JavaAI智能輔助編程全套視頻教程&#xff0c;java零基礎入門到大牛一套通關》&#xff0c;章節分布參考視頻教程&#xff0c;為同樣學習Javase系列課程的同學們提供參考。 01 認識泛型…

Oracle VirtualBox 在 macOS 上的詳細安裝步驟

Oracle VirtualBox 在 macOS 上的詳細安裝步驟 一、準備工作1. 系統要求2. 下載安裝包二、安裝 VirtualBox1. 掛載安裝鏡像2. 運行安裝程序3. 處理安全限制(僅限首次安裝)三、安裝擴展包(增強功能)四、配置第一個虛擬機1. 創建新虛擬機2. 分配內存3. 創建虛擬硬盤4. 加載系…

RAGFlow 接入企業微信應用實現原理剖析與最佳實踐

背景 近期有醫美行業客戶咨詢我們智能客服產品&#xff0c;期望將自己企業的產品、服務以及報價信息以企微應用的方式給到客戶進行體驗互動&#xff0c;提升企業運營效率。關于企業微信對接&#xff0c;我們分享下最佳實踐&#xff0c;拋磚引玉。效果圖如下&#xff1a; 這里也…

【心海資源】子比主題新增注冊與會員用戶展示功能模塊及實現方法

內容改寫&#xff1a; 本次分享的是子比主題頂部展示注冊用戶與會員信息的功能模塊及其實現方式。 你可以通過兩種方式啟用該功能&#xff1a; 直接在后臺進入“外觀 → 小工具”啟用該展示模塊&#xff0c;操作簡便&#xff1b;也可將提供的代碼覆蓋至子比主題目錄中&#…

CSDN積分詳解(介紹、獲取、用途)

&#x1f91f;致敬讀者 &#x1f7e9;感謝閱讀&#x1f7e6;笑口常開&#x1f7ea;生日快樂?早點睡覺 &#x1f4d8;博主相關 &#x1f7e7;博主信息&#x1f7e8;博客首頁&#x1f7eb;專欄推薦&#x1f7e5;活動信息 文章目錄 積分**一、積分類型及用途****二、積分獲取途…

【iview】es6變量結構賦值(對象賦值)

變量的解構賦值 以iview的src/index.js中Vue.prototype.$IVIEW改造為例練習下怎么使用變量的解構賦值 原來的寫法&#xff1a; const install function(Vue, opts {}) {if (install.installed) return;locale.use(opts.locale);locale.i18n(opts.i18n);Object.keys(iview).fo…

【c++深入系列】:萬字詳解vector(附模擬實現的vector源碼)

&#x1f525; 本文專欄&#xff1a;c &#x1f338;作者主頁&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客勵志語錄&#xff1a; 種子破土時從不問‘會不會有光’&#xff0c;它只管生長 ★★★ 本文前置知識&#xff1a; 模版 1.什么是vector 那么想必大家都學過順…

MySQL基礎關鍵_007_DQL 練習

目 錄 一、題目 二、答案&#xff08;不唯一&#xff09; 1.查詢每個部門薪資最高的員工信息 2.查詢每個部門高于平均薪水的員工信息 3. 查詢每個部門平均薪資等級 4.查詢部門中所有員工薪資等級的平均等級 5.不用分組函數 max 查詢最高薪資 6.查詢平均薪資最高的部門編…

Jenkis安裝、配置及賬號權限分配保姆級教程

Jenkis安裝、配置及賬號權限分配保姆級教程 安裝Jenkins下載Jenkins啟動Jenkins配置Jenkins入門Jenkins配置配置中文配置前端自動化任務流新建任務拉取代碼打包上傳云服務并運行配置后端自動化任務流新建任務拉取代碼打包上傳云服務并運行賬號權限分配創建用戶分配視圖權限安裝…

虛函數 vs 純虛函數 vs 靜態函數(C++)

&#x1f9e9; 一圖看懂&#xff1a;虛函數 vs 純虛函數 特性虛函數&#xff08;Virtual&#xff09;純虛函數&#xff08;Pure Virtual&#xff09;語法virtual void foo();virtual void foo() 0;是否必須實現? 必須在類中實現? 不在基類實現&#xff0c;派生類必須實現是…