拷貝建構函式(比較全的)

licup123發表於2009-02-11

(轉載自http://www.pconline.com.cn/pcedu/empolder/gj/c/0503/570112.html)

我們之前已經學習過了類的建構函式和解構函式的相關知識,對於普通型別的物件來說,他們之間的複製是很簡單的,例如:

int a = 10;
int b =a;

自己定義的類的物件同樣是物件,誰也不能阻止我們用以下的方式進行復制,例如:

#include <iostream
using namespace std; 
 
class Test 

public
    Test(int temp) 
    { 
        p1=temp; 
    } 
protected
    int p1; 
 
}; 
 
void main() 

    Test a(99); 
    Test b=a; 
}

  普通物件和類物件同為物件,他們之間的特性有相似之處也有不同之處,類物件內部存在成員變數,而普通物件是沒有的,當同樣的複製方法發生在不同的物件上的時候,那麼系統對他們進行的操作也是不一樣的,就類物件而言,相同型別的類物件是通過拷貝建構函式來完成整個複製過程的,在上面的程式碼中,我們並沒有看到拷貝建構函式,同樣完成了複製工作,這又是為什麼呢?因為當一個類沒有自定義的拷貝建構函式的時候系統會自動提供一個預設的拷貝建構函式,來完成複製工作。

  下面,我們為了說明情況,就普通情況而言(以上面的程式碼為例),我們來自己定義一個與系統預設拷貝建構函式一樣的拷貝建構函式,看看它的內部是如何工作的!

  程式碼如下:

#include <iostream
using namespace std; 
 
class Test 

public
    Test(int temp) 
    { 
        p1=temp; 
    } 
    Test(Test &c_t)//這裡就是自定義的拷貝建構函式 
    { 
        cout<        p1=c_t.p1;//這句如果去掉就不能完成複製工作了,此句複製過程的核心語句 
    } 
public
    int p1; 
}; 
 
void main() 

    Test a(99); 
    Test b=a; 
    cout<    cin.get(); 
}

  上面程式碼中的Test(Test &c_t)就是我們自定義的拷貝建構函式,拷貝建構函式的名稱必須與類名稱一致,函式的形式引數是本型別的一個引用變數,必須是引用

  當用一個已經初始化過了的自定義類型別物件去初始化另一個新構造的物件的時候,拷貝建構函式就會被自動呼叫,如果你沒有自定義拷貝建構函式的時候系統將會提供給一個預設的拷貝建構函式來完成這個過程,上面程式碼的複製核心語句就是通過Test(Test &c_t)拷貝建構函式內的p1=c_t.p1;語句完成的。如果取掉這句程式碼,那麼b物件的p1屬性將得到一個未知的隨機值;

下面我們來討論一下關於淺拷貝和深拷貝的問題。

  就上面的程式碼情況而言,很多人會問到,既然系統會自動提供一個預設的拷貝建構函式來處理複製,那麼我們沒有意義要去自定義拷貝建構函式呀,對,就普通情況而言這的確是沒有必要的,但在某寫狀況下,類體內的成員是需要開闢動態開闢堆記憶體的,如果我們不自定義拷貝建構函式而讓系統自己處理,那麼就會導致堆記憶體的所屬權產生混亂,試想一下,已經開闢的一端堆地址原來是屬於物件a的,由於複製過程發生,b物件取得是a已經開闢的堆地址,一旦程式產生析構,釋放堆的時候,計算機是不可能清楚這段地址是真正屬於誰的,當連續發生兩次析構的時候就出現了執行錯誤。

  為了更詳細的說明問題,請看如下的程式碼。

#include <iostream
using namespace std; 
 
class Internet 

public
    Internet(char *name,char *address) 
    { 
        cout<        strcpy(Internet::name,name); 
        strcpy(Internet::address,address); 
        cname=new char[strlen(name)+1]; 
        if(cname!=NULL) 
        { 
            strcpy(Internet::cname,name); 
        } 
    } 
    Internet(Internet &temp) 
    { 
        cout<        strcpy(Internet::name,temp.name); 
        strcpy(Internet::address,temp.address); 
        cname=new char[strlen(name)+1];//這裡注意,深拷貝的體現! 
        if(cname!=NULL) 
        { 
            strcpy(Internet::cname,name); 
        } 
    } 
    ~Internet() 
    { 
        cout<        delete[] cname; 
        cin.get(); 
    } 
    void show(); 
protected
    char name[20]; 
    char address[30]; 
    char *cname; 
}; 
void Internet::show() 

    cout<
void test(Internet ts) 

    cout<} 
void main() 

    Internet a("中國軟體開發實驗室","www.cndev-lab.com"); 
    Internet b = a; 
    b.show(); 
    test(b); 
}

  上面程式碼就演示了深拷貝的問題,對物件b的cname屬性採取了新開闢記憶體的方式避免了記憶體歸屬不清所導致析構釋放空間時候的錯誤,最後我必須提一下,對於上面的程式我的解釋並不多,就是希望讀者本身執行程式觀察變化,進而深刻理解。

  拷貝和淺拷貝的定義可以簡單理解成:如果一個類擁有資源(堆,或者是其它系統資源),當這個類的物件發生複製過程的時候,這個過程就可以叫做深拷貝,反之物件存在資源但複製過程並未複製資源的情況視為淺拷貝


  淺拷貝資源後在釋放資源的時候會產生資源歸屬不清的情況導致程式執行出錯,這點尤其需要注意!

  以前我們的教程中討論過函式返回物件產生臨時變數的問題,接下來我們來看一下在函式中返回自定義型別物件是否也遵循此規則產生臨時物件
先執行下列程式碼:

#include <iostream
using namespace std; 
 
class Internet 

public
    Internet() 
    { 
         
    }; 
    Internet(char *name,char *address) 
    { 
        cout<        strcpy(Internet::name,name); 
    } 
    Internet(Internet &temp) 
    { 
        cout<        strcpy(Internet::name,temp.name); 
        cin.get(); 
    } 
    ~Internet() 
    { 
        cout<        cin.get(); 
    } 
protected
    char name[20]; 
    char address[20]; 
}; 
Internet tp() 

    Internet b("中國軟體開發實驗室","www.cndev-lab.com"); 
    return b; 

void main() 

    Internet a; 
    a=tp(); 
}

  從上面的程式碼執行結果可以看出,程式一共載入過解構函式三次,證明了由函式返回自定義型別物件同樣會產生臨時變數,事實上物件a得到的就是這個臨時Internet類型別物件temp的值。

  這一下節的內容我們來說一下無名物件

  利用無名物件初始化物件系統不會不呼叫拷貝建構函式。

  那麼什麼又是無名物件呢?

  很簡單,如果在上面程式的main函式中有:

  Internet ("中國軟體開發實驗室","www.cndev-lab.com");

  這樣的一句語句就會產生一個無名物件,無名物件會呼叫建構函式但利用無名物件初始化物件系統不會不呼叫拷貝建構函式!

  下面三段程式碼是很見到的三種利用無名物件初始化物件的例子。

#include <iostream
using namespace std; 
 
class Internet 

public
    Internet(char *name,char *address) 
    { 
        cout<        strcpy(Internet::name,name); 
    } 
    Internet(Internet &temp) 
    { 
        cout<        strcpy(Internet::name,temp.name); 
        cin.get(); 
    } 
    ~Internet() 
    { 
        cout<    } 
public
    char name[20]; 
    char address[20]; 
}; 
 
void main() 

    Internet a=Internet("中國軟體開發實驗室","www.cndev-lab.com"); 
    cout<    cin.get(); 
}

  上面程式碼的執行結果有點“出人意料”,從思維邏輯上說,當無名物件建立了後,是應該呼叫自定義拷貝建構函式,或者是預設拷貝建構函式來完成複製過程的,但事實上系統並沒有這麼做,因為無名物件使用過後在整個程式中就失去了作用,對於這種情況c++會把程式碼看成是:

Internet a("中國軟體開發實驗室",www.cndev-lab.com);

  省略了建立無名物件這一過程,所以說不會呼叫拷貝建構函式

最後讓我們來看看引用無名物件的情況。

#include <iostream>   
using namespace std;   
   
class Internet   
{   
public:   
    Internet(char *name,char *address)   
    {   
        cout<        strcpy(Internet::name,name);   
    }   
    Internet(Internet &temp)   
    {   
        cout<        strcpy(Internet::name,temp.name);   
        cin.get();   
    }   
    ~Internet()   
    {   
        cout<    }   
public:   
    char name[20];   
    char address[20];   
};   
   
void main()   
{   
    Internet &a=Internet("中國軟體開發實驗室","www.cndev-lab.com");   
    cout<    cin.get();   
}

  引用本身是物件的別名,和複製並沒有關係,所以不會呼叫拷貝建構函式,但要注意的是,在c++看來:

Internet &a=Internet("中國軟體開發實驗室","www.cndev-lab.com");

  是等價與:

Internet a("中國軟體開發實驗室","www.cndev-lab.com");

  的,注意觀察呼叫解構函式的位置(這種情況是在main()外呼叫,而無名物件本身是在main()內析構的)。

 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10697500/viewspace-548659/,如需轉載,請註明出處,否則將追究法律責任。

相關文章