跳到主要内容
C++ 左值引用与右值引用详解 | 极客日志
C++ 算法
C++ 左值引用与右值引用详解 综述由AI生成 详细讲解了 C++11 中左值与右值的概念区别,重点阐述了右值引用的定义、语法规则及核心应用场景。内容包括移动语义的实现原理,如何通过移动构造函数和移动赋值运算符避免深拷贝以提升性能;以及完美转发技术,利用万能引用和 std::forward 在模板函数中保留参数属性。文章还总结了高频踩坑点,如 std::move 后对象状态、返回值优化干扰等,并给出了实践建议,帮助开发者掌握现代 C++ 资源管理的最佳实践。
宁静 发布于 2026/3/27 更新于 2026/6/10 33 浏览一、右值引用的意义
在 C++ 的编程世界中,对象的拷贝与资源管理一直是性能优化的核心痛点。在 C++11 标准之前,我们编写的代码中隐藏着大量临时对象的深拷贝开销:比如函数返回一个非引用类型的对象、容器插入临时对象时,都会触发拷贝构造函数,完成堆内存的重新分配与数据复制,而临时对象在表达式结束后就会被销毁,这就造成了完全无意义的内存操作与性能浪费。
举个最简单的例子:std::string str = "hello" + " world";,表达式会生成一个临时的 string 对象,再通过拷贝构造把数据复制给 str,随后临时对象被销毁。如果字符串很长,这个深拷贝的开销是完全可以避免的。
而 C++11 引入的右值引用 ,正是为了解决这个问题。它不仅重新定义了 C++ 的资源管理哲学,让我们可以对即将销毁的临时对象的资源进行转移而非拷贝,还支撑了泛型编程中的完美转发,成为现代 C++ 不可或缺的核心特性。
二、基础:理解左值与右值
1. 左值(Lvalue,Locator Value)
左值的核心定义是:可以通过 & 取地址运算符获取其内存地址、拥有持久生命周期的表达式 。
简单来说,左值就是一个'有名字、有固定内存地址'的对象,你可以找到它的存储位置,除了 const 修饰的左值外,都可以修改其内容。左值既可以放在赋值号的左边,也可以放在赋值号的右边(作为右值使用)。
常见的左值场景:
普通变量
const 修饰的变量(const 左值不可修改,但依然可以取地址,属于左值)
对指针解引用的结果 *ptr
数组的元素 arr[index]
结构体 / 类的非静态成员变量
返回左值引用的函数调用
前缀自增 / 自减表达式 ++i、--i
int main () {
int i = 0 ;
i = 1 ;
const int ci = 5 ;
int * pi = &i;
*pi = 1 ;
int arr[5 ] = {0 };
arr[0 ] = 1 ;
struct { int m_a; } st, *pst = &st;
st.m_a = 1 ;
pst->m_a = 2 ;
;
}
return
0
2. 右值(Rvalue,Read Value) C++11 之后,右值被细分为纯右值(prvalue,Pure Rvalue) 和将亡值(xvalue,eXpiring Value) 两类,二者共同构成了右值。
右值的核心定义是:无法通过 & 取地址、生命周期短暂的临时表达式结果 ,它代表的是一个值,而非值所在的存储位置。右值只能放在赋值号的右侧,不能放在左侧。
2.1 纯右值(prvalue) 纯右值是 C++98 标准中传统意义上的右值,核心是'字面量、临时计算结果',没有持久的存储地址。
常见的纯右值场景:
除字符串字面量外的字面常量,如 42、'a'、true(字符串字面量是 const char 数组类型,属于左值)
算术、逻辑、关系运算符的求值结果,如 1+2、a>10、a&&b
非引用类型返回值的函数调用结果
后置自增 / 自减表达式 i++、i--
lambda 表达式
2.2 将亡值(xvalue) 将亡值是 C++11 为了配合右值引用新增的概念,核心是'资源即将被转移、生命周期即将结束的对象 ',它是连接左值和右值的桥梁。
将亡值虽然可以通过特殊方式获取地址,但它的核心使命是完成资源转移,使用后就会被销毁,因此归属于右值范畴。
常见的将亡值场景:
std::move() 的返回结果
返回右值引用类型的函数调用结果
类型转换为右值引用的表达式结果
int getNum () { return 100 ; }
int main () {
int i = 0 , c = 0 ;
i = 42 ;
c = 'a' ;
i = 1 + 2 ;
i = (c != 'a' );
i = getNum ();
int && rri = std::move (i);
return 0 ;
}
3. 左值与右值的核心区别 核心特性 左值 右值(纯右值 + 将亡值) 内存地址 可通过 & 获取稳定的内存地址 无法直接通过 & 获取地址 生命周期 持久,随变量 / 对象的作用域存在 临时,表达式结束后即销毁(除非被引用绑定) 可修改性 非 const 左值可修改,const 左值不可修改 不可直接修改 赋值位置 可放在赋值号左侧、右侧 只能放在赋值号右侧 引用绑定 可被左值引用、const 左值引用绑定 只能被 const 左值引用、右值引用绑定
三、右值引用的定义与语法规则
1. 左值引用的回顾与局限 在 C++11 之前,我们使用的引用都是左值引用 ,用 & 符号声明,本质是给变量起一个'别名',底层通过指针实现,必须在定义时初始化,且初始化后无法重新绑定到其他对象。
左值引用的核心局限:非 const 左值引用只能绑定到左值,无法绑定到右值 。只有 const 左值引用是个例外,它可以同时绑定左值和右值,但 const 修饰决定了我们无法通过它修改绑定的对象。
int main () {
int a = 10 ;
int & ra = a;
const int & rca = a;
const int & rcb = 10 ;
return 0 ;
}
无法对临时的右值对象进行修改和资源复用,只能通过拷贝
泛型编程中,无法区分传入的参数是左值还是右值,无法实现精准的参数转发
2. 右值引用的语法与本质 右值引用是 C++11 新增的引用类型,用 && 符号声明,核心是专门绑定到右值(临时对象、将亡值)的引用 。
基础语法:
必须在定义时初始化,且初始化后无法重新绑定到其他对象
只能直接绑定到右值,无法直接绑定到左值
绑定到右值后,会延长该临时右值的生命周期 ,生命周期与右值引用变量一致
可以通过右值引用修改绑定的右值对象
右值引用变量本身是左值 (有名字、可通过 & 取地址),这是初学者最容易踩的坑
#include <iostream>
using namespace std;
int getNum () { return 100 ; }
void func (int && rri) { rri = 0 ; }
int main () {
int && ri0 = 42 ;
int && ri1 = 1 + 2 ;
int && ri2 = getNum ();
ri0 = 100 ;
cout << "ri0 = " << ri0 << endl;
int * pi = &ri0;
*pi = 200 ;
cout << "*pi = " << *pi << ", ri0 = " << ri0 << endl;
func (1 + 2 );
return 0 ;
}
3. std::move () 做了什么? std::move() 实现了资源的移动?这是错误的。
std::move() 的本质:不做任何资源移动、不生成任何机器码,只是无条件地将一个左值强制转换为右值引用类型(将亡值),仅此而已 。
它的唯一作用,就是让左值可以被右值引用绑定,从而为后续的资源转移提供可能。真正的资源转移,是在类的移动构造函数、移动赋值运算符中完成的,std::move() 只是打开了'资源转移'的入口。
#include <iostream>
#include <string>
using namespace std;
int main () {
string str = "Hello World!" ;
cout << "move 前:str = " << str << endl;
string tmp = std::move (str);
cout << "move 后:str = " << str << endl;
cout << "move 后:tmp = " << tmp << endl;
return 0 ;
}
std::move() 之后的原对象,其资源已经被转移,处于'有效但未定义'的状态,只能对其进行析构或重新赋值,绝对不能再访问其内部资源,否则会出现未定义行为。
4. 左值引用 vs 右值引用 特性 左值引用(Type&) 右值引用(Type&&) 声明符号 & && 初始化要求 必须初始化,绑定后不可重新绑定 必须初始化,绑定后不可重新绑定 直接绑定对象 非 const 左值引用:仅左值 const 左值引用:左值 + 右值 仅右值(纯右值 + 将亡值),左值需通过 std::move 转换后绑定 绑定对象的修改性 非 const 左值引用:可修改 const 左值引用:不可修改 可修改绑定的右值对象(const 右值引用除外) 本身属性 左值 有名字的右值引用变量是左值,匿名右值引用是右值 核心应用场景 函数参数传递、避免拷贝、返回左值对象 移动语义、完美转发、资源所有权转移 引用折叠 与任何引用折叠均为左值引用 仅与右值引用折叠为右值引用,其余均为左值引用 能否为 nullptr 不能,必须绑定合法对象 不能,必须绑定合法对象
四、右值引用的两大核心应用场景
1. 移动语义:告别无意义的深拷贝,实现资源高效转移 移动语义是右值引用最核心的价值,它的核心思想是:对于即将销毁的对象,不做资源的深拷贝,而是直接将其资源的所有权'转移'到新对象中 ,避免了内存分配、数据复制和内存释放的巨大开销。
移动语义的实现,依赖于移动构造函数 和移动赋值运算符 ,二者的参数都是右值引用类型。
1.1 无移动语义的痛点:深拷贝的性能浪费 先实现一个简单的字符串类,看看没有移动语义时,临时对象带来的性能开销:
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
public :
char * data;
size_t len;
MyString (const char * str) {
len = strlen (str);
data = new char [len + 1 ];
strcpy (data, str);
cout << "构造函数:分配内存,地址=" << (void *)data << ",内容=" << data << endl;
}
MyString (const MyString& other) {
len = other.len;
data = new char [len + 1 ];
strcpy (data, other.data);
cout << "拷贝构造函数:深拷贝内存,新地址=" << (void *)data << ",内容=" << data << endl;
}
MyString& operator =(const MyString& other) {
if (this == &other) { return *this ; }
delete [] data;
len = other.len;
data = new char [len + 1 ];
strcpy (data, other.data);
cout << "拷贝赋值运算符:深拷贝内存,新地址=" << (void *)data << ",内容=" << data << endl;
return *this ;
}
~MyString () {
if (data) {
cout << "析构函数:释放内存,地址=" << (void *)data << ",内容=" << data << endl;
delete [] data;
} else {
cout << "析构函数:资源已转移,无需释放" << endl;
}
}
};
MyString createString () {
MyString str ("Hello C++" ) ;
return str;
}
int main () {
MyString str1 ("Hello World" ) ;
MyString str2 = str1;
MyString str3 = createString ();
return 0 ;
}
此处,每一次对象拷贝都会触发深拷贝,重新分配内存、复制数据,临时对象创建后很快被销毁,造成了巨大的性能浪费。
1.2 基于右值引用实现移动语义 我们给 MyString 类添加移动构造函数 和移动赋值运算符 ,通过右值引用实现资源转移:
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
public :
char * data;
size_t len;
MyString (const char * str) {
len = strlen (str);
data = new char [len + 1 ];
strcpy (data, str);
cout << "构造函数:分配内存,地址=" << (void *)data << ",内容=" << data << endl;
}
MyString (const MyString& other) {
len = other.len;
data = new char [len + 1 ];
strcpy (data, other.data);
cout << "拷贝构造函数:深拷贝内存,新地址=" << (void *)data << ",内容=" << data << endl;
}
MyString (MyString&& other) noexcept {
data = other.data;
len = other.len;
other.data = nullptr ;
other.len = 0 ;
cout << "移动构造函数:转移资源,目标地址=" << (void *)data << endl;
}
MyString& operator =(const MyString& other) {
if (this == &other) { return *this ; }
delete [] data;
len = other.len;
data = new char [len + 1 ];
strcpy (data, other.data);
cout << "拷贝赋值运算符:深拷贝内存,新地址=" << (void *)data << ",内容=" << data << endl;
return *this ;
}
MyString& operator =(MyString&& other) noexcept {
if (this == &other) { return *this ; }
delete [] data;
data = other.data;
len = other.len;
other.data = nullptr ;
other.len = 0 ;
cout << "移动赋值运算符:转移资源,目标地址=" << (void *)data << endl;
return *this ;
}
~MyString () {
if (data) {
cout << "析构函数:释放内存,地址=" << (void *)data << ",内容=" << data << endl;
delete [] data;
} else {
cout << "析构函数:资源已转移,无需释放" << endl;
}
}
};
MyString createString () {
MyString str ("Hello C++" ) ;
return str;
}
int main () {
MyString str1 ("Hello World" ) ;
MyString str2 = std::move (str1);
MyString str3 = createString ();
str3 = MyString ("Hello Move" );
return 0 ;
}
此处,移动构造 / 移动赋值没有做任何深拷贝,只是转移了指针的指向,没有新的内存分配,性能得到了提升。
移动构造 / 移动赋值的参数必须是非 const 右值引用 ,因为我们需要修改源对象的指针,将其置空
强烈建议给移动构造 / 移动赋值添加 noexcept 关键字,STL 容器(如 vector)在扩容时,只有移动构造被声明为 noexcept,才会优先调用移动构造,否则会保守地调用拷贝构造函数
当类没有自定义拷贝构造、拷贝赋值、析构函数时,编译器会自动生成默认的移动构造和移动赋值函数;如果自定义了上述函数,编译器不会自动生成,需要手动实现
1.3 移动语义的常见场景
STL 容器的优化 :C++11 之后,所有 STL 容器都实现了移动构造和移动赋值,使用 std::move() 转移容器可以避免全量拷贝,比如 vector<int> vec2 = std::move(vec1); 时间复杂度从 O(n) 降到 O(1)
emplace 系列函数 :vector::emplace_back()、map::emplace() 等函数,通过完美转发直接在容器内存中构造对象,避免了临时对象的拷贝和移动,性能优于 push_back()/insert()
智能指针的资源转移 :std::unique_ptr 无法拷贝,只能通过移动语义转移所有权,正是基于右值引用实现
函数返回大对象 :返回大容器、大对象时,编译器会优先进行 RVO/NRVO 返回值优化,优化失效时会自动调用移动构造,无需手动加 std::move()
2. 完美转发 完美转发是右值引用的第二大核心应用,它的核心目标是:在模板函数中,将参数原封不动地转发给另一个函数,保留参数的左值 / 右值属性、const/volatile 修饰符 。
简单来说,传入的是左值,转发后还是左值;传入的是右值,转发后还是右值,不会因为转发而改变参数的类型属性,这就是'完美'的含义。
2.1 完美转发的底层支撑 完美转发的实现,依赖三个 C++11 的核心特性:
右值引用
万能引用(转发引用)
引用折叠规则
std::forward()
2.1.1 万能引用(转发引用) 万能引用是指模板函数中,形如 T&& 的参数,它既可以绑定左值,也可以绑定右值,同时保留参数的所有类型属性。
注意 :只有发生模板参数推导时,T&& 才是万能引用;如果是具体类型的 Type&&,只是普通的右值引用,只能绑定右值。
template <typename T> void func (T&& arg) { }
void func2 (int && arg) { }
int main () {
int a = 10 ;
func (a);
func (10 );
func2 (10 );
return 0 ;
}
2.1.2 引用折叠规则 C++ 不允许引用的引用,但在模板参数推导时,会出现引用嵌套的情况,此时编译器会执行引用折叠 ,规则如下:
左值引用 & 与 任何引用(&/&&)折叠,最终结果都是左值引用 &
右值引用 && 与 右值引用 && 折叠,最终结果才是右值引用 &&
嵌套类型 折叠结果 T& & T& T& && T& T&& & T& T&& && T&&
当传入左值 int& 时,T 被推导为 int&,T&& = int& &&,折叠为 int&,左值引用
当传入右值 int&& 时,T 被推导为 int,T&& = int&&,右值引用
2.1.3 std::forward () 的作用 std::forward() 是实现完美转发的核心函数,它的作用是有条件地进行类型转换 :
当传入的是左值时,不做任何转换,依然返回左值
当传入的是右值时,转换为右值引用,返回右值
和 std::move() 的无条件转右值不同,std::forward() 是'按需转换',完美保留参数的原始属性。
2.2 完美转发的示例 我们通过一个完整的例子,看看完美转发的效果,以及不使用完美转发的问题:
#include <iostream>
#include <utility>
using namespace std;
void process (int & x) { cout << "左值引用版本被调用,值=" << x << endl; }
void process (int && x) { cout << "右值引用版本被调用,值=" << x << endl; }
template <typename T> void noForward (T&& arg) {
cout << "无完美转发:" ;
process (arg);
}
template <typename T> void withForward (T&& arg) {
cout << "有完美转发:" ;
process (std::forward<T>(arg));
}
int main () {
int a = 5 ;
cout << "===== 传入左值 a =====" << endl;
noForward (a);
withForward (a);
cout << "\n===== 传入右值 10 =====" << endl;
noForward (10 );
withForward (10 );
return 0 ;
}
===== 传入左值 a =====
无完美转发:左值引用版本被调用,值=5
有完美转发:左值引用版本被调用,值=5
===== 传入右值 10 =====
无完美转发:左值引用版本被调用,值=10
有完美转发:右值引用版本被调用,值=10
可以看到,不使用 std::forward() 时,无论传入左值还是右值,都会调用左值版本的 process 函数,因为函数内部的 arg 是有名字的变量,本身是左值。而使用完美转发后,参数的左值 / 右值属性被完美保留,正确调用了对应的重载函数。
2.3 完美转发的常见场景
STL 容器的 emplace 系列函数 :直接在容器内存中构造对象,将用户传入的参数完美转发给对象的构造函数,保留所有类型属性
std::bind/std::function :实现函数对象的封装和参数转发,底层依赖完美转发
线程库 std::thread :将线程函数的参数完美转发到线程执行上下文
泛型编程中的中间转发函数 :需要将参数透传给其他函数的模板场景
五、高频踩坑点与实践建议
1. 高频踩坑点
坑 1:std::move () 之后继续使用原对象 std::move() 之后,原对象的资源已经被转移,处于有效但未定义的状态,此时访问其内部资源(如 string 的字符、vector 的元素)会触发未定义行为,只能对其进行析构或重新赋值。
坑 2:对函数返回值使用 std::move () 很多人会写 return std::move(obj);,这是典型的错误写法。编译器默认会对返回的局部对象执行 RVO/NRVO 返回值优化,直接在返回值地址构造对象,零拷贝。而加了 std::move() 之后,会阻止编译器的优化,强制调用移动构造,反而降低性能。
坑 3:误以为右值引用变量本身是右值 有名字的右值引用变量,本身是左值,无法直接绑定到右值引用参数。如果需要在函数中转发右值引用形参,必须使用 std::move() 或 std::forward()。
void func (int && x) { }
void test (int && arg) {
func (std::move (arg));
}
坑 4:给移动构造 / 移动赋值加 const 修饰 移动构造 / 移动赋值需要修改源对象的指针,将其置空,加了 const 之后无法修改源对象,只能做深拷贝,完全失去了移动语义的意义。const 右值引用几乎没有使用场景,不如直接用 const 左值引用。
坑 5:把普通右值引用当成万能引用 只有在模板参数推导场景下的 T&& 才是万能引用,类成员函数的模板参数如果没有推导过程,也不是万能引用。
class Test {
public :
template <typename T> void func (T&& arg) ;
};
2. 实践建议
只在需要转移资源的时候使用 std::move(),且确保 move 之后的对象不再使用
实现移动构造 / 移动赋值时,必须添加 noexcept 关键字,确保 STL 容器能优先调用
函数返回局部对象时,不要加 std::move(),交给编译器做返回值优化
泛型编程中,需要转发参数时,使用 std::forward() 实现完美转发,不要用 std::move()
遵循'三五法则':如果自定义了析构函数、拷贝构造函数、拷贝赋值运算符中的任意一个,就应该同时实现或禁用移动构造、移动赋值运算符
对于不需要拷贝、只需要转移所有权的对象(如 unique_ptr、文件句柄),禁用拷贝构造和拷贝赋值,只实现移动构造和移动赋值
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online