C++移動構造函數以及move語句簡單介紹


C++移動構造函數以及move語句簡單介紹

首先看一個小例子:

復制代碼
#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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM