C++移動建構函式和移動賦值運算子詳解

2020-07-16 10:04:41
先來看一個 NumberArray 類程式:
//overload2.h中
class NumberArray
{
    private:
        double *aPtr;
        int arraySize;
    public:
        //複製賦值和複製建構函式
        NumberArray& operator=(const NumberArray &right);
        NumberArray(const NumberArray &);
       
        //預設建構函式和常規建構函式
        NumberArray();
        NumberArray(int size, double value);
       
        //解構函式
        ?NumberArray(〉 { if(arraySize > 0) delete [ ] aPtr; }
       
        void print() const;
        void seValue(double value);
}
該類的每個物件都擁有資源,也就是一個指向動態分配記憶體的指標。像這樣的類需要程式設計師定義的複製建構函式和過載的複製賦值運算子,這些都是很有必要的,如果沒有它們,那麼按成員複製操作時將出現對資源的無意共用從而導致傳送錯誤。

從 C++ 11 開始,類可以定義一個移動建構函式和一個移動賦值運算子。要更好地理解這些概念,則需要仔細研究解構函式、複製建構函式和複製賦值運算子的操作。為了幫助監控這些函數被呼叫時所發生的事情,可以修改 NumberArray 解構函式和所有的建構函式,使它們包含一個列印語句,跟蹤這些函數的執行情況。

例如,可以按以下方式修改複製建構函式:
NumberArray::NumberArray(const NumberArray &obj)
{
    cout <<"Copy constructor runningn";
    arraySize = obj.arraySize;
    aPtr = new double[arraySize];
    for(int index = 0; index < arraySize; index++)
    {
        aPtr[index] = obj.aPtr[index];
    }
}
同樣地,也可以將以下語句新增到解構函式、其他建構函式以及複製賦值運算子:
cout <<"Destructor runningn";
cout <<"Default constructor runningn";
cout <<"Regular constructor runningn";
cout <<"Assignment operator runningn";
下面的程式演示了複製建構函式和複製賦值運算子的工作方式。
// This program demonstrates the copy constructor and the
// copy assignment operator for the NumberArray class. #include <iostream>
#include "overload2.h"
using namespace std;

//Function Prototype
NumberArray makeArray();

int main()
{
    NumberArray first;
    first = makeArray();
    NumberArray second = makeArray();
    cout << endl << "The objectTs data is ";
    first.print();
    cout << endl;
    return 0;
}

//Creates a local object and returns it by value.
NumberArray makeArray()
{
    NumberArray nArr(5, 10.5);
    return nArr;
}
程式輸出結果:

(1) Default constructor running
(2) Regular constructor running
(3) Copy constructor running
(4) Destructor running
(5) Copy Assignment operator running
(6) Destructor running
(7) Regular constructor running
(8) Copy constructor running
(9) Destructor running
(10) The objecfs data is 10.5010.5010.5010.5010.50
(11) Destructor running
(12) Destructor running

上述程式輸出結果中的行編號並不是由程式生成的,這裡新增它們只是為了方便討論:
  • 第(1)行中的輸出是由程式第 11 行中的預設建構函式生成的;
  • 第(2)行是由程式 makeArray() 函數建立區域性物件時生成的;
  • 第(3)行的輸出是當複製建構函式被呼叫,複製區域性物件並建立從函數返回的臨時物件時生成的;
  • 此時,區域性物件被銷毀,於是產生了第(4)行的輸出;
  • 接下來,程式的第 12 行執行,呼叫複製賦值運算子並生成了第(5)行的輸出;
  • 在賦值完成之後,臨時物件被銷毀,於是產生了第(6)行的輸出;
  • 第(7)行的輸出是在第 2 次呼叫 makeArray() 時通過建立區域性物件生成的;
  • 返回的物件被直接複製到由複製建構函式第 2 次建立的物件中,由此產生了第(8)行的輸出;
  • 此後函數中的區域性物件被銷毀,產生了第(9)行的輸出;
  • 當在main函數的末尾銷毀了 first 和 second 物件時,即產生了第(11)行和第(12)行的輸出。

注意,有些編譯器可能會進行優化,取消對某些建構函式的呼叫。

這裡特別要關注的是程式的第 12 行:

first = makeArray();

它呼叫了複製賦值運算子以複製臨時物件 makeArray(),如輸出結果中的第(5)行所示。複製賦值運算子將刪除 first 中的 aPtr 陣列,分配另外一個陣列,其大小和臨時物件中的大小一樣,然後將臨時陣列中的值複製到 first 的 aPtr 陣列中。

複製賦值運算子將努力避免在 first 和臨時物件之間共用指標,但是就在這以後,臨時物件被銷毀,其 aPtr 陣列也被刪除。隱藏在移動賦值運算子後面的想法就是,通過讓被賦值的物件與臨時物件交換資源,從而避免以上所有工作。

釆用這種方法之後,當臨時物件被銷毀時,它刪除的是之前被 first 所佔用的記憶體,而first則避免了複製以前在臨時aPtr陣列中的元素,因為這些元素現在已經屬於它的了。

NumberArray 類的移動賦值運算子編寫方法如下所示。為了簡化程式碼,這裡使用了一個庫函數 swap 來交換兩個記憶體位置的內容。swap 函數是在 <algorithm> 標頭檔案中宣告的。
NumberArray& NumberArray::operator=(NumberArray&& right)
{
    if (this != &right)
    {
        swap(arraySize, right.arraySize);
        swap(aPtr, right.aPtr);
    }
    return *this;
}
請注意,該函數的原型和複製賦值的原型類似,但是移動賦值釆用了右值參照作為形參。這是因為移動賦值應該僅在賦值的來源是一個臨時物件時才執行。還需要注意的是,移動賦值的形參不能是 const,這是因為它需要通過修改物件 "移動" 資源。

移動賦值是在 C++11 中引入的,它明顯比複製賦值高效得多,並且應該僅在賦值的來源是一個臨時物件時才使用。此外還有一個移動建構函式,當建立一個新物件,並且新物件初始化的值來源於一個臨時物件時,即可使用該建構函式。

與移動賦值一樣,移動建構函式不必複製資源,而是直接從臨時物件那裡“竊取”資源。以下是 NumberArray 類的移動建構函式。在此說明一下,該建構函式的形參是一個右值參照,表示該形參是一個臨時物件。
NumberArray::NumberArray(NumberArray && temp)
{
    //從temp物件中“竊取”資源
    this->arraySize = temp.arraySize;
    this->aPtr = temp.aPtr;
    //將temp放置到安全狀態以防止其解構函式執行
    temp.arraySize = 0;
    temp.aPtr = nullptr;
}
請注意,移動建構函式的形參也不能是 const。此外,作為移動建構函式獲取資源來源 的臨時物件必須放置到安全狀態,使得其解構函式可以正常執行而不會導致出錯。

NumberArray 類的移動操作的實現可以在如下所示的 overload3.h 和 overload3.cpp 檔案中找到:
//overload3.h 的內容
#include <iostream>
using namespace std;

class NumberArray
{
    private:
        double *aPtr;
        int arraySize;
    public:
        //Copy assignment and copy constructor
        NumberArrays operator=(constNumberArray &right);
        NumberArray(constNumberArray &);

        //Default constructor and Regular constructor NumberArray;
        NumberArray(int size, double value);

        //Move Assignment and Move Constructor NumberArrays
        operator=(NumberArray &&);
        NumberArray (NumberArray &&);
       
        // Destructor
        ?NumberArray();
       
        void print () const;
        void setValue(double value);
};
//overload3.cpp 的內容
#include <iostream>
#include "overload3.h"
using namespace std;
NumberArray&NumberArray::operator=(const NumberArray&right) {
    cout <<"Copy Assignment operator runningn";
    if (this != &right)
    {
        if (arraySize > 0)
        {
            delete [ ] aPtr;
        }
        arraySize = right.arraySize;
        aPtr = new double[arraySize];
        for (int index = 0; index < arraySize; index++) {
            aPtr[index] = right.aPtr[index];
        }
    }
    return *this;
}

NumberArray::NumberArray(const NumberArray&obj)
{
    cout <<"Copy constructor runningn";
    arraySize = obj.arraySize;
    aPtr = new double[arraySize];
    for (int index = 0; index < arraySize; index++)
    {
        aPtr[index] = obj.aPtr[index];
    }
}
NumberArray::NumberArray(int size1, double value)
{
    cout << "Regular constructor runningn"; arraySize = Size1;
    aPtr = new double[arraySize];
    setValue(value);
}
NumberArray::NumberArray()
{
    cout <<"Default constructor runningn";
    arraySize = 2;
    aPtr = new double[arraySize];
    setValue (0.0);
}
void NumberArray::setValue(double value)
{
    for (int index = 0; index < arraySize; index++)
    {
        aPtr[index] = value;
    }
}
void NumberArray::print()const
{
    for (int index = 0; index < arraySize; index++)
    {
        cout << aPtr [index] = " ";
    }
}
NumberArray::?NumberArray()
{
    cout <<"Destructor runningn";
    if (arraySize > 0)
    {
        delete[] aPtr;
    }
}
NumberArray &NumberArray::operator(NumberArray&& right)
{
    cout << "Move assignment is runningn";
    if (this != &right)
    {
        swap(arraySize, right.arraySize);
        swap(aPtr, right.aPtr);
    }
    return *this;
}
NumberArray::NumberArray(NumberArray && temp)
{
    //從temp物件中“竊取”資源
    this->arraySize = temp.arraySize;
    this->aPtr = temp.aPtr;
    //將temp放置到安全狀態
    //以防止其解構函式執行
    temp.arraySize = 0;
    temp.aPtr = nullptr;
}
下面的程式中演示了這些操作:
// This program demonstrates move constructor the move assignment operator.
#include <iostream>
#include "Toverload3.h"
using namespace std;

NumberArray makeArray();// Prototype

int main()
{
    NumberArray first;
    first = makeArray();
    NumberArray second = makeArray();
    cout << endl << "The object's data is ";
    first.print();
    cout << endl;
    return 0;
}
NumberArray makeArray()
{
    NumberArray nArr(5,10.5);
    return nArr;
}
程式輸出結果:

(1) Default constructor running
(2) Regular constructor running
(3) Destructor running
(4) Move assignment is running
(5) Destructor running
(6) Regular constructor running
(7) Destructor running
(8) The objectTs data is10.5010.5010.5010.5010.50
(9) Destructor running
(10) Destructor running

通過檢查程式輸出結果的第(4)行可知,當所賦值的來源是一個臨時物件時(參見程式的第 11 行程式碼),即可呼叫移動賦值函數。程式的第 12 行使用了一個臨時物件來初始化一個 NumberArray 物件,這樣就導致移動建構函式被呼叫。但是,在程式輸出結果中並沒有相關輸出記錄,這是因為編譯器有些時候會使用優化技術避免呼叫複製或移動建構函式。

編譯器使用移動操作的時機

與複製建構函式以及賦值運算子一樣,移動操作也是在合適的時候才被編譯器呼叫。

特別是,編譯器將在以下時機使用移動操作。
  1. 函數通過值返回結果。
  2. 物件被賦值並且右側是一個臨時物件。
  3. 物件被使用一個臨時物件進行初始化。

雖然絕大多數移動操作都被用於從一個臨時物件中轉移資源,但也並不總是這樣。

例如,有一個 unique_ptr 類,某個 unique_ptr 可能需要將它管理的物件轉移到另外一個 unique_ptr 物件。如果這個源目標並不是一個臨時物件,那麼編譯器並不會告訴它自己說,這裡應該使用移動操作。所以,在這種情況下,可以使用 std::move() 庫函數。該函數的效果就是讓它的實參看起來像是一個右值,並且允許移動它。