C++11 引入的右值引用和移动构造,很多初学者一听就头大:
“左值右值是啥?”
“为什么要有移动语义?”
“拷贝构造我懂了,移动构造又是个啥?”
这篇文章不讲术语堆砌,我们用一个搬家的故事,把这几个概念一次性讲清楚。
假设你有一台很重的钢琴。
你要把钢琴“给”朋友 A,但你还想继续用这台钢琴。
那怎么办?只能再买一台一模一样的,送给 A。
这就是拷贝。
你现在要搬家,钢琴也要搬到新家。
这时候你不会再买一台新钢琴——太蠢了。
你会直接叫搬家公司,把钢琴从旧家搬到新家。
这就是移动。
关键区别一句话总结: 拷贝 = 复制资源;移动 = 偷走资源。
在 C++ 里,“能不能长期用”“有没有名字”,决定了它是左值还是右值。
int x = 10;x 有名字
&x 合法
x
👉 左值 = 还在“家里”的东西
int foo() {
return 10;
}
int y = foo();这里 foo() 返回的临时值:
👉 右值 = 马上要被扔掉的“临时快递盒”
如果一个对象“活不长”,它就是右值。
&& 是在干什么? 普通引用(T&)只能绑定左值。
右值引用(T&&)专门用来绑定右值。
int&& r = 10; // OK
int&& r2 = foo(); // OK为什么需要它?
因为我们要告诉编译器一句话:
“这个对象马上就要死了,你可以放心地把它里面的资源偷走。”
我们用一个简化版的 String 类来演示。
class String {
public:
char* data;
// 拷贝构造
String(const String& other) {
std::cout << "拷贝构造:重新分配内存\n";
data = new char[std::strlen(other.data) + 1];
std::strcpy(data, other.data);
}
};语义是:
“别人还要用他的数据,我必须自己复制一份。”
代价:
new
strcpy
class String {
public:
char* data;
// 移动构造
String(String&& other) noexcept {
std::cout << "移动构造:直接接管资源\n";
data = other.data; // 偷指针
other.data = nullptr; // 防止析构时释放
}
~String() {
delete[] data;
}
};语义是:
“反正你马上要走了,你的内存我直接拿来用。”
代价:
⚠️ 注意这一行:
other.data = nullptr;这是关键:
原来的对象不能再释放这块内存,否则就 double free 了。
String createString() {
String s("hello");
return s; // 这里会触发移动构造(或 RVO)
}
String str = createString();s 是局部变量,函数结束就销毁了。
编译器会说:
“反正
s马上没了,直接把它的内存挪给str。”
✅ 移动构造登场。
std::move 强制搬家 String a("world");
String b(std::move(a));此时:
a 的资源被“偷走”
b 接管资源
a.data == nullptr
⚠️ 重要提醒:
// a 不再拥有有效字符串
std::cout << a.data; // ❌ 未定义行为std::move 并不“移动”,它只是把一个左值变成右值,告诉编译器:
“我现在允许你把它当成右值处理。”
真正的移动动作,发生在构造函数里。
操作 | 是否复制数据 | 原对象状态 | 性能 |
|---|---|---|---|
拷贝构造 | ✅ 是 | 不变 | 慢 |
移动构造 | ❌ 否 | 置空 / 可析构 | 极快 |
std::move | ❌ 否 | 变为“可移动”状态 | 无开销 |
想象你在做这些事:
std::vector 扩容
如果没有移动语义:
每次都要深拷贝一大块内存 😱
有了移动语义:
只拷贝几个指针,O(1) 🚀
这也是为什么现代 C++ 中:
std::vector
std::string
std::unique_ptr
都依赖移动语义来实现高性能。
概念 | 生活比喻 |
|---|---|
左值 | 你家里的钢琴 |
右值 | 搬家时要扔掉的旧家具 |
拷贝构造 | 再买一台钢琴 |
移动构造 | 把钢琴搬去新家 |
std::move | 贴个标签:“此物可搬走” |
右值引用 && | 搬家公司专用通道 |
std::move 不移动,只是“放行”
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。