天王的记忆 發表於 2026-1-5 01:02:00

【C++】移动语义和完美转发

<h2 id="前言">前言</h2>
<p>学习C++移动语义和完美转发笔记,记录左值、右值、std::move()、万能引用、引用折叠等相关内容。</p>
<h2 id="概念">概念</h2>
<ul>
<li>
<p><strong>左值 (lvalue)</strong> 它是<strong>在内存中有明确存储地址、可以被寻址</strong>的值。如果你可以对一个表达式取地址(使用 <code>&amp;</code> 运算符),那么它就是一个左值。左值通常是持久的,在它所在的定义域结束之前一直存在</p>
</li>
<li>
<p><strong>左值引用(Lvalue Reference)本质上就是给一个现有的左值起了一个“别名”</strong>,左值引用定义即初始化。</p>
<ul>
<li><strong>普通左值引用 (<code>T&amp;</code>)</strong>:只能绑定到<strong>非 const 左值</strong>。</li>
<li><strong>常量左值引用 (<code>const T&amp;</code>)</strong>:可以绑定到<strong>一切</strong>(左值、const 左值、右值)。</li>
</ul>
</li>
</ul>
<pre><code class="language-C++">const int&amp; temp = 10;
//编译器会在内存中产生一个临时变量存储 10。
//temp 绑定到这个临时变量上。
//这个临时变量的寿命会变得和引用 temp 一样长。
</code></pre>
<ul>
<li>
<p><strong>右值 (rvalue)</strong> 右值就是那些<strong>临时出现、没有持久名字、无法取地址</strong>的值。如果你无法对一个表达式使用 <code>&amp;</code> 取地址运算符,或者它是一个即将销毁的临时对象,它就是右值。右值通常是“瞬时”的,在包含它的表达式执行完之后,它就会被立即销毁.</p>
</li>
<li>
<p><strong>右值引用(Rvalue Reference)一种绑定到右值(临时对象)的引用类型</strong>。</p>
<blockquote>
<p>int&amp;&amp; rref = 10;</p>
</blockquote>
</li>
<li>
<p><code>std::move</code> 并不移动任何东西。它的唯一作用是:<strong>强制将一个左值转为右值引用</strong>。</p>
<pre><code class="language-C++">template &lt;typename T&gt;
typename std::remove_reference&lt;T&gt;::type&amp;&amp; move(T&amp;&amp; t) {   
    return static_cast&lt;typename std::remove_reference&lt;T&gt;::type&amp;&amp;&gt;(t);
}
</code></pre>
<ul>
<li>
<p><strong><code>std::remove_reference&lt;T&gt;::type</code></strong>:这是一个类型萃取工具。无论 <code>T</code> 是 <code>int</code>、<code>int&amp;</code> 还是 <code>int&amp;&amp;</code>,它都能把修饰符去掉,只留下纯粹的底层类型 <code>int</code>。</p>
</li>
<li>
<p><code>static_cast</code> 将输入变量 <code>t</code> 强制转换为该类型的<strong>右值引用</strong>(即 <code>type&amp;&amp;</code>)</p>
</li>
</ul>
</li>
<li>
<p><strong>std::forward</strong> 如果原始参数是<strong>左值</strong>,转发后仍然是<strong>左值</strong>。如果原始参数是<strong>右值</strong>,转发后仍然是<strong>右值</strong>。</p>
</li>
</ul>
<pre><code class="language-C++">template &lt;typename T&gt;
T&amp;&amp; forward(typename std::remove_reference&lt;T&gt;::type&amp; t) noexcept {
    return static_cast&lt;T&amp;&amp;&gt;(t);
}
</code></pre>
<ul>
<li>
<p><strong>移动语义(Move Semantics)</strong> 本质是<strong>资源所有权的转移</strong>,即将一个临时对象(右值)持有的资源转移,来避免昂贵的深拷贝操作。是由类实现的<strong>功能</strong>(通过移动构造函数)</p>
</li>
<li>
<p><strong>完美转发(Perfect Forwarding)</strong> 是:<strong>在函数模板中,将参数原封不动地转发给另一个函数,同时完全保留参数的所有属性(包括它是左值还是右值、是否带有 <code>const</code> 或 <code>volatile</code> 修饰符)。</strong></p>
<pre><code class="language-C++">template &lt;typename T&gt;
T&amp;&amp; forward(typename std::remove_reference&lt;T&gt;::type&amp; t) noexcept {
    return static_cast&lt;T&amp;&amp;&gt;(t);
}
</code></pre>
<ul>
<li>
<p>如果原始参数是<strong>左值</strong>,转发后仍然是<strong>左值</strong>。</p>
</li>
<li>
<p>如果原始参数是<strong>右值</strong>,转发后仍然是<strong>右值</strong>。</p>
</li>
</ul>
</li>
<li>
<p><strong>万能引用</strong></p>
<ul>
<li>使用 <code>&amp;&amp;</code> 符号</li>
<li><strong>必须发生模板类型推导</strong>:通常出现在 <code>template &lt;typename T&gt;</code> 之后的 <code>T&amp;&amp;</code>(不能加 <code>const</code>)。</li>
<li><strong>形式必须完全匹配</strong>:必须是具体的 <code>T&amp;&amp;</code>,不能有 <code>const</code> 或 <code>std::vector&lt;T&gt;&amp;&amp;</code> 等修饰。</li>
<li><strong>万能引用是完美转发的“门”。</strong> 它把参数原封不动地领进来(无论是左值还是右值),然后配合 <code>std::forward</code> 把它原封不动地送出去。</li>
</ul>
</li>
<li>
<p><strong>引用折叠</strong>是 C++ 编译器在处理“引用的引用”时遵循的一套自动简化规则。只要有左值引用(&amp;)参与,结果就是左值引用;只有全是右值引用(&amp;&amp;)时,结果才是右值引用。</p>
</li>
</ul>
<h2 id="内容">内容</h2>
<h3 id="左值">左值</h3>
<p>内存中有明确存储地址、可以被寻址的值</p>
<pre><code class="language-C++">int a = 10;      // a 是左值(有名字,可取地址)
a = 20;            // a 在左边,OK

int* p = &amp;a;       // p 是左值
*p = 30;         // *p(解引用结果)是左值

int** pp = &amp;(*p);// 我们可以对 (*p) 再次取地址 ,可以看到*p是可以取地址的

const int b = 5;   // b 是左值(虽然不可修改,但它有内存地址,是具名变量)

void func(int&amp;&amp; x) {
    // 这里的 x 类型是右值引用,但 x 本身是一个有名字的变量
    // 所以在函数内部,x 是一个左值!
    int* p = &amp;x; // 这是合法的
}
</code></pre>
<h3 id="左值引用">左值引用</h3>
<p>对左值的引用,即给左值起别名,必须初始化。</p>
<pre><code class="language-C++">int a = 10;
int&amp; ref = a;// ref 是 a 的左值引用

const int&amp; r3 = 10; // 正确!

std::cout &lt;&lt; "a" &lt;&lt; "b" &lt;&lt; "c"; // 每次调用 &lt;&lt; 都返回 cout 的左值引用
</code></pre>
<h3 id="右值">右值</h3>
<p>没有固定地址、没有名字的值,通常是临时结果、字面量或即将销毁的对象。</p>
<pre><code class="language-C++">10;               // 纯右值:字面量

a + b;            // 纯右值:运算的中间结果,没有名字

func(5);            // 如果 func 返回一个值(非引用),func(5) 就是右值

std::string("Hi");// 纯右值:临时构造的匿名对象
</code></pre>
<h3 id="右值引用">右值引用</h3>
<p>绑定到右值上。 它的符号是 <code>&amp;&amp;</code></p>
<pre><code class="language-C++">int&amp;&amp; r1 = 10;          // 正确:10 是右值

int a = 10;
int&amp;&amp; r3 = std::move(a); // 正确:std::move 把左值转成了右值(将亡值)

int n = 5;
// int&amp;&amp; r = ++n; // 错误:++n 返回的是修改后的 n 本身(有地址)
int&amp;&amp; r = n++;    // 正确:n++ 返回的是一个临时副本(旧值 5),n 本身已变
</code></pre>
<h3 id="万能引用引用折叠">万能引用&amp;引用折叠</h3>
<pre><code class="language-C++">template&lt;typename T&gt;
void func(T&amp;&amp; param); // 这是一个万能引用
//场景 A:传入左值变量
int a = 10;
func(a);

//因为 a 是左值,根据万能引用的特殊推导规则,编译器将 T 推导为 int&amp;。
//为什么不能是int;因为如果为 int,函数变为 void func(int &amp;&amp; param) 这个函数只能接收右值,所以编译器只能推导为int &amp;
//代入模板:函数签名变成 void func(int&amp; &amp;&amp; param);。
//引用折叠:根据规则,int&amp; &amp;&amp; 含有左值引用,折叠为 int&amp;。
//最终形态:void func(int&amp; param); —— 成功以左值引用的方式接收了变量。


//场景 B:传入右值字面量 20
func(20);
//推导:因为 20 是右值,编译器将 T 推导为 int。
//模板:函数签名变成 void func(int&amp;&amp; param);。
//折叠:没有冲突,或者看作 int&amp;&amp;,保持 int&amp;&amp;。
//最终形态:void func(int&amp;&amp; param); —— 成功以右值引用的方式接收了临时变量
</code></pre>
<h3 id="移动语义">移动语义</h3>
<p>我们将一个临时对象赋值给另一个对象时,会触发<strong>深拷贝</strong>。比如一个包含 很多数据的 <code>std::vector</code>,拷贝它需要重新分配内存再复制数据,这非常耗时。</p>
<p><strong>移动语义</strong> 允许我们直接获取临时对象的资源,只需修改指针指向,而不必重新分配内存。</p>
<pre><code class="language-C++">#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;utility&gt;

class MyBuffer {
private:
    int* data; // 唯一的私有属性

public:
    // 构造函数 explicit防止隐式转换   
    //MyBuffer b2 = 100;   ❌ 编译错误,不能隐式转换
    explicit MyBuffer(int value) : data(new int(value)) {
      std::cout &lt;&lt; "分配内存并存入: " &lt;&lt; *data &lt;&lt; std::endl;
    }

    // 析构函数
    ~MyBuffer() {
      if (data) {
            delete data;
            data = nullptr;
            std::cout &lt;&lt; "释放内存" &lt;&lt; std::endl;
      }
    }

    // ---------------------------------------------------------
    // 拷贝构造函数 (Copy Constructor) - 深拷贝
    // ---------------------------------------------------------
    MyBuffer(const MyBuffer&amp; other) : data(other.data ? new int(*other.data) : nullptr) {
      std::cout &lt;&lt; "深拷贝数据: " &lt;&lt; (data ? *data : 0) &lt;&lt; std::endl;
    }

    // ---------------------------------------------------------
    // 移动构造函数 (Move Constructor)
    // ---------------------------------------------------------
    MyBuffer(MyBuffer&amp;&amp; other) noexcept : data(other.data) {
      other.data = nullptr;
      std::cout &lt;&lt; "资源所有权已转移" &lt;&lt; std::endl;
    }

    // ---------------------------------------------------------
    // 移动赋值运算符 (Move Assignment Operator)
    // ---------------------------------------------------------
    MyBuffer&amp; operator=(MyBuffer&amp;&amp; other) noexcept {
      std::cout &lt;&lt; "执行移动赋值" &lt;&lt; std::endl;
      
      if (this != &amp;other) {
            delete data;      // 1. 释放当前对象持有的旧内存
              data = other.data;// 2. 接管新资源
              other.data = nullptr;
      }
      return *this;
    }

    // 访问器方法
    int getValue() const {
      return data ? *data : 0;
    }

    void setValue(int value) {
      if (data) {
            *data = value;
      } else {
            data = new int(value);
      }
    }

    // 检查是否拥有资源
    bool isEmpty() const {
      return data == nullptr;
    }
};

int main() {
    std::cout &lt;&lt; "=== 测试移动语义 ===" &lt;&lt; std::endl;
   
    // 测试移动构造函数
    MyBuffer b1(100);
    MyBuffer b2(std::move(b1)); // 触发移动构造
    // b1 现在是"有效但未指定状态"
    std::cout &lt;&lt; "b1是否为空: " &lt;&lt; b1.isEmpty() &lt;&lt; std::endl;
    std::cout &lt;&lt; "b2的值: " &lt;&lt; b2.getValue() &lt;&lt; std::endl;
   
    // 测试移动赋值
    MyBuffer b3(300);
    MyBuffer b4(400);
    std::cout &lt;&lt; "\n移动赋值前 - b3: " &lt;&lt; b3.getValue() &lt;&lt; ", b4: " &lt;&lt; b4.getValue() &lt;&lt; std::endl;
    b4 = std::move(b3);
    std::cout &lt;&lt; "移动赋值后 - b3是否为空: " &lt;&lt; b3.isEmpty() &lt;&lt; std::endl;
    std::cout &lt;&lt; "移动赋值后 - b4: " &lt;&lt; b4.getValue() &lt;&lt; std::endl;
   
    return 0;
}
</code></pre>
<p><strong>out</strong></p>
<pre><code class="language-bat">root1@ubuntu:~/work/hello/build$ ./hello_cmake_g
=== 测试移动语义 ===
分配内存并存入: 100
资源所有权已转移
b1是否为空: 1
b2的值: 100

分配内存并存入: 300
分配内存并存入: 400

移动赋值前 - b3: 300, b4: 400
执行移动赋值
移动赋值后 - b3是否为空: 1
移动赋值后 - b4: 300
释放内存
释放内存

</code></pre>
<h3 id="完美转发">完美转发</h3>
<pre><code>#include &lt;iostream&gt;
#include &lt;utility&gt;

void target(int&amp; x) {
        std::cout &lt;&lt; "调用左值函数\n";
}
void target(int&amp;&amp; x) {
        std::cout &lt;&lt; "调用右值函数\n";
}

template &lt;typename T&gt;
void perfectForwarder(T&amp;&amp; arg) {
    // std::forward 会根据 T 的类型决定是 cast 成左值还是右值
    target(std::forward&lt;T&gt;(arg));
}

int main() {
    int a = 10;
    std::cout &lt;&lt; "传递了右值\n";
    perfectForwarder(a);
   
    std::cout &lt;&lt; "传递了右值\n";
    perfectForwarder(20);
}
</code></pre>
<p><strong>out</strong></p>
<pre><code class="language-bat">root1@ubuntu:~/work/hello/build$ ./hello_cmake_g
传递了右值
调用左值函数
传递了右值
调用右值函数
</code></pre>
<h2 id="思考">思考</h2>
<hr>
<h3 id="i-和-i的区别为什么要习惯性写i-"><strong>++i</strong> 和 <strong>i++</strong>的区别,为什么要习惯性写++i ?</h3>
<ul>
<li>
<p>前置自增 <code>++i</code> (左值)</p>
<p>在 C++ 的底层实现中,前置自增的操作类似于:“先给这个内存地址里的值加 1,然后把这个<strong>地址</strong>传回去。”</p>
<ul>
<li>
<p><strong>返回类型:</strong> 通常是引用类型(如 <code>T&amp;</code>)。</p>
</li>
<li>
<p><strong>内存逻辑:</strong> 它直接在原变量上操作,不产生中间人。</p>
</li>
<li>
<p><strong>为什么是左值:</strong> 因为它返回的是变量本身,它在表达式结束后依然存在,拥有确定的内存地址。</p>
</li>
</ul>
</li>
<li>
<p>后置自增 <code>i++</code> (右值)</p>
<p>后置自增的操作逻辑则复杂一些:“先把当前的值存到一个临时地方,给原变量加 1,然后把刚才那个<strong>临时值</strong>传回去。”</p>
<ul>
<li>
<p><strong>返回类型:</strong> 通常是按值返回(如 <code>T</code>)。</p>
</li>
<li>
<p><strong>内存逻辑:</strong> 产生了一个<strong>临时对象(Temporary Object)</strong>。</p>
<ul>
<li><strong>为什么是右值:</strong> 那个临时副本在表达式执行完的那一刻就被销毁了。它没有持久的“身份”,你无法通过地址再次找到它,因此它是右值。</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><strong>对于内置类型(如 int):</strong> 现代编译器非常聪明,通常会把 <code>i++</code> 优化掉,使两者性能一致。</p>
<p><strong>对于自定义类型(如迭代器 <code>std::map::iterator</code>):</strong> 区别极大。</p>
<ul>
<li><code>++i</code> 直接修改内部指针并返回引用。</li>
<li><code>i++</code> 必须先调用拷贝构造函数创建一个<strong>副本</strong>,修改原值,最后返回那个副本。这个副本的创建和随后的析构都是额外的开销。</li>
</ul>
<hr>
<hr>
<h3 id="stdmove-与-stdforward-的本质区别">std::move 与 std::forward 的本质区别?</h3>
<ul>
<li><strong><code>std::move&lt;T&gt;(x)</code></strong>:<strong>无条件转换</strong>。不管 <code>x</code> 是什么,通通强制转为右值引用。</li>
<li><strong><code>std::forward&lt;T&gt;(x)</code></strong>:<strong>有条件转换</strong>。只有当 <code>T</code> 被推导为右值引用时,才将其转换为右值引用;否则保持左值属性。</li>
</ul>
<hr>
<hr>
<h3 id="移动构造函数为什么要加-noexcept">移动构造函数为什么要加 <code>noexcept</code>?</h3>
<p><strong>为了 STL 容器的异常安全性。</strong> 当 <code>std::vector</code> 扩容需要搬移元素时,如果你的移动构造函数不声明 <code>noexcept</code>,<code>vector</code> 为了保证在搬移失败时能回滚,会放弃使用高效的“移动语义”,转而使用效率较低的“拷贝构造”。</p>
<hr><br><br>
来源:https://www.cnblogs.com/hjk-airl/p/19434573
頁: [1]
查看完整版本: 【C++】移动语义和完美转发