详解C++中对构造函数和赋值运算符的复制和移动操作
复制构造函数和复制赋值运算符
从 C++ 11 中开始,该语言支持两种类型的分配:复制赋值和移动赋值。 在本文中,“赋值”意味着复制赋值,除非有其他显式声明。 赋值操作和初始化操作都会导致对象被复制。
赋值:在将一个对象的值赋给另一个对象时,第一个对象将复制到第二个对象中。 因此,
Point a, b; ... a = b;
导致 b 的值被复制到 a 中。
初始化:在以下情况下将进行初始化:声明新对象、参数通过值传递给函数或值通过值从函数返回。
您可以为类类型的对象定义“复制”的语义。 例如,考虑此代码:
TextFile a, b; a.Open( "FILE1.DAT" ); b.Open( "FILE2.DAT" ); b = a;
前面的代码可能表示“将 FILE1.DAT 的内容复制到 FILE2.DAT”,也可能表示“忽略 FILE2.DAT 并使 b 成为 FILE1.DAT 的另一个句柄”。 您必须将适当的复制语义附加到每个类,如下所示。
通过将赋值运算符 operator= 与对类类型的引用一起用作返回类型和 const 引用所传递的参数(例如,ClassName& operator=(const ClassName& x);)。
通过通过复制构造函数。 有关复制构造函数的详细信息,请参阅声明构造函数的规则。
如果不声明复制构造函数,编译器将为你生成 member-wise 复制构造函数。 如果不声明复制赋值运算符,编译器将为你生成 member-wise 复制赋值运算符。 声明复制构造函数不会取消编译器生成的复制赋值运算符,反之亦然。 如果实现上述其中一项,建议您还实现另一项以使代码的含义变得明确。
逐个成员赋值和初始化 中更详细地介绍了逐个成员赋值。
复制构造函数采用了 class-name& 类型的参数,其中 class-name 是为其定义构造函数的类的名称。 例如:
// spec1_copying_class_objects.cpp class Window { public: Window( const Window& ); // Declare copy constructor. // ... }; int main() { }
说明:
尽可能创建该类型的复制构造函数的参数 const class-name&。 这可防止复制构造函数意外更改从中复制它的对象。 它还支持从 const 对象进行复制。
编译器生成的构造函数
编译器生成的复制构造函数(如用户定义的复制构造函数)具有单个参数类型“对 class-name 的引用”。 当所有基类和成员类都具有声明为采用 const class-name& 类型的单个参数的复制构造函数时,将引发异常。 在这种情况下,编译器生成的复制构造函数的参数也是 const。
当复制构造函数的参数类型不是 const 时,通过复制 const 对象进行初始化将产生错误。 反之则不然:如果参数是 const,您可以通过复制不是 const 的对象进行初始化。
编译器生成的赋值运算符遵循关于 const 的相同模式。 除非所有基类和成员类中的赋值运算符都采用 const class-name& 类型的参数,否则它们将采用 class-name& 类型的单个参数。 在这种情况下,类的生成的赋值运算符采用 const 参数。
说明:
当虚拟基类由复制构造函数(编译器生成或用户定义的)初始化时,将只初始化这些基类一次:在构造它们时。
含义类似于复制构造函数的含义。 当参数类型不是 const 时,从 const 对象赋值将产生错误。 反之则不然:如果将 const 值赋给不是 const 的值,则赋值能成功。
移动构造函数和移动赋值运算符
下面的示例基于用于管理内存缓冲区的 C++ 类 MemoryBlock。
// MemoryBlock.h #pragma once #include <iostream> #include <algorithm> class MemoryBlock { public: // Simple constructor that initializes the resource. explicit MemoryBlock(size_t length) : _length(length) , _data(new int[length]) { std::cout << "In MemoryBlock(size_t). length = " << _length << "." << std::endl; } // Destructor. ~MemoryBlock() { std::cout << "In ~MemoryBlock(). length = " << _length << "."; if (_data != nullptr) { std::cout << " Deleting resource."; // Delete the resource. delete[] _data; } std::cout << std::endl; } // Copy constructor. MemoryBlock(const MemoryBlock& other) : _length(other._length) , _data(new int[other._length]) { std::cout << "In MemoryBlock(const MemoryBlock&). length = " << other._length << ". Copying resource." << std::endl; std::copy(other._data, other._data + _length, _data); } // Copy assignment operator. MemoryBlock& operator=(const MemoryBlock& other) { std::cout << "In operator=(const MemoryBlock&). length = " << other._length << ". Copying resource." << std::endl; if (this != &other) { // Free the existing resource. delete[] _data; _length = other._length; _data = new int[_length]; std::copy(other._data, other._data + _length, _data); } return *this; } // Retrieves the length of the data resource. size_t Length() const { return _length; } private: size_t _length; // The length of the resource. int* _data; // The resource. };
以下过程介绍如何为示例 C++ 类编写移动构造函数和移动赋值运算符。
为 C++ 创建移动构造函数
定义一个空的构造函数方法,该方法采用一个对类类型的右值引用作为参数,如以下示例所示:
MemoryBlock(MemoryBlock&& other) : _data(nullptr) , _length(0) { }
在移动构造函数中,将源对象中的类数据成员添加到要构造的对象:
_data = other._data; _length = other._length;
将源对象的数据成员分配给默认值。 这样可以防止析构函数多次释放资源(如内存):
other._data = nullptr; other._length = 0;
为 C++ 类创建移动赋值运算符
定义一个空的赋值运算符,该运算符采用一个对类类型的右值引用作为参数并返回一个对类类型的引用,如以下示例所示:
MemoryBlock& operator=(MemoryBlock&& other) { }
在移动赋值运算符中,如果尝试将对象赋给自身,则添加不执行运算的条件语句。
if (this != &other) { }
在条件语句中,从要将其赋值的对象中释放所有资源(如内存)。
以下示例从要将其赋值的对象中释放 _data 成员:
// Free the existing resource. delete[] _data;
执行第一个过程中的步骤 2 和步骤 3 以将数据成员从源对象转移到要构造的对象:
// Copy the data pointer and its length from the // source object. _data = other._data; _length = other._length; // Release the data pointer from the source object so that // the destructor does not free the memory multiple times. other._data = nullptr; other._length = 0;
返回对当前对象的引用,如以下示例所示:
return *this;
以下示例显示了 MemoryBlock 类的完整移动构造函数和移动赋值运算符:
// Move constructor. MemoryBlock(MemoryBlock&& other) : _data(nullptr) , _length(0) { std::cout << "In MemoryBlock(MemoryBlock&&). length = " << other._length << ". Moving resource." << std::endl; // Copy the data pointer and its length from the // source object. _data = other._data; _length = other._length; // Release the data pointer from the source object so that // the destructor does not free the memory multiple times. other._data = nullptr; other._length = 0; } // Move assignment operator. MemoryBlock& operator=(MemoryBlock&& other) { std::cout << "In operator=(MemoryBlock&&). length = " << other._length << "." << std::endl; if (this != &other) { // Free the existing resource. delete[] _data; // Copy the data pointer and its length from the // source object. _data = other._data; _length = other._length; // Release the data pointer from the source object so that // the destructor does not free the memory multiple times. other._data = nullptr; other._length = 0; } return *this; }
以下示例演示移动语义如何能提高应用程序的性能。此示例将两个元素添加到一个矢量对象,然后在两个现有元素之间插入一个新元素。在 Visual C++ 2010 中,vector 类使用移动语义,通过移动矢量元素(而非复制它们)来高效地执行插入操作。
// rvalue-references-move-semantics.cpp // compile with: /EHsc #include "MemoryBlock.h" #include <vector> using namespace std; int main() { // Create a vector object and add a few elements to it. vector<MemoryBlock> v; v.push_back(MemoryBlock(25)); v.push_back(MemoryBlock(75)); // Insert a new element into the second position of the vector. v.insert(v.begin() + 1, MemoryBlock(50)); }
该示例产生下面的输出:
In MemoryBlock(size_t). length = 25. In MemoryBlock(MemoryBlock&&). length = 25. Moving resource. In ~MemoryBlock(). length = 0. In MemoryBlock(size_t). length = 75. In MemoryBlock(MemoryBlock&&). length = 25. Moving resource. In ~MemoryBlock(). length = 0. In MemoryBlock(MemoryBlock&&). length = 75. Moving resource. In ~MemoryBlock(). length = 0. In MemoryBlock(size_t). length = 50. In MemoryBlock(MemoryBlock&&). length = 50. Moving resource. In MemoryBlock(MemoryBlock&&). length = 50. Moving resource. In operator=(MemoryBlock&&). length = 75. In operator=(MemoryBlock&&). length = 50. In ~MemoryBlock(). length = 0. In ~MemoryBlock(). length = 0. In ~MemoryBlock(). length = 25. Deleting resource. In ~MemoryBlock(). length = 50. Deleting resource. In ~MemoryBlock(). length = 75. Deleting resource.
使用移动语义的此示例版本比不使用移动语义的版本更高效,因为前者执行的复制、内存分配和内存释放操作更少。
可靠编程
若要防止资源泄漏,请始终释放移动赋值运算符中的资源(如内存、文件句柄和套接字)。
若要防止不可恢复的资源损坏,请正确处理移动赋值运算符中的自我赋值。
如果为您的类同时提供了移动构造函数和移动赋值运算符,则可以编写移动构造函数来调用移动赋值运算符,从而消除冗余代码。以下示例显示了调用移动赋值运算符的移动构造函数的修改后的版本:
// Move constructor. MemoryBlock(MemoryBlock&& other) : _data(nullptr) , _length(0) { *this = std::move(other); }