当前位置: 首页 > news >正文

左值引用和右值引用

一、基本概念

  1. 左值(lvalue)和右值(rvalue)

    • 左值指的是有确定存储位置(地址)的对象,通常可以出现在赋值语句左侧。例如:变量名、解引用指针得到的对象、数组元素等都属于左值。

    • 右值一般指临时对象或字面常量,通常没有固定的存储地址,只能出现在赋值语句右侧。例如:字面量(42"hello")、表达式求值产生的临时结果(a + b、函数返回的非引用类型)等都属于右值。

  2. 左值引用(lvalue reference)T&

    • 语法:T&

    • 含义:引用一个左值,必须绑定到一个具有名字且可寻址的对象上。

    • 作用:可以通过引用直接操作原对象,不产生拷贝;常用于函数参数(接收可修改的实参)或延长临时对象的生命周期(使用 const T&)。

  3. 右值引用(rvalue reference)T&&(C++11 引入)

    • 语法:T&&

    • 含义:引用一个右值,只能绑定到临时对象(比如函数返回的非引用类型对象、字面量或者std::move之后的结果)。

    • 作用:为“移动语义(move semantics)”和“完美转发(perfect forwarding)”提供基础,通过接收将要被销毁的临时对象,可以“窃取”其内部资源而不是拷贝。


二、左值引用与右值引用的区别

特性左值引用 T&右值引用 T&&
可绑定的对象只能绑定到 左值(命名变量等)只能绑定到 右值(临时对象、字面常量、std::move产生的中间值)
是否可修改可修改所引用的对象可以修改所引用的临时对象(临时对象本来就要销毁)
延长生命周期const T& 可延长临时对象生命周期T& 不能绑定临时对象绑定临时对象后,可操作临时,直到其生命周期结束
主要用途传递可修改的已有对象,避免拷贝实现移动语义、完美转发,减少不必要的深度拷贝
  • 左值引用 T&

    • 只能引用已有的命名对象。

    • 用途:

      1. 函数参数接收时,可以直接修改传入的实参(例如 void foo(int& x))。

      2. 避免拷贝开销(例如 void print(const std::string& s))。

      3. const T& 可以绑定到右值,用于只读访问且延长临时对象生命周期。

  • 右值引用 T&&

    • 只能引用临时对象(右值)。

    • 用途:

      1. 移动构造/移动赋值:从临时对象“窃取”内部资源,而不做深拷贝。

      2. 完美转发:在模板中,通过 T&&(与 std::forward<T>(…))保持函数参数的值类别(左值/右值)不变。

      3. 禁止绑定左值:直接传递命名对象到 T&& 参数会编译错误,除非显式使用 std::move


三、典型示例

下面通过几个示例来直观说明它们的使用场景和区别。

1. 直接绑定示例

#include <iostream>
#include <string>int main() {int  a = 10;            // a 是左值int& lref = a;          // 左值引用只能绑定到左值// int& lref2 = 20;     // 错误:不能把 int& 绑定到字面量 20(右值)上int&& rref = 20;        // 右值引用只能绑定到右值// int&& rref2 = a;     // 错误:不能把 int&& 绑定到左值 a 上std::string s = "Hello";std::string& ls = s;    // 合法,引用已有 std::string 对象const std::string& lsc = "World"; // const std::string& 可以绑定到右值 "World",临时字符串的生命周期延长至 lsc 作用域结束std::string&& rs = std::string("Temp"); // 右值引用绑定到了临时 std::string("Temp"),可在后续对 rs 进行修改std::cout << "a: " << a << "\n";              // a: 10std::cout << "lref: " << lref << "\n";        // lref: 10std::cout << "rref: " << rref << "\n";        // rref: 20std::cout << "ls: " << ls << "\n";            // ls: Hellostd::cout << "lsc: " << lsc << "\n";          // lsc: Worldstd::cout << "rs: " << rs << "\n";            // rs: Tempreturn 0;
}
  • int& lref = a;:左值引用 lref 绑定到左值 a

  • int&& rref = 20;:右值引用 rref 绑定到字面量 20(右值)。

  • const std::string& lsc = "World";const T& 可以绑定到右值(临时字符串),并将其生命周期延长至 lsc 的作用域结束。

  • std::string&& rs = std::string("Temp");:右值引用 rs 绑定到临时字符串对象,此时可以对该临时对象进行修改(不过它最终会析构)。


2. 作为函数参数的区别

通常我们会看到如下重载示例,用以区分传入的是左值还是右值:

#include <iostream>// 重载 1:接受左值引用
void process(int& x) {std::cout << "process(int&): 接收左值引用, x = " << x << "\n";
}// 重载 2:接受右值引用
void process(int&& x) {std::cout << "process(int&&): 接收右值引用, x = " << x << "\n";
}int main() {int a = 5;process(a);      // 调用 process(int&), 因为 a 是左值process(10);     // 调用 process(int&&), 因为 10 是右值process(std::move(a)); // std::move(a) 将 a 转换成右值, 因此调用 process(int&&)return 0;
}
  • 当实参是一个命名变量a),它是左值,因此会匹配 void process(int&)

  • 当实参是一个字面量临时表达式(如 10std::move(a)),它们是右值,因此会匹配 void process(int&&)


3. 移动构造 vs 拷贝构造

以一个简单的自定义类 MyString 为例,演示移动构造函数与拷贝构造函数的区别。

#include <iostream>
#include <cstring>class MyString {
public:char* data;// 构造函数:从 C 风格字符串构造MyString(const char* s) {size_t len = std::strlen(s);data = new char[len + 1];std::strcpy(data, s);std::cout << "构造 MyString(\"" << data << "\")\n";}// 拷贝构造:深拷贝MyString(const MyString& other) {size_t len = std::strlen(other.data);data = new char[len + 1];std::strcpy(data, other.data);std::cout << "调用 拷贝构造, 源 = \"" << other.data << "\"\n";}// 移动构造:窃取指针,避免拷贝MyString(MyString&& other) noexcept {data = other.data;         // 直接窃取资源指针other.data = nullptr;      // 将源置空,避免析构时释放两次std::cout << "调用 移动构造\n";}// 析构函数~MyString() {if (data) {std::cout << "析构 MyString(\"" << data << "\")\n";delete[] data;} else {std::cout << "析构 MyString(nullptr)\n";}}
};MyString makeString() {MyString temp("临时");return temp; // C++11 以后可进行移动构造而非拷贝
}int main() {MyString s1("Hello");       // 普通构造MyString s2 = s1;           // 拷贝构造MyString s3 = makeString(); // 由于 makeString 返回临时对象,可触发移动构造(或编译器优化)// 可以强制使用移动构造:MyString s4 = std::move(s1);return 0;
}
  • 调用 MyString s2 = s1; 时,实参 s1 是一个左值,因此调用的是 拷贝构造,会分配新内存并拷贝字符串内容。

  • 调用 MyString s3 = makeString(); 时,makeString() 返回的临时对象是一个右值,因此会优先调用 移动构造(如果编译器没有做完全优化)。移动构造只会“窃取”临时对象的内部 char* 指针,而不会再做深拷贝。

  • s1 应用 std::move(s1),即把左值 s1 强制转换为右值,再传递给 MyString s4 = std::move(s1);,也会调用移动构造,将 s1 的内部指针转移给 s4,此时 s1.data 置为 nullptr


4. 完美转发(Perfect Forwarding)示例

在模板中,如果想让函数“如实”地将传入参数的值类别(左值/右值)传给另一个函数,可以使用右值引用和 std::forward。示例:

#include <iostream>
#include <utility> // std::forward, std::movevoid target(int& x) {std::cout << "target(int&) 被调用,x = " << x << "\n";
}void target(int&& x) {std::cout << "target(int&&) 被调用,x = " << x << "\n";
}// 通用转发函数模板
template <typename T>
void wrapper(T&& param) {// 直接传给 target,但保留 param 本身的值类别// 如果原参数是左值,就调用 target(int&); 如果是右值,就调用 target(int&&)target(std::forward<T>(param));
}int main() {int a = 100;wrapper(a);            // a 是左值 -> 调用 target(int&)wrapper(200);          // 200 是右值 -> 调用 target(int&&)int&& rr = 300;wrapper(rr);           // rr 本身虽然声明为右值引用,但 rr 名称是左值 -> 调用 target(int&)wrapper(std::move(rr)); // std::move(rr) 是右值 -> 调用 target(int&&)return 0;
}
  • wrapper(a)a 是左值,模板参数 T 被推断为 int&T&& 变为 int& &&,折叠后仍为 int&,所以 param 是左值引用,std::forward<T>(param) 仍是左值,调用 target(int&)

  • wrapper(200)200 是右值,T 被推断为 intT&&int&&param 是右值引用。std::forward<int>(param) 是右值,调用 target(int&&)

  • int&& rr = 300; 声明了一个右值引用 rr,但 rr 本身是一个命名变量(左值)。所以:

    • wrapper(rr):传入 rr(是左值),和 wrapper(a) 类似,依然调用 target(int&)

    • wrapper(std::move(rr))std::move(rr)rr 强制转换为右值,调用 target(int&&)


四、总结

  1. 绑定限制

    • T& 只能绑定到左值,不能绑定到右值(除非加上 const,即 const T& 可以绑定到右值,用于只读)。

    • T&& 只能绑定到右值,不能直接绑定到左值(除非对左值使用 std::movestd::forward 强制转换为右值)。

  2. 主要用途

    • 左值引用 T&:主要用于接收已有对象并进行读取/修改,避免拷贝开销。

    • 右值引用 T&&:主要用于实现“移动语义”,在可以销毁的临时对象上直接窃取资源;同时在模板中用于“完美转发”以保留参数的值类别。

  3. 性能意义

    • 使用 T&& 实现移动构造/移动赋值,可以显著减少对大对象(如容器、字符串等)的深拷贝,从而提升性能。

    • std::forward<T>T&& 配合可以让模板函数在转发参数时不丢失值类别,避免不必要的拷贝或移动。

  4. 注意事项

    • 虽然 T&& 只能绑定右值,但当 T 已推断为引用类型(如 int&)时,T&& 会出现“引用折叠”(reference collapsing)现象:

      • 如果 TU&,那么 T&& 等价于 U&

      • 如果 TU,那么 T&& 就是 U&&

    • 在函数重载中,若同时存在 void f(int&)void f(int&&),传入的实参的值类别会直接影响调用哪个重载。

http://www.xdnf.cn/news/791461.html

相关文章:

  • 【C++篇】STL适配器(下篇):优先级队列与反向迭代器的底层奥秘
  • Splitting Items
  • torch.nn中的各种组件
  • element级联地址选择器
  • java类的生命周期
  • Make All Equal
  • 2.2.2 06年T3
  • LeetCode 152. 乘积最大子数组 - 动态规划解法详解
  • 集成学习三种框架
  • C++中的指针参数传递与引用参数传递详解
  • 5985/wsman 是什么?
  • 一、基础环境配置
  • Linux中实现用户态DMA直通访问的零拷贝机制
  • 《Spring Bean 是怎么被创建出来的?容器启动流程全景分析》
  • 小体积涵盖日常办公等多功能的软件
  • MyBatis实战项目测试
  • 2025.6.3学习日记 Nginx 基本概念 配置 指令 文件
  • React-native之Flexbox
  • nginx 如何禁用tls1.0
  • CSS radial-gradient函数详解
  • JVM-内存结构
  • MAU算法流程理解
  • VueUse:组合式API实用函数全集
  • ADI硬件笔试面试题型解析上
  • DevEco Studio的使用
  • VUE组件库开发 八股
  • 时态--10--被动语态
  • Selenium 中 JavaScript 点击操作的原理及应用
  • Java:跨越时代的编程语言,持续引领技术革新
  • IPython 使用技巧整理