一、基础:先吃透「左值」和「右值」(值的类别)
C++ 中所有表达式(变量、字面量、函数返回值等)都属于「左值(lvalue)」或「右值(rvalue)」,C++11 后右值又细分为「纯右值(prvalue)」和「将亡值(xvalue)」------ 核心区别是是否有名字、能否取地址、生命周期。
1. 核心定义与示例
| 类别 | 核心特征 | 典型示例 |
|---|---|---|
| 左值 | 有名字、能取地址、生命周期持久(可跨语句使用); 是可被重复访问的对象 / 变量 | ① 普通变量(int a;) ② 函数返回的左值引用(int& func()) ③ 数组 / 对象成员(obj.member) |
| 纯右值 | 无名字、默认不可取地址、生命周期临时(默认当前语句结束后释放,除非被引用延长); 是临时创建的 "值"(无已有资源) | ① 字面量(10、"hello"、true) ② 算术表达式(a+b) ③ 函数返回的非引用值(int func()) ④ 临时对象(T()) |
| 将亡值 | 属于右值,核心是 "对应已有对象的资源可被移动接管"; 无名字(或需通过引用访问)、资源可被转移 | ① std::move(x) 的结果(x是已有左值) ② 返回右值引用的函数 (T&& func() { static T t; return std::move(t); }) |
std::move(a),不会改变a的生命周期.。在调用int &&b = std::move(a)后,当a被释放时,b会变成悬空引用(野引用),后续使用b时,会触发未定义行为(程序崩溃、结果乱码等)。
区分纯右值和将亡值:
| 场景 | 绑定的对象 | 对象生命周期是否被延长 | b 是否会悬空? |
|---|---|---|---|
int&& b = std::move(a) |
原左值 a |
❌ 否(a 生命周期由自身决定) |
是(a 释放后 b 悬空) |
int&& b = 5 |
临时对象 int(5) |
✅ 是(与 b 绑定) |
否(b 销毁时临时对象才释放) |
通俗记:
- 左值:能放在 "
="左边赋值(a = 10;中a是左值);- 右值:只能放在"
="右边(10 = a;非法,10是右值);- 将亡值:属于 "可被移动的右值"(核心价值是触发移动语义)。
对象资源传递的两种方式:
- 拷贝语义:
比如用对象 A 构造对象 B 时,会把 A 的资源(如动态内存、文件句柄)复制一份给 B,A 和 B 各有独立的资源,开销较大。- 移动语义(C++11 新增的):
用对象 A 构造对象 B 时,直接把 A 的资源所有权转移给 B(A 放弃资源,变成 "空壳"),不用复制资源,开销极小。
移动语义与拷贝语义触发的示例:
cpp
#include <iostream>
#include <utility> // std::move
using namespace std;
class String {
public:
char* buf; // 动态内存资源
// 1. 拷贝构造(拷贝语义:复制资源)
String(const String& other) {
cout << "调用【拷贝构造】(复制资源)\n";
buf = new char[100];
memcpy(buf, other.buf, 100); // 复制other的内存
}
// 2. 移动构造(移动语义:转移资源)
String(String&& other) noexcept {
cout << "调用【移动构造】(转移资源)\n";
buf = other.buf; // 直接拿走other的资源
other.buf = nullptr; // other放弃资源(变成空壳)
}
String(const char* str) {
buf = new char[100];
strcpy(buf, str);
}
};
| 代码语句 | 调用的构造函数 | 是否是移动构造? | 核心逻辑 |
|---|---|---|---|
String a("hello"); |
String(const char*) |
❌ 否 | 从零创建对象,分配新资源 |
String b = String("hello"); |
String(String&&) |
✅ 是 | 转移临时对象的已有资源 |
String b = std::move(a); |
String(String&&) |
✅ 是 | 转移已有左值对象的资源 |
2. 关键判断技巧
cpp
#include <iostream>
using namespace std;
int global = 10;
int& left_value_func() { return global; } // 返回左值引用(左值)
int right_value_func() { return 20; } // 返回非引用(纯右值)
int main() {
int a = 5; // a:左值;5:纯右值
// 左值判断:能取地址
cout << &a << endl; // 合法(a是左值)
// cout << &5 << endl; // 非法(5是纯右值,不能取地址)
// cout << &(a+1) << endl;// 非法(a+1是纯右值)
// 左值引用只能绑定左值
int& lref1 = a; // 合法
// int& lref2 = 5; // 非法(5是右值)
const int& lref3 = 5; // 特例:const左值引用可绑定右值(延长临时对象生命周期)
// 右值引用只能绑定右值
int&& rref1 = 5; // 合法(5是纯右值)
int&& rref2 = right_value_func(); // 合法(返回值是纯右值)
// int&& rref3 = a; // 非法(a是左值)
int&& rref4 = std::move(a); // 合法(std::move(a)是将亡值)
return 0;
}
3. 易踩坑点:右值引用本身是左值!
cpp
int&& rref = 10; // rref是右值引用(类型),但rref本身是左值(有名字、能取地址)
// int&& rref2 = rref; // 非法!rref是左值,不能绑定到右值引用
int&& rref2 = std::move(rref); // 合法:将左值rref转为右值
这是理解
std::forward的关键:右值引用变量本身是左值,若想保留其 "右值属性",需用完美转发。
二、左值/右值引用
两者只要不是const都可以直接修改指向对象的值(效果相同):
之所以弄个左值/右值引用是为了编程时更容易区分哪些对象是需要被接管的。需要就使用右值引用、不需要就使用左值引用
| 维度 | 左值引用(T&,非 const) |
右值引用(T&&,非 const) |
|---|---|---|
| 核心定义 | 绑定到「左值」的引用(左值:有名字、可寻址、生命周期长的对象,如变量) | 绑定到「右值」的引用(右值:无名字、临时的对象,如临时变量、std::move 后的左值) |
| 绑定规则(硬限制) | ✅ 仅能绑定普通左值(如 QueryCallback cb;) ❌ 不能绑定临时右值(如 QueryCallback{})、std::move 后的左值 |
✅ 仅能绑定临时右值、std::move 后的左值 ❌ 不能绑定普通左值(需 std::move 转换) |
| 可修改性 | ✅ 可修改绑定对象的值(如 cb.timeout_ = 1000;) |
✅ 可修改绑定对象的值(如移动构造中置空 other.future_) |
| 语义意图 | 「借用」已有对象:用完后原对象仍有效,调用方无需放弃所有权 | 「接管」临时对象:转移资源后原对象(右值)置空 / 失效,调用方需明确放弃所有权 |
| 设计目标 | 1. 函数传参修改原对象(避免拷贝) 2. 为已有对象创建别名 | 1. 实现移动语义(零拷贝转移资源,如 std::unique_ptr/std::future 转移) 2. 支持完美转发、优化临时对象开销 |
| 典型场景 | 1. 函数参数修改原变量:void modify(QueryCallback& cb) 2. 避免大对象拷贝传参 |
1. 移动构造 / 赋值:QueryCallback(QueryCallback&& other) 2. 接收临时对象:AddQueryCallback(QueryCallback&& cb) 3. std::move 配合转移资源 |
| 重载匹配优先级 | 仅匹配左值入参 | 优先匹配右值入参; 不可用时,编译器降级匹配 const T& 拷贝构造 |
补充:(const T&)可以指向右值!!!
如: const int& a = 5;
| 语句 | 临时对象(值 5)的生命周期 | 关键结论 |
|---|---|---|
int a = 5; |
无临时对象(直接拷贝值) | 变量 a 的栈空间存储值 5,离开作用域释放;字面量 5 始终存在 |
int&& a = 5; |
延长至 a 的作用域 | 临时对象不会在语句执行后释放,a 可修改其值,直到 a 离开作用域才释放临时对象 |
const int& a = 5; |
延长至 a 的作用域 | 临时对象不会在语句执行后释放,a 不可修改其值,直到 a 离开作用域才释放临时对象 |
什么时候才会产生 "临时 int 对象"?
只有当编译器需要 "一个可寻址的 int 对象(有内存地址)",但仅能拿到字面量时,才会隐式创建临时对象 ------ 典型场景是「引用绑定字面量」:
引用(
&/&&)必须绑定到可寻址的对象 (有内存地址),但字面量5本身不可寻址,因此编译器会 "兜底" 创建一个临时 int 对象[隐式执行int(5)],再把引用绑定到这个临时对象。
例子:
cpp
#include <iostream>
using namespace std;
int main() {
// 1. 字面量5:不可寻址(取地址编译报错)
// cout << &5 << endl; // 错误:invalid lvalue in unary '&'
// 2. 临时对象:可寻址(显式创建临时对象)
cout << &int(5) << endl; // 合法:int(5)是临时int对象,有地址
// 3. int a; a=5;:仅拷贝字面量值,无临时对象
int a;
a = 5;
cout << &a << endl; // 地址是变量a的栈空间,和字面量5无关
int&& b = 5; // 编译器隐式执行:int&& b = int(5); 执行完后 临时对象int(5)不会被释放。生命周期跟b绑定
b = 4;
cout << b << endl;
const int& c = 5; // 编译器隐式执行:const int& c = int(5); 执行完后 临时对象int(5)不会被释放,生命周期跟c绑定
return 0;
}
引用对对象生命周期的影响:
仅当引用绑定「临时对象(纯右值)」时,才会改变对象生命周期(延长);绑定「普通左值 / 将亡值(std::move 后的左值)」时,不改变原对象生命周期。
三、std::move:左值 → 右值引用的 "强制转换器"
1. 本质(无任何 "移动" 行为!)
std::move(x) 是一个模板函数 ,核心作用是:将任意左值 x 强制转换为 T&&(右值引用类型),标记 x 为 "可被移动"------ 它不移动任何数据,也不改变 x 的值,仅改变编译器对 x 的 "值类别判断"。
注意:std::move(x)之后x还是保持原来的类型
简化的 std::move 实现(便于理解):
cpp
template <typename T>
typename std::remove_reference<T>::type&& move(T&& x) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(x);
// 注意并没有改变x的类型,x还是左值,static_cast只是基于x创建一个右值返回过去。
}
2. 核心用法:触发移动语义
移动语义(移动构造 / 移动赋值)仅对 "右值" 生效,std::move 让左值能触发移动语义,从而转移资源所有权(避免拷贝)
示例:移动 unique_ptr(不可拷贝,仅可移动)
cpp
#include <memory>
#include <iostream>
int main() {
std::unique_ptr<int> p1 = std::make_unique<int>(100);
// std::unique_ptr<int> p2 = p1; // 非法:unique_ptr禁用拷贝构造
std::unique_ptr<int> p2 = std::move(p1); // 合法:std::move(p1)转为右值,触发移动构造
// 移动后p1为空(资源被转移),p2持有资源
std::cout << (p1 ? "p1非空" : "p1为空") << endl; // 输出:p1为空
std::cout << *p2 << endl; // 输出:100
return 0;
}
unique_ptr移动构造函数(参数是右值)的核心逻辑是:
- 把源对象(
p1)持有的资源指针 转移给新对象(p2);- 把源对象(
p1)的内部指针置为nullptr(空指针)
简化的 unique_ptr 移动构造实现(便于理解):
cpp
template <typename T>
class unique_ptr {
private:
T* ptr_ = nullptr; // 内部持有裸指针
public:
// 移动构造函数(参数是右值引用)
unique_ptr(unique_ptr&& other) noexcept {
// 1. 接管源对象的资源指针
this->ptr_ = other.ptr_;
// 2. 把源对象的指针置空(避免重复释放)
other.ptr_ = nullptr;
}
// 析构函数:释放资源
~unique_ptr() {
delete ptr_;
}
// ... 其他成员函数
};
示例:移动 std::promise(传线程时)
cpp
#include <future>
#include <thread>
void producer(std::promise<int> prom) {
prom.set_value(42);
}
int main() {
std::promise<int> prom;
// std::thread t(producer, prom); // 非法:promise不可拷贝
std::thread t(producer, std::move(prom)); // 合法:转为右值,触发移动构造
t.join();
return 0;
}
3. 移动构造函数:
3.1 独占资源类:移动后源对象通常 "置空"(类似 unique_ptr)
对于管理独占性资源 (如动态内存、文件句柄、锁)的类(如 std::unique_ptr、std::promise),移动构造后源对象必须被 "清空"------ 这是为了保证资源唯一所有权,避免重复释放 / 操作资源。
示例:std::unique_ptr/std::future
std::unique_ptr移动后,源对象的内部指针被置为nullptr;std::future移动后,源对象的valid()返回false(不再关联任何异步结果)。
3.2 容器类:移动后源对象通常 "为空"(但标准不强制)
对于 std::vector、std::string 这类容器类,移动构造的目标是高效转移资源 (比如转移内部动态数组的指针),移动后源对象的状态通常是 "空容器",但 C++ 标准没有强制要求必须为空------ 只要求源对象是 "有效但未定义" 的。
以 std::string 为例:
cpp
#include <string>
#include <iostream>
int main() {
std::string s1 = "hello world";
std::string s2 = std::move(s1); // 触发移动构造
// 移动后s2持有原数据,s1的状态是"有效但未定义"
std::cout << "s2: " << s2 << std::endl; // 输出 "hello world"
std::cout << "s1是否为空:" << (s1.empty() ? "是" : "否") << std::endl; // 通常输出"是",但标准不保证
s1 = "new value"; // 可以赋值(合法)
return 0;
}
3.3 自定义类:移动后源对象的状态由你决定
如果你自己写一个类的移动构造函数,完全可以灵活控制源对象的状态 ------ 只要满足 "有效但未定义" 的要求(可析构、可赋值)。
示例 1:移动后源对象置空(模仿 unique_ptr)
cpp
class MyResource {
private:
int* ptr_ = nullptr;
public:
MyResource(int val) : ptr_(new int(val)) {}
// 移动构造:源对象置空
MyResource(MyResource&& other) noexcept {
ptr_ = other.ptr_;
other.ptr_ = nullptr; // 源对象置空
}
~MyResource() { delete ptr_; }
};
示例 2:移动后源对象保留默认值(非置空)
cpp
class MyData {
private:
int id_;
std::string name_;
public:
MyData(int id, std::string name) : id_(id), name_(std::move(name)) {}
// 移动构造:源对象重置为默认值
MyData(MyData&& other) noexcept {
// 转移资源
id_ = other.id_;
name_ = std::move(other.name_);
// 源对象重置为默认值(而非置空)
other.id_ = 0;
other.name_ = "default";
}
};
这里移动后源对象没有 "置空",而是被重置为默认值 ------ 这也是合法的,因为源对象依然是 "有效可操作" 的。
核心结论
- 不是所有移动构造都会把源对象 "置空";
- 移动构造后源对象的状态,由类的资源类型 和设计目标 决定:
- 独占资源类:通常置空(保证唯一所有权);
- 容器类:通常为空(但标准不强制);
- 自定义类:可灵活控制(只要满足 "有效但未定义");
- C++ 标准的唯一要求:移动后的源对象必须是 "有效但未定义" 的(可析构、可赋值,不能依赖旧值)
4. 关键注意事项
std::move不移动数据:真正的 "移动" 是在「移动构造函数 / 移动赋值运算符」中完成的(比如unique_ptr转移指针所有权);- 移动(注意不是std::move()导致的)后原对象状态:变为 "有效但未定义"(可析构、可赋值,但不可读取);
- 不要滥用
std::move:仅对 "不再使用的左值" 使用(否则会破坏原对象)
四、std::forward:完美转发(精准保留值类别)
1. 为什么需要完美转发?
函数模板传参时,默认会丢失 "值类别信息"(右值传进去会变成左值),std::forward 的作用是:精准保留实参的原始值类别(左值 / 右值),即 "传左值则转发为左值,传右值则转发为右值"------ 这就是 "完美转发"。
问题示例(无完美转发,丢失值类别):
cpp
// 移动构造函数(仅接收右值)
void func(std::unique_ptr<int>&& ptr) {
std::cout << "接收右值" << endl;
}
// 拷贝构造函数(仅接收左值)
void func(std::unique_ptr<int>& ptr) {
std::cout << "接收左值" << endl;
}
// 模板函数传参(无完美转发)
template <typename T>
void wrapper(T&& arg) {
func(arg); // arg是右值引用,但本身是左值 → 永远调用左值版本
}
int main() {
std::unique_ptr<int> p = std::make_unique<int>(10);
wrapper(std::move(p)); // 期望调用右值版本,但实际调用左值版本
return 0;
}
2. std::forward 的本质
std::forward 是一个条件转换模板,核心逻辑:
- 若实参是左值 → 转发为左值引用;
- 若实参是右值 → 转发为右值引用。
简化的 std::forward 实现:
cpp
template <typename T>
T&& forward(typename std::remove_reference<T>::type& arg) noexcept {
return static_cast<T&&>(arg);
}
3. 完美转发示例(修复上述问题)
cpp
void func(std::unique_ptr<int>&& ptr) {
std::cout << "接收右值" << endl;
}
void func(std::unique_ptr<int>& ptr) {
std::cout << "接收左值" << endl;
}
// 模板函数(带完美转发)
template <typename T>
void wrapper(T&& arg) {
func(std::forward<T>(arg)); // 精准保留原始值类别
}
int main() {
std::unique_ptr<int> p = std::make_unique<int>(10);
wrapper(p); // 实参是左值 → 转发为左值 → 调用左值版本
wrapper(std::move(p)); // 实参是右值 → 转发为右值 → 调用右值版本
return 0;
}
4. 完美转发的核心场景
- 泛型函数 / 模板(如容器的
emplace_back、线程池任务提交); - 转发构造函数(继承构造、委托构造);
- 需精准传递 "左值 / 右值" 属性的场景(如移动语义的嵌套调用)。
五、核心对比(左值 / 右值、move/forward)
1. 左值引用 vs 右值引用
| 特性 | 左值引用(T&) | 右值引用(T&&) |
|---|---|---|
| 绑定对象 | 只能绑定左值(const 版可绑右值) | 只能绑定右值(纯右值 / 将亡值) |
| 自身值类别 | 左值 | 左值(关键!) |
| 核心作用 | 避免拷贝、修改原对象 | 实现移动语义、延长临时对象生命周期 |
2. std::move vs std::forward
| 特性 | std::move | std::forward |
|---|---|---|
| 本质 | 无条件转换:左值→右值引用 | 条件转换:保留原始值类别(左值→左值,右值→右值) |
| 适用场景 | 明确要转移资源(左值变右值) | 模板传参,精准转发值类别(完美转发) |
| 调用形式 | std::move(x)(无需指定模板参数) |
std::forward<T>(x)(必须指定模板参数) |
| 结果 | 永远返回右值引用 | 实参是左值→返回左值引用;实参是右值→返回右值引用 |
一句话记:
- 想转移资源 → 用
std::move;- 想精准转发值类别 → 用
std::forward;- 区分值类别是所有操作的基础。
六、有效但未定义的含义
"有效但未定义" 是描述对象状态 的核心术语(最常见于移动操作后的源对象),可以拆成 "有效" 和 "未定义" 两个部分理解,核心是:对象处于合法的、可安全操作的状态,但它的具体值 / 细节状态是不确定的,不能依赖。
简单点说:该对象是一个正常对象,但是其内部成员的值是不确定的,可用赋予它新的值,让它进去"确定状态"就可以正常使用了。
cpp
std::string s1 = "hello";
std::string s2 = std::move(s1); // 移动后,s1处于"有效但未定义"状态
// std::cout << s1 << endl; // 最好还是不要访问未定义的值
s1 = "world"; // 将s1 置为 " 确定状态 "
std::cout << s1 << endl; // 正常访问s1