std::move做了什么?它保证会“移动”吗?
左值和右值
通俗来说:
- **左值:**可以取地址、有名字、能长期存在的
- **右值:**取不了地址、没名字、即将消失的临时结果
左值 (lvalue):有固定地址的“信箱”
代表一个在内存中有确定位置的对象或变量
- 有持久的身份:在表达式结束后,它依然存在
- 可以被取地址:你可以对一个左值使用
&运算符 - 可以被赋值:它能出现在赋值符号 (
=) 的左边
常见例子:
- 所有有名字的变量:
int x = 10;(这里的x就是一个左值) - 返回左值引用的函数调用:
std::getline(std::cin, str)中的std::cin
int x; // x 是一个左值
x = 100; // 正确:左值可以被赋值
int* p = &x; // 正确:左值可以被取地址
右值 (rvalue):即将消失的“信件”
右值(rvalue)的 “r” 可以理解为 read(读取)。它通常指一个临时的计算结果,没有固定的内存地址,在一个表达式结束之后就会被销毁
- 是临时的:表达式一结束,它就“烟消云散”
- 不能被取地址:对一个右值使用
&运算符通常是错误的 - 不能被赋值:它不能出现在赋值符号 (
=) 的左边
eg:
- 字面量:
10,true,'A' - 算术表达式的结果:
x + 5,a * b - 按值返回的函数调用:
int get_number() { return 42; },这里的get_number()的返回值就是一个右值
对左值(信箱):你只能拷贝 (copy) 里面的东西,因为原来的主人还需要它
对右值(信件):因为这封信读完就要扔掉,所以你可以直接**“窃取” (move)** 它的内容,而不用花力气去复制
C++11引入了右值引用 (&&),它允许“捕获”这些即将被销毁的右值,并“窃取”它们的内部资源(如动态分配的内存),从而避免了昂贵的深拷贝操作
std::move
std::move的本质:一个“可以被拿走”的信号
一个装满贵重物品的箱子 (Box A),现在想把这些物品放进一个新箱子 (Box B)
- 拷贝 (Copy):把
Box A里的物品一件一件地复制出来,再把复制品放进Box B。这个过程很慢,而且消耗新材料。Box A里的物品原封不动 - 移动 (Move):直接把
Box A里的所有物品整体搬到Box B里。这个过程非常快。Box A虽然还在,但它里面已经空了
std::move就好比在Box A上贴一个标签,上面写着:“可以拿走,我不用了”。它没有搬任何东西,只是发出了一个信号
看到这个信号的“搬家工人”(即移动构造函数或移动赋值运算符)就知道可以安全地“搬空”这个箱子,而不用担心原来的主人还需要它
这个“类型转换”等价于:
static_cast<T&&>(lvalue)
真正的移动是如何发生的?
class ResourceHolder {
public:
std::string name;
std::vector<int> data;
// 默认构造函数
ResourceHolder(const std::string& n) : name(n) {
data.resize(1000); // 模拟一个很大的资源
std::cout << "Constructing " << name << " (regular).\n";
}
// 拷贝构造函数 (深拷贝)
ResourceHolder(const ResourceHolder& other)
: name(other.name + " (copy)"), data(other.data) {
std::cout << "Constructing " << name << " using COPY.\n";
}
// 移动构造函数 (浅拷贝/资源窃取)
ResourceHolder(ResourceHolder&& other) noexcept
: name(std::move(other.name)), data(std::move(other.data)) {
name += " (moved)";
std::cout << "Constructing " << name << " using MOVE.\n";
}
};
int main() {
ResourceHolder holder1("Holder1");
std::cout << "\n--- Trying to copy ---\n";
ResourceHolder holder2 = holder1; // 调用拷贝构造函数
std::cout << "\n--- Trying to move ---\n";
// std::move将holder1转换为右值引用,因此匹配并调用移动构造函数
ResourceHolder holder3 = std::move(holder1);
std::cout << "\nAfter move, holder1's name is: '" << holder1.name << "'\n";
std::cout << "After move, holder1's data size is: " << holder1.data.size() << "\n";
return 0;
}
-------------
/*
Constructing Holder1 (regular).
--- Trying to copy ---
Constructing Holder1 (copy) using COPY.
--- Trying to move ---
Constructing Holder1 (moved) using MOVE.
After move, holder1's name is: ''
After move, holder1's data size is: 0
*/
-
ResourceHolder holder2 = holder1;:holder1是一个左值,因此调用拷贝构造函数,holder2得到了holder1数据的一份完整拷贝 -
ResourceHolder holder3 = std::move(holder1);:std::move(holder1)将holder1转换成右值引用,因此移动构造函数被调用。holder3没有复制数据,而是直接“窃取”了holder1的name和data(std::vector和std::string内部也实现了移动语义) -
移动后,原来的
holder1对象进入了一个有效但未指定的状态。它的资源已经被“搬走”,不能再对其值做任何假设,但它本身仍然是一个可以被安全销毁的对象
总结:std::move的关键点
- 不移动,只转换类型:
std::move是一个信号,告诉编译器可以进行移动优化 - 不保证移动:如果一个类没有定义移动构造函数/移动赋值运算符,那么即使使用了
std::move,编译器仍然会退回到拷贝操作 - 何时使用:确定一个对象(特别是拥有堆内存等昂贵资源的对象)在后续代码中不再需要其当前值时,就应该使用
std::move将其资源转移给新对象,以避免不必要的深拷贝 - 不要对一个之后还想正常使用的对象调用
std::move。一旦其资源被移走,它的状态就不再可靠