C++ 左值、右值、左值引用、右值引用的用途及区别
作者:xclic
1、左值与右值
左值和右值是表达式的属性,核心区别在于:能否取地址、是否有持久的存储。
1.1 左值:有名字、能取地址、可被修改(通常)
左值是 “可以放在赋值号左边” 的表达式(但并非绝对,如 const
左值不能被修改),它有明确的内存地址,生命周期较长(如变量)。
int a = 10; // a 是左值(有名字、能取地址) int* p = &a; // 合法:左值可以取地址 const int b = 20; // b 是 const 左值(有名字、能取地址,但不能修改) // b = 30; // 错误:const 左值不可修改 int arr[5]; arr[0] = 1; // arr[0] 是左值(数组元素有地址)
1.2 右值:无名字、不能取地址、临时存在
右值是 “只能放在赋值号右边” 的表达式,通常是临时结果(如字面量、表达式计算结果),没有持久的内存地址,生命周期短暂(表达式结束后销毁)。
int a = 10; int b = 20; // 以下都是右值 100; // 字面量(无名字,不能取地址) a + b; // 表达式结果(临时值,无名字) func(); // 函数返回值(非引用类型时,是临时值)
关键特征:
- 右值不能被取地址:
&(a + b)
会编译报错(无法对临时值取地址)。 - 右值是 “消耗品”:使用后就会销毁(除非被保存到左值中)。
右值的细分
C++11 后,右值又分为两种,但日常使用中无需严格区分,知道它们都是右值即可:
- 纯右值(Prvalue):字面量(如
10
)、表达式结果(如a + b
)、非引用返回的函数结果。 - 将亡值(Xvalue):通过
std::move
转换后的左值(本质是 “即将被销毁的左值”,可被移动)。
2、左值引用:绑定左值的 “别名”
左值引用是最常用的引用类型,用 &
表示,只能绑定左值,本质是给左值起一个 “别名”,操作引用等价于操作原对象。
2.1 基本用法
int a = 10; int& ref_a = a; // 正确:左值引用绑定左值(ref_a 是 a 的别名) ref_a = 20; // 等价于 a = 20,a 现在是 20
2.2 左值引用的限制
不能绑定右值
// int& ref = 10; // 错误:左值引用不能绑定右值(10 是右值)
const
左值引用是特例:可以绑定右值(临时延长右值的生命周期):
const int& ref = 10; // 正确:const 左值引用可绑定右值 // 原理:编译器会生成临时变量存储 10,ref 绑定这个临时变量(临时变量生命周期被延长至 ref 相同)
2.3 左值引用的用途
避免函数传参时的拷贝(如传递大对象 vector
):
void func(const vector<int>& v) { ... } // 传引用,无拷贝
允许函数修改外部变量(非 const
引用):
void increment(int& x) { x++; } int a = 5; increment(a); // a 变为 6
3、右值引用:绑定右值的 “专属引用”
右值引用是 C++11 新增的引用类型,用 &&
表示,专门绑定右值,目的是 “利用右值的临时特性” 实现移动语义(避免不必要的拷贝)。
3.1 基本用法
int&& ref1 = 10; // 正确:右值引用绑定纯右值(10 是右值) int a = 10, b = 20; int&& ref2 = a + b; // 正确:右值引用绑定表达式结果(右值)
3.2 右值引用的限制
不能直接绑定左值:
int a = 10; // int&& ref = a; // 错误:右值引用不能直接绑定左值
但可以通过 std::move
将左值 “强制转换” 为右值引用(本质是告诉编译器:“这个左值可以被当作右值处理,资源可以被转移”)
int a = 10; int&& ref = std::move(a); // 正确:std::move 将 a 转为右值引用
注意:std::move
不会移动任何数据,只是 “标记” 左值为 “可被移动” 的右值,本身是编译期操作,无运行时开销。
3.3 右值引用的核心用途:移动语义
右值引用的最大价值是实现移动语义—— 对于临时对象(右值),不再进行昂贵的拷贝,而是直接 “窃取” 其资源(如内存),大幅提升性能。
移动构造函数
class MyString { private: char* data; // 存储字符串的动态内存 public: // 普通构造函数 MyString(const char* str) { size_t len = strlen(str); data = new char[len + 1]; strcpy(data, str); cout << "构造函数:分配内存" << endl; } // 拷贝构造函数(左值引用参数,深拷贝) MyString(const MyString& other) { size_t len = strlen(other.data); data = new char[len + 1]; strcpy(data, other.data); cout << "拷贝构造:深拷贝(性能差)" << endl; } // 移动构造函数(右值引用参数,直接窃取资源) MyString(MyString&& other) noexcept { data = other.data; // 直接接管 other 的内存 other.data = nullptr; // other 放弃资源(避免析构时重复释放) cout << "移动构造:窃取资源(性能好)" << endl; } ~MyString() { if (data) delete[] data; cout << "析构函数:释放内存" << endl; } }; int main() { MyString s1("hello"); // 调用普通构造 MyString s2 = s1; // 调用拷贝构造(s1 是左值,必须深拷贝) MyString s3 = MyString("world"); // 调用移动构造(临时对象是右值,直接窃取资源) MyString s4 = std::move(s1); // 调用移动构造(s1 被转为右值,资源被 s4 窃取) return 0; }
输出:
构造函数:分配内存
拷贝构造:深拷贝(性能差)
构造函数:分配内存
移动构造:窃取资源(性能好)
移动构造:窃取资源(性能好)
析构函数:释放内存
析构函数:释放内存
析构函数:释放内存
关键:移动构造函数通过右值引用参数,识别出临时对象(或被 std::move
标记的左值),直接接管其资源,避免了拷贝开销(对于大对象,性能提升显著)。
4、万能引用与完美转发
在模板中,还有一种特殊的 “万能引用”,它能同时接受左值和右值,并通过 “完美转发” 保持原表达式的属性(左值还是右值)。
4.1 万能引用:能接受左值和右值的引用
万能引用仅出现在模板参数中,形式为 T&&
,其类型会根据传入的参数自动推导:
- 若传入左值,
T&&
会被推导为 “左值引用”(T&
); - 若传入右值,
T&&
会被推导为 “右值引用”(T&&
)。
template <typename T> void func(T&& t) { // 万能引用(仅模板中 T&& 才是万能引用) // 根据传入的参数,t 可能是左值引用或右值引用 } int main() { int a = 10; func(a); // 传入左值,T 推导为 int&,func 实际为 void func(int& t) func(20); // 传入右值,T 推导为 int,func 实际为 void func(int&& t) return 0; }
注意:万能引用≠右值引用。只有模板中 T&&
且 T
是模板参数时,才是万能引用;其他场景的 &&
都是右值引用(如 void func(int&& t)
是右值引用)。
4.2 完美转发:保持原参数的左值 / 右值属性
完美转发是指在函数模板中,将参数通过万能引用接收后,原封不动地转发给其他函数(保持其左值 / 右值属性)。需要配合 std::forward
实现。
为什么需要完美转发?如果直接传递万能引用参数,会丢失原属性(因为引用本身是左值):
void target(int& x) { cout << "左值版本" << endl; } void target(int&& x) { cout << "右值版本" << endl; } template <typename T> void func(T&& t) { target(t); // 错误:t 是引用(有名字),被当作左值处理,始终调用 target(int&) } int main() { int a = 10; func(a); // 传入左值,期望调用 target(int&) → 实际正确 func(20); // 传入右值,期望调用 target(int&&) → 实际错误(调用了左值版本) return 0; }
用 std::forward
实现完美转发:
template <typename T> void func(T&& t) { target(std::forward<T>(t)); // 完美转发:保持 t 的原始属性 } int main() { int a = 10; func(a); // 传入左值 → 转发为左值 → 调用 target(int&) func(20); // 传入右值 → 转发为右值 → 调用 target(int&&) return 0; }
原理:std::forward<T>(t)
会根据 T
的类型(左值引用或右值引用),将 t
还原为原始的左值或右值属性。
5、常见问题
- 左值和右值有什么区别?
- 左值:有标识符,可以取地址,生命周期较长,可以出现在赋值左边
- 右值:匿名临时对象,不能取地址,生命周期短,只能出现在赋值右边
- 左值引用和右值引用有什么区别?
- 左值引用 (
T&
):只能绑定到左值,用于创建别名 - 右值引用 (
T&&
):只能绑定到右值,用于实现移动语义和完美转发
- 左值引用 (
std::move
做了什么?它真的移动数据吗?std::move
只是进行类型转换,将左值转换为右值引用- 它本身不移动任何数据,只是标记对象为"可移动的"
- 实际的移动操作在移动构造函数或移动赋值运算符中完成
- 使用
std::move
后,原始对象会怎样? - 对象处于"有效但未指定状态"
- 不应该再使用该对象,除非重新赋值
- 析构函数仍然需要正常工作
- 移动语义有什么优势?
- 性能提升:避免不必要的深拷贝,特别是对于管理资源的类
- 资源转移:允许高效地转移资源所有权
- 支持不可拷贝对象:可以移动但不能拷贝的对象
std::forward
和std::move
有什么区别?std::move
:无条件将左值转为右值引用std::forward
:有条件地转换,保持参数的原始值类别
- 如何实现一个支持移动语义的字符串类?
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; data = other.data; size = other.size; other.data = nullptr; other.size = 0; } return *this; } // 析构函数 ~MyString() { delete[] data; } // 禁用拷贝(可选) MyString(const MyString&) = delete; MyString& operator=(const MyString&) = delete; };
什么情况下应该使用完美转发?
应该在包装函数、工厂函数、构造函数转发等场景中使用完美转发,以保持参数的原始值类别。
// 包装器函数 template<typename Func, typename... Args> auto wrapper(Func&& func, Args&&... args) { return std::forward<Func>(func)(std::forward<Args>(args)...); } // 工厂函数 template<typename T, typename... Args> T create(Args&&... args) { return T(std::forward<Args>(args)...); }
右值引用本身是左值还是右值?
右值引用本身是左值。因为右值引用有名字、可以取地址(符合左值的特征)。
int&& ref = 10; // ref 是右值引用,但自身是左值 int& ref2 = ref; // 正确:ref 是左值,可绑定到左值引用
这也是为什么在转发右值引用时,需要用 std::forward
才能还原其右值属性。
const
左值引用为什么能绑定右值?
为了灵活性。const
左值引用设计的初衷之一是 “安全地引用临时对象”,编译器会为右值创建一个临时变量,const
左值引用绑定这个临时变量,并延长其生命周期(与引用同生命周期)。
用途:允许函数接受右值作为参数(如 void print(const string& s)
可接收字符串字面量 print("hello")
),同时保证不修改原对象(const
约束)。
什么时候该用移动语义?
函数返回局部对象时
传递临时对象给函数时
容器重新分配内存时
资源管理类(如智能指针)传递所有权时
什么是返回值优化(RVO)?与移动语义的关系?
返回值优化(RVO)是编译器优化技术,允许直接在调用者内存中构造返回对象,避免拷贝。与移动语义的关系:
RVO优先级高于移动语义
当RVO不可用时,编译器使用移动语义
C++17强制要求部分场景的RVO(称为"guaranteed copy elision")
6、总结
左值/右值 ├── 左值 (lvalue):有名字、可寻址 ├── 右值 (rvalue):临时对象、字面量 │ ├── 纯右值 (prvalue) │ └── 将亡值 (xvalue) │ 引用类型 ├── 左值引用 (&):绑定左值 ├── 右值引用 (&&):绑定右值 └── 通用引用 (T&&):模板推导 └── 完美转发 (std::forward)
到此这篇关于C++ 左值、右值、左值引用、右值引用的文章就介绍到这了,更多相关C++ 左值、右值、左值引用、右值引用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!