詳解C++中對構造函數(shù)和賦值運算符的復制和移動操作
復制構造函數(shù)和復制賦值運算符
從 C++ 11 中開始,該語言支持兩種類型的分配:復制賦值和移動賦值。 在本文中,“賦值”意味著復制賦值,除非有其他顯式聲明。 賦值操作和初始化操作都會導致對象被復制。
賦值:在將一個對象的值賦給另一個對象時,第一個對象將復制到第二個對象中。 因此,
Point a, b; ... a = b;
導致 b 的值被復制到 a 中。
初始化:在以下情況下將進行初始化:聲明新對象、參數(shù)通過值傳遞給函數(shù)或值通過值從函數(shù)返回。
您可以為類類型的對象定義“復制”的語義。 例如,考慮此代碼:
TextFile a, b; a.Open( "FILE1.DAT" ); b.Open( "FILE2.DAT" ); b = a;
前面的代碼可能表示“將 FILE1.DAT 的內(nèi)容復制到 FILE2.DAT”,也可能表示“忽略 FILE2.DAT 并使 b 成為 FILE1.DAT 的另一個句柄”。 您必須將適當?shù)膹椭普Z義附加到每個類,如下所示。
通過將賦值運算符 operator= 與對類類型的引用一起用作返回類型和 const 引用所傳遞的參數(shù)(例如,ClassName& operator=(const ClassName& x);)。
通過通過復制構造函數(shù)。 有關復制構造函數(shù)的詳細信息,請參閱聲明構造函數(shù)的規(guī)則。
如果不聲明復制構造函數(shù),編譯器將為你生成 member-wise 復制構造函數(shù)。 如果不聲明復制賦值運算符,編譯器將為你生成 member-wise 復制賦值運算符。 聲明復制構造函數(shù)不會取消編譯器生成的復制賦值運算符,反之亦然。 如果實現(xiàn)上述其中一項,建議您還實現(xiàn)另一項以使代碼的含義變得明確。
逐個成員賦值和初始化 中更詳細地介紹了逐個成員賦值。
復制構造函數(shù)采用了 class-name& 類型的參數(shù),其中 class-name 是為其定義構造函數(shù)的類的名稱。 例如:
// spec1_copying_class_objects.cpp class Window { public: Window( const Window& ); // Declare copy constructor. // ... }; int main() { }
說明:
盡可能創(chuàng)建該類型的復制構造函數(shù)的參數(shù) const class-name&。 這可防止復制構造函數(shù)意外更改從中復制它的對象。 它還支持從 const 對象進行復制。
編譯器生成的構造函數(shù)
編譯器生成的復制構造函數(shù)(如用戶定義的復制構造函數(shù))具有單個參數(shù)類型“對 class-name 的引用”。 當所有基類和成員類都具有聲明為采用 const class-name& 類型的單個參數(shù)的復制構造函數(shù)時,將引發(fā)異常。 在這種情況下,編譯器生成的復制構造函數(shù)的參數(shù)也是 const。
當復制構造函數(shù)的參數(shù)類型不是 const 時,通過復制 const 對象進行初始化將產(chǎn)生錯誤。 反之則不然:如果參數(shù)是 const,您可以通過復制不是 const 的對象進行初始化。
編譯器生成的賦值運算符遵循關于 const 的相同模式。 除非所有基類和成員類中的賦值運算符都采用 const class-name& 類型的參數(shù),否則它們將采用 class-name& 類型的單個參數(shù)。 在這種情況下,類的生成的賦值運算符采用 const 參數(shù)。
說明:
當虛擬基類由復制構造函數(shù)(編譯器生成或用戶定義的)初始化時,將只初始化這些基類一次:在構造它們時。
含義類似于復制構造函數(shù)的含義。 當參數(shù)類型不是 const 時,從 const 對象賦值將產(chǎn)生錯誤。 反之則不然:如果將 const 值賦給不是 const 的值,則賦值能成功。
移動構造函數(shù)和移動賦值運算符
下面的示例基于用于管理內(nèi)存緩沖區(qū)的 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++ 類編寫移動構造函數(shù)和移動賦值運算符。
為 C++ 創(chuàng)建移動構造函數(shù)
定義一個空的構造函數(shù)方法,該方法采用一個對類類型的右值引用作為參數(shù),如以下示例所示:
MemoryBlock(MemoryBlock&& other) : _data(nullptr) , _length(0) { }
在移動構造函數(shù)中,將源對象中的類數(shù)據(jù)成員添加到要構造的對象:
_data = other._data; _length = other._length;
將源對象的數(shù)據(jù)成員分配給默認值。 這樣可以防止析構函數(shù)多次釋放資源(如內(nèi)存):
other._data = nullptr; other._length = 0;
為 C++ 類創(chuàng)建移動賦值運算符
定義一個空的賦值運算符,該運算符采用一個對類類型的右值引用作為參數(shù)并返回一個對類類型的引用,如以下示例所示:
MemoryBlock& operator=(MemoryBlock&& other) { }
在移動賦值運算符中,如果嘗試將對象賦給自身,則添加不執(zhí)行運算的條件語句。
if (this != &other) { }
在條件語句中,從要將其賦值的對象中釋放所有資源(如內(nèi)存)。
以下示例從要將其賦值的對象中釋放 _data 成員:
// Free the existing resource. delete[] _data;
執(zhí)行第一個過程中的步驟 2 和步驟 3 以將數(shù)據(jù)成員從源對象轉(zhuǎn)移到要構造的對象:
// 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 類的完整移動構造函數(shù)和移動賦值運算符:
// 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; }
以下示例演示移動語義如何能提高應用程序的性能。此示例將兩個元素添加到一個矢量對象,然后在兩個現(xiàn)有元素之間插入一個新元素。在 Visual C++ 2010 中,vector 類使用移動語義,通過移動矢量元素(而非復制它們)來高效地執(zhí)行插入操作。
// 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)); }
該示例產(chǎn)生下面的輸出:
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.
使用移動語義的此示例版本比不使用移動語義的版本更高效,因為前者執(zhí)行的復制、內(nèi)存分配和內(nèi)存釋放操作更少。
可靠編程
若要防止資源泄漏,請始終釋放移動賦值運算符中的資源(如內(nèi)存、文件句柄和套接字)。
若要防止不可恢復的資源損壞,請正確處理移動賦值運算符中的自我賦值。
如果為您的類同時提供了移動構造函數(shù)和移動賦值運算符,則可以編寫移動構造函數(shù)來調(diào)用移動賦值運算符,從而消除冗余代碼。以下示例顯示了調(diào)用移動賦值運算符的移動構造函數(shù)的修改后的版本:
// Move constructor. MemoryBlock(MemoryBlock&& other) : _data(nullptr) , _length(0) { *this = std::move(other); }