C++移動建構函式以及move語句簡單介紹

晚餐吃什麼發表於2018-11-28

首先看一個小例子:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

int main()
{
    string st = "I love xing";
    vector<string> vc ;
    vc.push_back(move(st));
    cout<<vc[0]<<endl;
    if(!st.empty())
        cout<<st<<endl;

    return 0;
}

結果為:

 

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

int main()
{
    string st = "I love xing";
    vector<string> vc ;
    vc.push_back(st);
    cout<<vc[0]<<endl;
    if(!st.empty())
        cout<<st<<endl;

    return 0;
}

結果為:

 

這兩個小程式唯一的不同是呼叫vc.push_back()將字串插入到容器中去時,第一段程式碼使用了move語句,而第二段程式碼沒有使用move語句。輸出的結果差異也很明顯,第一段程式碼中,原來的字串st已經為空,而第二段程式碼中,原來的字串st的內容沒有變化。

 

好,記住這兩端程式碼的輸出結果之間的差異。下面我們簡單介紹一下移動建構函式。

在介紹移動建構函式之前,我們先要回顧一下拷貝建構函式。

我們都知道,C++在三種情況下會呼叫拷貝建構函式(可能有紕漏),第一種情況是函式形實結合時,第二種情況是函式返回時,函式棧區的物件會複製一份到函式的返回去,第三種情況是用一個物件初始化另一個物件時也會呼叫拷貝建構函式。

除了這三種情況下會呼叫拷貝建構函式,另外如果將一個物件賦值給另一個物件,這個時候回撥用過載的賦值運算子函式。

無論是拷貝建構函式,還是過載的賦值運算子函式,我記得當時在上C++課的時候,老師再三強調,一定要注意指標的淺層複製問題。

這裡在簡單回憶一下拷貝建構函式中的淺層複製問題

首先看一個淺層複製的程式碼

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

class Str{
    public:
    char *value;
    Str(char s[])
    {
        cout<<"呼叫建構函式..."<<endl;
        int len = strlen(s);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,s);
    }
    Str(Str &v)
    {
        cout<<"呼叫拷貝建構函式..."<<endl;
        this->value = v.value;
    }
    ~Str()
    {
        cout<<"呼叫解構函式..."<<endl;
        if(value != NULL)
            delete[] value;
    }
};

int main()
{

    char s[] = "I love BIT";
    Str *a = new Str(s);
    Str *b = new Str(*a);
    delete a;
    cout<<"b物件中的字串為:"<<b->value<<endl;
    delete b;
    return 0;
}

輸出結果為:

 

 

首先結果並不符合預期,我們希望b物件中的字串也是I love BIT但是輸出為空,這是因為b->value和a->value指向了同一片記憶體區域,當delete a的時候,該記憶體區域已經被收回,所以再用b->value訪問那塊記憶體實際上是不合適的,而且,雖然我執行時程式沒有崩潰,但是程式存在崩潰的風險呀,因為當delete b的時候,那塊記憶體區域又被釋放了一次,兩次釋放同一塊記憶體,相當危險呀。

我們用valgrind檢查一下,發現,相當多的記憶體錯誤呀!

 

 

其中就有一個Invalid free 也就是刪除b的時候呼叫解構函式,對已經釋放掉對空間又釋放了一次。

 

那麼深層複製應該怎樣寫呢?

程式碼如下:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

class Str{
    public:
    char *value;
    Str(char s[])
    {
        cout<<"呼叫建構函式..."<<endl;
        int len = strlen(s);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,s);
    }
    Str(Str &v)
    {
        cout<<"呼叫拷貝建構函式..."<<endl;
        int len = strlen(v.value);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,v.value);
    }
    ~Str()
    {
        cout<<"呼叫解構函式..."<<endl;
        if(value != NULL)
        {
            delete[] value;
            value = NULL;
        }
    }
};

int main()
{

    char s[] = "I love BIT";
    Str *a = new Str(s);
    Str *b = new Str(*a);
    delete a;
    cout<<"b物件中的字串為:"<<b->value<<endl;
    delete b;
    return 0;
}

結果為:

 

 

這次達到了我們預想的效果,而且,用valgrind檢測一下,發現,沒有記憶體錯誤!

 

 

 

所以,寫拷貝建構函式的時候,切記要注意指標的淺層複製問題呀!

 

好的,回顧了一下拷貝建構函式,下面回到移動建構函式上來。

有時候我們會遇到這樣一種情況,我們用物件a初始化物件b,後物件a我們就不在使用了,但是物件a的空間還在呀(在析構之前),既然拷貝建構函式,實際上就是把a物件的內容複製一份到b中,那麼為什麼我們不能直接使用a的空間呢?這樣就避免了新的空間的分配,大大降低了構造的成本。這就是移動建構函式設計的初衷。

下面這個圖,很好地說明了拷貝建構函式和移動建構函式的區別。

 

 

看明白了嗎?

通俗一點的解釋就是,拷貝建構函式中,對於指標,我們一定要採用深層複製,而移動建構函式中,對於指標,我們採用淺層複製。

但是上面提到,指標的淺層複製是非常危險的呀。沒錯,確實很危險,而且通過上面的例子,我們也可以看出,淺層複製之所以危險,是因為兩個指標共同指向一片記憶體空間,若第一個指標將其釋放,另一個指標的指向就不合法了。所以我們只要避免第一個指標釋放空間就可以了。避免的方法就是將第一個指標(比如a->value)置為NULL,這樣在呼叫解構函式的時候,由於有判斷是否為NULL的語句,所以析構a的時候並不會回收a->value指向的空間(同時也是b->value指向的空間)

所以我們可以把上面的拷貝建構函式的程式碼修改一下:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

class Str{
    public:
    char *value;
    Str(char s[])
    {
        cout<<"呼叫建構函式..."<<endl;
        int len = strlen(s);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,s);
    }
    Str(Str &v)
    {
        cout<<"呼叫拷貝建構函式..."<<endl;
        this->value = v.value;
        v.value = NULL;
    }
    ~Str()
    {
        cout<<"呼叫解構函式..."<<endl;
        if(value != NULL)
            delete[] value;
    }
};

int main()
{

    char s[] = "I love BIT";
    Str *a = new Str(s);
    Str *b = new Str(*a);
    delete a;
    cout<<"b物件中的字串為:"<<b->value<<endl;
    delete b;
    return 0;
}

結果為:

 

 

修改後的拷貝建構函式,採用了淺層複製,但是結果仍能夠達到我們想要的效果,關鍵在於在拷貝建構函式中,最後我們將v.value置為了NULL,這樣在析構a的時候,就不會回收a->value指向的記憶體空間。

 

這樣用a初始化b的過程中,實際上我們就減少了開闢記憶體,構造成本就降低了。

 

但要注意,我們這樣使用有一個前提是:用a初始化b後,a我們就不需要了,最好是初始化完成後就將a析構。如果說,我們用a初始化了b後,仍要對a進行操作,用這種淺層複製的方法就不合適了。

所以C++引入了移動建構函式,專門處理這種,用a初始化b後,就將a析構的情況。

 

*************************************************************

**移動建構函式的引數和拷貝建構函式不同,拷貝建構函式的引數是一個左值引用,但是移動建構函式的初值是一個右值引用。(關於右值引用大家可以看我之前的文章,或者查詢其他資料)。這意味著,移動建構函式的引數是一個右值或者將亡值的引用。也就是說,只用用一個右值,或者將亡值初始化另一個物件的時候,才會呼叫移動建構函式。而那個move語句,就是將一個左值變成一個將亡值。

 

移動建構函式應用最多的地方就是STL中

給出一個程式碼,大家自行驗證使用move和不適用move的區別吧

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>
using namespace std;

class Str{
    public:
        char *str;
        Str(char value[])
        {
            cout<<"普通建構函式..."<<endl;
            str = NULL;
            int len = strlen(value);
            str = (char *)malloc(len + 1);
            memset(str,0,len + 1);
            strcpy(str,value);
        }
        Str(const Str &s)
        {
            cout<<"拷貝建構函式..."<<endl;
            str = NULL;
            int len = strlen(s.str);
            str = (char *)malloc(len + 1);
            memset(str,0,len + 1);
            strcpy(str,s.str);
        }
        Str(Str &&s)
        {
            cout<<"移動建構函式..."<<endl;
            str = NULL;
            str = s.str;
            s.str = NULL;
        }
        ~Str()
        {
            cout<<"解構函式"<<endl;
            if(str != NULL)
            {
                free(str);
                str = NULL;
            }
        }
};
int main()
{
    char value[] = "I love zx";
    Str s(value);
    vector<Str> vs;
    //vs.push_back(move(s));
    vs.push_back(s);
    cout<<vs[0].str<<endl;
    if(s.str != NULL)
        cout<<s.str<<endl;
    return 0;
}

原文連結:https://www.cnblogs.com/qingergege/p/7607089.html

相關文章