Menu
CoddyTech

移动语义与右值

本课属于 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 函数实际上不会移动任何东西,它只是将左值转换为右值引用,表示你愿意放弃对象的资源。真正的移动发生在移动构造函数或移动赋值运算符中。

处理容器或字符串等资源占用较大的对象时,移动语义能显著提升性能。你不必深度复制数兆字节的数据,只需转移指针所有权即可。无论数据大小如何,这都是一个常数时间操作。

challenge icon

挑战

简单

让我们来编写一个管理资源的 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:读取两个输入:
    1. 缓冲区的名称(字符串)
    2. 缓冲区的大小(整数)

    使用给定的名称和大小创建一个 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;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器