移动语义与右值
本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 83 课,共 104 课。
在 C++ 中,每个表达式要么是 左值(具有持久的标识,可以获取其地址),要么是 右值(临时的,即将被销毁)。理解这种区别,就能掌握 移动语义,这是一种强大的优化方式,可以避免不必要的复制。
右值引用使用 && 声明,专门绑定到临时对象。这样,你就可以从即将消失的对象中“窃取”资源:
#include <iostream>
#include <utility>
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : data(new int[s]), size(s) {
std::cout << "Constructed\n";
}
// 移动构造函数:接管资源
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // 让源对象保持有效状态
other.size = 0;
std::cout << "Moved\n";
}
~Buffer() { delete[] data; }
};
int main() {
Buffer b1(1000);
Buffer b2(std::move(b1)); // 触发移动构造函数
}std::move 函数实际上不会移动任何东西,它只是将左值转换为右值引用,表示你愿意放弃对象的资源。真正的移动发生在移动构造函数或移动赋值运算符中。
处理容器或字符串等资源占用较大的对象时,移动语义能显著提升性能。你不必深度复制数兆字节的数据,只需转移指针所有权即可。无论数据大小如何,这都是一个常数时间操作。
挑战
简单让我们来编写一个管理资源的 DataBuffer 类,演示移动语义的实际效果。你将看到,在转移动态分配内存的所有权时,移动资源而不是复制资源能大幅提高效率。
你需要将代码分成三个文件:
DataBuffer.h:定义管理动态分配的整数数组的DataBuffer类。你的类应包含以下私有成员:数据指针(
int*)、大小(size_t)和名称(std::string),用于在操作过程中区分各个缓冲区。声明以下内容:
- 一个构造函数,接收
std::string类型的名称和size_t类型的大小,分配数组,并输出:[name] constructed with size [size] - 一个移动构造函数,接收右值引用并接管资源,然后输出:
[name] moved from [source_name](移动后的缓冲区会使用源缓冲区的名称) - 一个析构函数,输出
[name] destroyed(如果缓冲区已被移走,则输出empty destroyed) - 一个返回当前大小的
getSize()方法 - 一个返回缓冲区名称的
getName()方法
记得将移动构造函数标记为
noexcept,并让源对象保持有效的空状态(nullptr、大小为 0、名称为 "empty")。- 一个构造函数,接收
DataBuffer.cpp:实现头文件中声明的所有方法。析构函数运行时,只有在指针不为 null 时才删除数据。包含用于输出的<iostream>。main.cpp:读取两个输入:- 缓冲区的名称(字符串)
- 缓冲区的大小(整数)
使用给定的名称和大小创建一个
DataBuffer。然后使用std::move()从第一个缓冲区移动资源,创建第二个缓冲区。移动后,输出两个缓冲区的状态:Original: [name] size=[size]New: [name] size=[size]
包含用于
std::move的<utility>。
例如,输入 Alpha 和 100 时:
Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyed输入 Buffer 和 50 时:
Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyed注意,移动构造函数会转移已分配内存的所有权,而不会复制任何数据。原始缓冲区会保留为空但有效的状态。程序结束时两个缓冲区都会被销毁,但只有仍拥有内存的缓冲区会实际删除它。
亲手试试
#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"
int main() {
std::string name;
int size;
std::cin >> name;
std::cin >> size;
// TODO: 使用给定的名称和大小创建一个 DataBuffer
// TODO: 使用 std::move() 从第一个缓冲区移动,创建第二个缓冲区
// TODO: 输出两个缓冲区的状态:
// 原缓冲区:[name] size=[size]
// 新缓冲区:[name] size=[size]
return 0;
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: C++ 在线编译器