1. 程式人生 > >筆記十:複製建構函式、深拷貝、淺拷貝

筆記十:複製建構函式、深拷貝、淺拷貝

複製建構函式

定義:

只有單個形參,而且該形參是對本類型別物件的引用(常用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 中認為最好使用深拷貝或智慧指標。深拷貝相對於淺拷貝來說,會佔據額外的記憶體資源,但是使用更加安全。