很多人学 C++ 时都会卡在这几个点:
- 左值引用和右值引用到底区别是什么?
- 为什么拷贝构造用
const T&? - 为什么移动构造用
T&&? - 右值引用是不是马上销毁?
- 为什么移动后要置 nullptr?
这篇文章用一个最简单的 Student 类,把这些问题一次讲透。
一、一个有资源的类
我们写一个最小可运行示例:
cpp
#include <iostream>
using namespace std;
class Student {
public:
int* age;
Student(int a) : age(new int(a)) {
cout << "Ctor this=" << this
<< " age_ptr=" << age
<< " val=" << *age << endl;
}
// 拷贝构造(深拷贝)
Student(const Student& other)
: age(new int(*other.age)) {
cout << "CopyCtor this=" << this
<< " age_ptr=" << age
<< " val=" << *age << endl;
}
// 移动构造
Student(Student&& other) noexcept
: age(other.age) {
other.age = nullptr;
cout << "MoveCtor this=" << this
<< " age_ptr=" << age << endl;
}
~Student() {
cout << "Dtor this=" << this
<< " age_ptr=" << age << endl;
delete age;
}
};
这里有一个关键点:
age是堆内存资源。
只要类里有资源,拷贝和移动问题就一定会出现。
二、为什么拷贝构造用 const T&?
cpp
Student(const Student& other)
这是 左值引用。
它能绑定:
cpp
Student a(20);
Student b = a; // 走拷贝构造
因为:
a是左值(有名字,可寻址)- 左值只能绑定
T&
为什么加 const?
因为:
左值通常还会继续被使用
你不能掏空它的资源。
所以必须:
cpp
const Student&
防止误修改。
三、深拷贝在做什么?
关键语句:
cpp
age = new int(*other.age);
拆解:
other.age是int**other.age取出值(比如 20)new int(20)开新内存- 把新地址赋给 age
内存图:
cpp
other.age ----> 0x1000 (20)
this->age ----> 0x2000 (20)
两份资源。
这就是深拷贝。
四、为什么移动构造用 T&&?
cpp
Student(Student&& other)
&& 是右值引用。
它能绑定:
cpp
Student b = Student(30); // 临时对象
Student c = std::move(a); // 被标记为右值
右值通常是:
- 临时对象
- 即将被销毁的对象
- 或被允许"掏空"的对象
所以可以:
cpp
age = other.age;
other.age = nullptr;
五、移动构造在做什么?
cpp
Student(Student&& other) noexcept
: age(other.age) {
other.age = nullptr;
}
执行流程:
移动前:
cpp
other.age ----> 0x1000 (20)
移动后:
cpp
this->age ----> 0x1000 (20)
other.age ----> nullptr
没有分配新内存。
没有复制数据。
只是:
转移所有权
六、左值引用 vs 右值引用区别
| 对比项 | 左值引用 T& |
右值引用 T&& |
|---|---|---|
| 绑定对象 | 左值 | 右值 |
| 是否允许修改 | 看是否 const | 允许修改 |
| 是否允许转移资源 | 不允许 | 允许 |
| 典型用途 | 拷贝 | 移动 |
七、重要知识点:右值引用参数在函数内部是左值
这是很多人最容易混乱的地方。
cpp
Student(Student&& other)
虽然 other 类型是右值引用,
但在函数内部:
cpp
other
是 左值。
因为:
只要一个变量有名字,它就是左值。
这就是为什么:
如果你在函数内部再传递 other,
必须写:
cpp
std::move(other)
否则会走拷贝构造。
八、右值引用是不是马上销毁?
不是。
销毁由作用域决定。
例如:
cpp
Student&& r = Student(40);
临时对象生命周期会延长到 r 结束。
所以:
右值引用 ≠ 马上销毁
右值引用 = 允许资源转移
九、拷贝 vs 移动本质区别
| 项目 | 拷贝 | 移动 |
|---|---|---|
| 是否分配内存 | 是 | 否 |
| 是否复制数据 | 是 | 否 |
| 是否转移所有权 | 否 | 是 |
| 性能 | 慢 | 快 |
十、终极理解
cpp
Student b = a; // 拷贝
Student c = std::move(a); // 移动
Student d = Student(50); // 移动
拷贝:
保留原对象完整性
移动:
掏空原对象,把资源转走
总结
- 左值引用用于拷贝
- 右值引用用于移动
- 移动语义是资源所有权转移
- 引用不决定生命周期
- 右值引用参数在函数内部是左值