C++移动构造函数和移动赋值运算符

在 C++ 中,移动构造函数(Move Constructor)和移动赋值运算符(Move Assignment Operator)是管理资源转移的关键工具,旨在避免不必要的深拷贝,提升程序性能。文章将详细介绍它们的应用场景。

欢迎进入我的哔哩哔哩频道进行学习!

我们可以通过一个自定义的类来详细说明拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符的用法。这次我们用一个简单的 Buffer类来管理动态分配的数组。


1. 移动构造函数

作用

移动构造函数用于将资源从一个对象转移到另一个对象,或者理解为将资源从一个临时对象(右值)转移到新对象,避免不必要的深拷贝。

语法

ClassName(ClassName&& other) noexcept;

实现要点

  • 参数:右值引用 ClassName&&,通常来自临时对象或 std::move
  • 资源转移:直接接管原对象的资源(如指针、文件句柄)。
  • 原对象状态:将原对象的资源指针置空,确保析构时不会重复释放。
  • 异常安全:标记为 noexcept,以便在容器操作中优化性能。

示例代码

#include <iostream>

class Buffer
{
public:
// 普通构造函数
Buffer(size_t size) : m_size(size)
{
std::cout << "普通构造函数" << std::endl;
m_data = new int[m_size];
for (size_t i = 0; i < m_size; ++i)
{
m_data[i] = i; // 初始化数据
}
}

// 移动构造函数
Buffer(Buffer&& other) noexcept : m_data(other.m_data), m_size(other.m_size)
{
std::cout << "移动构造函数" << std::endl;
other.m_data = nullptr; // 将原对象的指针置为空
other.m_size = 0;
}

// 析构函数
~Buffer()
{
std::cout << "析构函数" << std::endl;
delete[] m_data;
}

// 打印数据
void print() const
{
if (m_data)
{
for (size_t i = 0; i < m_size; ++i)
{
std::cout << m_data[i] << " ";
}
std::cout << std::endl;
}
else
{
std::cout << "nullptr" << std::endl;
}
}
private:
int* m_data;
size_t m_size;
};

int main()
{
Buffer b1(5); // 调用普通构造函数
Buffer b2 = std::move(b1); // 调用移动构造函数

std::cout << "b1: ";
b1.print(); // 输出: nullptr
std::cout << "b2: ";
b2.print(); // 输出: 0 1 2 3 4

return 0;
}

输出

普通构造函数
移动构造函数
b1: nullptr
b2: 0 1 2 3 4
析构函数
析构函数

2. 移动赋值运算符

移动赋值运算符用于将资源从一个对象转移到另一个已经存在的对象。

作用

移动赋值运算符用于将资源从一个临时对象(右值)转移到另一个已经存在的对象。

语法

ClassName& operator=(ClassName&& other) noexcept;

实现要点

  • 释放当前资源:先释放目标对象的原有资源。
  • 资源转移:接管原对象的资源,并将原对象资源置空。
  • 自赋值检查:处理 obj = std::move(obj) 的情况。
  • 异常安全:标记为 noexcept

示例代码

#include <iostream>

class Buffer
{
public:
// 普通构造函数
Buffer(size_t size) : m_size(size)
{
std::cout << "普通构造函数" << std::endl;
m_data = new int[m_size];
for (size_t i = 0; i < m_size; ++i)
{
m_data[i] = i; // 初始化数据
}
}

// 移动构造函数
Buffer(Buffer&& other) noexcept : m_data(other.m_data), m_size(other.m_size)
{
std::cout << "移动构造函数" << std::endl;
other.m_data = nullptr; // 将原对象的指针置为空
other.m_size = 0;
}

// 移动赋值运算符
Buffer& operator=(Buffer&& other) noexcept
{
std::cout << "移动赋值运算符" << std::endl;
if (this == &other)
{
return *this; // 自赋值检查
}
delete[] m_data; // 释放原有资源
m_data = other.m_data;
m_size = other.m_size;
other.m_data = nullptr; // 将原对象的指针置为空
other.m_size = 0;
return *this;
}

// 析构函数
~Buffer()
{
std::cout << "析构函数" << std::endl;
delete[] m_data;
}

// 打印数据
void print() const
{
if (m_data)
{
for (size_t i = 0; i < m_size; ++i)
{
std::cout << m_data[i] << " ";
}
std::cout << std::endl;
}
else
{
std::cout << "nullptr" << std::endl;
}
}
private:
int* m_data;
size_t m_size;
};

int main()
{
Buffer b1(5); // 调用普通构造函数
Buffer b2(3); // 调用普通构造函数

b2 = std::move(b1); // 调用移动赋值运算符

std::cout << "b1: ";
b1.print(); // 输出: nullptr
std::cout << "b2: ";
b2.print(); // 输出: 0 1 2 3 4

return 0;
}

输出

普通构造函数
普通构造函数
移动赋值运算符
b1: nullptr
b2: 0 1 2 3 4
析构函数
析构函数

3.注意事项

3.1 移动后的原对象状态

  • 有效但不可用:原对象仍可安全析构(如指针已置空),但不应再使用其数据。例如Buffer对象在移动构造函数或移动赋值运算符中需要使被移走的对象处于一种可以被释放的状态。如下:

    other.m_data = nullptr; // 将原对象的指针置为空
    other.m_size = 0;
  • 调用移动构造函数的示例

    Buffer b1(100);
    Buffer b2 = std::move(b1);
    // b1.m_data 为 nullptr,b1.m_size 为 0

3.2 编译器生成的默认移动操作

如果不生成自己的拷贝构造函数和拷贝赋值运算符,那么,在某些情况下,编译器会合成拷贝构造函数和拷贝赋值运算符,同样道理,在某些情况下,编译器会合成移动构造函数和移动赋值运算符。针对合成问题有一些说法,总结如下:

  • 如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数(这三者之一,表示程序员要自己处理对象的复制或者释放问
    题),编译器就不会为它合成移动构造函数和移动赋值运算符。

  • 一个本该由系统调用移动构造函数和移动赋值运算符的地方,如果类中没有提供移动构造函数和移动赋值运算符,则系统会调用拷贝构造函数和拷贝赋值运算符代替。

  • 一个本该由系统调用移动构造函数和移动赋值运算符的地方,如果类中没有提供移动构造函数和移动赋值运算符,则系统会调用拷贝构造函数和拷贝赋值运算符代替。

    Buffer b1(100);
    Buffer b2 = std::move(b1); //如果Buffer未定义移动构造函数,则系统会调用拷贝构造函数
  • 行为:逐成员移动(对内置类型直接复制,对类类型调用其移动操作)。

3.3 修饰符noexcept作用

不抛出异常的移动构造函数、移动赋值运算符都应该加上noexcept,用于通知编译器该函数本身不抛出异常。否则有可能因为系统内部的一些运作机制原本程序员认为可能会调用移动构造函数的地方却调用了拷贝构造函数。此外,此举还可以提高编译器的工作效率。

4. 使用场景

4.1 返回临时对象

Buffer createBuffer() 
{
Buffer tmp(100);
return tmp; // 编译器优先使用移动而非拷贝(即使未写 std::move)
}

4.2 容器操作

std::vector<Buffer> vec;
vec.push_back(Buffer(100)); // 调用移动构造函数,避免深拷贝,Buffer(100)是临时变量(右值)

4.3 资源管理类

管理动态内存、文件句柄、网络连接等需要高效转移资源的场景。

5. 错误示例与修正

错误:未置空原对象指针

// 错误:移动后原对象的 data 未置空,导致双重释放
Buffer(Buffer&& other) : data(other.data), size(other.size) {}

修正:正确置空原对象

Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) 
{
other.data = nullptr;
other.size = 0;
}

6. 总结

操作 用途 关键行为
移动构造函数 构造新对象并转移资源 接管资源,原对象置空
移动赋值运算符 为已存在对象转移资源 释放旧资源,接管新资源
std::move 显式标记对象为右值 触发移动而非拷贝
noexcept 保证移动操作不抛异常 优化容器操作(如 vector

正确实现移动语义可以显著提升性能,尤其在处理大型对象或资源密集型操作时。