筆記十:複製構造函數、深拷貝、淺拷貝

複製構造函數

定義:

只有單個形參,而且該形參是對本類類型對象的引用(常用const修飾),這樣的構造函數成爲複製構造函數。複製構造函數可用於:
1、根據另一個同類型的對象顯示或隱式初始化一個對象
2、複製一個對象,將它作爲實參傳遞給一個函數
3、從函數返回時複製一個對象
4、初始化順序容器中的元素
5、根據元素初始化列表初始化數組元素
——以上定義來自《C++ Primer 中文版 第4版》


淺拷貝/淺複製

第一條中,若一個自定義類對象已經初始化了,並且用該類去初始化另一個同類類型的對象,假設類中存在指針型變量,若沒有顯示的構造複製構造函數,那麼編譯器會自動生成一個複製構造函數,此時的複製稱爲淺拷貝

更準確的定義如下:
在C++中,在用一個對象初始化另一個對象時,只複製了成員,並沒有複製資源,使兩個對象同時指向了同一資源的複製方式稱爲淺複製

那麼淺拷貝存在的風險是什麼呢?

若自定義類中存在指針成員函數,那麼:
1、淺拷貝只是複製了指針,使得2個指針指向了同一個地址,這樣在對象塊結束調用函數析構時,會造成對同一資源的2次析構,即delete 2次,引起程序崩潰。
2、淺拷貝使得2個指針成員指向同一個內存地址,修改其中一個指針指向的值,會改變另一個對象的指針指向的值
3、在內存釋放時,作爲實參傳遞的類由於析構不成功,造成內存泄露。
以上來自http://blog.csdn.net/feitianxuxue/article/details/9275979

通過一個實例來反應:
代碼:

#include<iostream>

using namespace std;

class A
{
public:
    A()
    {
        data = new char;
        cout << data << endl;
        cout << "默認構造函數" << endl;
    }
    ~A()
    {
        cout << "析構函數" << endl;
        delete data;
        cout << data << endl;
        data = NULL;
    }
private:
    char* data;
};


int main(int argc, char* argv[])
{
    A a;
    A b(a);

    return 0;
}

針對以上代碼,進行單步,逐語句的調試:

1、在A a; 處設置斷點,採取逐語句F11調試,進入到自定義構造函數中:
這裏寫圖片描述

2、對於對象a——動態分配一個內存給data,此時data的值爲0x004084f8,存儲data這個值得內存地址爲0x002ffbf4 :
這裏寫圖片描述

3、繼續執行F11,會發現程序不會再次進入到自定義構造函數,因爲此時A b(a) 執行的複製構造函數,由於類沒有顯示定義,故由編譯器自動完成,程序運行到:
這裏寫圖片描述

4、在main函數結束之前,由於對象的聲明週期已到,故此時需調用析構函數,且析構函數的順序是由後往前析構,即對象b在對象a之後定義,那麼b在a之前析構。通過觀察也可以判斷析構順序,此時存儲data變量的地址爲0x002ffbe8 與a中存儲data的地址不同。
但是a, b中data值均爲0x004084f8

這裏寫圖片描述

5、delete釋放掉內存資源後,data的值變爲0x004084f8<字符串中的字符無法…>:( 個人理解,這裏的0x004084f8<字符串中的字符無法…>0x004084f8<妄…>有本質區別,後者內存地址對應着一個實際的內存空間,內存中存儲的數據顯示亂碼。而前者儘管看似爲一個內存地址值,但並未對應到一個實際的內存空間,好比一個學號之前是可以對應一個學生的,但是學生信息註銷之後,儘管該學號存在,但是無法查找到此人。若理解有誤,懇請指正,虛心學習~ ,此時data成爲一個垂懸指針。

這裏寫圖片描述

6、避免垂懸指針的存在,將指針指向NULL:

這裏寫圖片描述

7、第一次析構結束:

這裏寫圖片描述

8、第2次析構,即對象a的析構。此時可以觀察到data存儲的值已經變爲0x004084f8<字符串中的字符無法…>,即data是一個野指針了。

這裏寫圖片描述

9、繼續析構,則導致程序崩潰:

這裏寫圖片描述

上述調試則解釋了淺拷貝可能造成的影響即:
1、同一內存空間析構2次引起程序崩潰
2、一個類成員修改資源,會使得另一個也隨之改變
3、第3點有疑問,data指向的內存存儲在堆中,但是已經由對象b給釋放了,而data是一個局部變量,其儲存地址在棧中,程序結束,系統自動收回棧中的資源,那麼此時所謂的內存泄露是泄露了哪一部分內存資源呢???表示不太理解,或許我理解有誤???

深拷貝/深複製

定義:
當拷貝對象中有對其他資源(如堆、文件、系統等)的引用時(引用可以是指針或引用)時,對象另開闢一塊新的資源,而不再對拷貝對象中資源的指針或引用進行單純的賦值。簡單地說,即是非共享同一塊內存資源,而是重新開闢一塊內容,將數據複製到新開闢的內存中。

通過一個實例來反應:

#include<iostream>

using namespace std;

class A
{
public:
    A()
    {
        data = new char;
        cout << data << endl;
        cout << "默認構造函數" << endl;
    }
    A(const A& a)
    {
        data = new char;
        memcpy(data, a.data, sizeof(a.data));
    }
    ~A()
    {
        cout << "析構函數" << endl;
        delete data;
        cout << data << endl;
        data = NULL;
    }
private:
    char* data;
};


int main(int argc, char* argv[])
{
    A a;
    A b(a);

    return 0;
}

採用單步調試F11:
1、程序執行到對象a的實例化:

這裏寫圖片描述

2、進入對象a的構造函數中:

這裏寫圖片描述

3、此時在堆中爲data開闢了一個內存,內存地址爲0x005584f8

這裏寫圖片描述

4、注意,在淺複製時,程序直接運行到return 0; 。而深複製,F11後進入複製構造函數:

這裏寫圖片描述

這裏寫圖片描述

5、觀察此時data指向的內存地址爲0x00558d10a.data(0x005584f8)是不同的。memcpy的目的即爲複製數據。

這裏寫圖片描述

6、執行對象b的析構,此時data的值爲0x00558d10,正好是b對象複製構造中分配的內存空間的地址。

這裏寫圖片描述

7、此時對象b佔據的堆中的資源被釋放。

這裏寫圖片描述

這裏寫圖片描述

8、第2次析構,即a對象中資源的釋放,此時data的值爲0x005584f8

這裏寫圖片描述

9、程序沒有出現崩潰的狀況,表明2次析構成功。
這裏寫圖片描述

那麼,什麼時候用淺拷貝?什麼時候用深拷貝呢?
http://www.cricode.com/753.html 中認爲最好使用深拷貝或智能指針。深拷貝相對於淺拷貝來說,會佔據額外的內存資源,但是使用更加安全。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章