構造函數 及複製構造函數

http://blog.chinaunix.net/uid-28662931-id-3496326.html

在C++中複製控制是一個比較重要的話題,主要包括複製構造函數、重載賦值操作符、析構函數這三部分,這三個函數是一致的,如果類需要析構函數,則它也需要複製操作符 和 複製構造函數,這個規則被稱爲 C++的“三法則”。如果需要手動定義了其中了一個,那麼另外的兩個也需要定義,通常在存在指針或者前期相關操作的情況下,都需要手動的定義。

       複製構造函數與重載賦值操作符實現的大題相同,如果沒有手動的實現,那麼編譯器會自動生成一個,而且這兩個函數的參數也是一致的,是不能夠改變的。析構函數相比前面的兩個存在一個巨大的差別,就是無論我們是否定義這個函數,編譯器都會自動生成一個析構函數。析構函數主要是完成對象的釋放操作。

      複製構造函數與重載賦值操作符在沒有定義的情況下,編譯器會爲我們生成一個,這說明這兩個函數是一個類必不可少的部分。由此可知如果一個類沒有定義任何的東西,編譯器也會幫助我們生成下面的4個函數:
1、一個構造函數,也就是所謂的類名比如classname(),這是在沒有定義構造函數時,編譯器會自動生成的。
2、析構函數,
3、複製構造函數。
4、重載賦值操作符。
假設存在一個類Base;
   class Base
    {
        public:
            Base(); //構造函數
            Base(const Base &);  // 複製構造函數
            Base & operator = (const Base &);   // 賦值操作符
            ~Base();   // 析構函數
        private:
            .......
    };

現在對複製構造函數和賦值操作符做個詳細的說明,下面是一個將要使用的例子;

class CExample
{
public :
     CExample(){pBuffer=NULL; nSize=0;}   //構造函數
     ~CExample(){delete pBuffer;}   // 析構函數
    void Init(int n){ pBuffer=new char [n]; nSize=n;}
private :
    char *pBuffer; //類的對象中包含指針,指向動態分配的內存資源
    int nSize;
};
    這個類的主要特點是包含指向其他資源的指針。 pBuffer指向堆中分配的一段內存空間。 
一、拷貝構造函數
int main(int argc, char * argv[])
{
     CExample A;
     A.Init40);
    
     CExample B=A; //把B初始化爲A的副本
     ...
}
    B = A ; 此語句的具體過程:首先建立對象theObjtwo,並調用其構造函數,然後成員被拷貝。
    語句"CExample B=A;"  用 A 初始化 B。 其完成方式是內存拷貝,複製所有成員的值。 完成後,A.pBuffer = B.pBuffer,  即它們將指向同樣的地方,指針雖然複製了,但所指向的空間並沒有複製,而是由兩個對象共用了。這樣不符合要求,對象之間不獨立了,併爲空間的刪除帶來隱患。 所以需要採用必要的手段(拷貝構造函數)來避免此類情況。 

 拷貝構造函數的格式爲 : 構造函數名(對象的引用)  提供了拷貝構造函數後的CExample類定義爲: 
class CExample
{
public :
     CExample(){pBuffer=NULL; nSize=0;}  //構造函數
     ~CExample(){delete pBuffer;}   // 析構函數
     CExample(const CExample&); //拷貝構造函數
    void Init(int n){ pBuffer=new char [n]; nSize=n;}
private :
    char *pBuffer; //類的對象中包含指針,指向動態分配的內存資源
    int nSize;
};

 //拷貝構造函數的定義
CExample::CExample(const CExample& RightSides)
{
     nSize=RightSides.nSize;    //複製常規成員
     pBuffer=new char [nSize];    //複製指針指向的內容
     memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof (char ));
}
    這樣,定義新對象,並用已有對象初始化新對象時,即執行語句“CExample B=A;  時,CExample(const CExample& RightSides)將被調用,而已有對象用別名RightSides傳給構造函數,以用來作複製。原則上,應該爲所有包含動態分配成員的類都提供拷貝構造函數。 

拷貝函數被調用的情況有:
1,定義新對象,並用已有對象初始化新對象時; 即執行語句“CExample B=A;  時(定義對象時使用賦值初始化);
2,當對象直接作爲參數傳給函數時,函數將建立對象的臨時拷貝,這個拷貝過程也將調同拷貝構造函數。 
    例如:
        BOOL testfunc(CExample obj)
        {
             //針對obj的操作實際上是針對複製後的臨時拷貝進行的
        }
        testfunc(theObjone); //對象直接作爲參數,拷貝函數將被調用
3,當函數中的局部對象被返回給函數調者時,也將建立此局部對象的一個臨時拷貝,拷貝構造函數也將被調用 ;
    例如:
    CTest func()
    {
         CTest   theTest;
         return   theTest
     } 

二、賦值符的重載 
   下面的代碼與上例相似
int main(int argc, char * argv[])
{
     CExample A;
     A.Init(40);
    
     CExample C;
     C.Init(60);

     //現在需要一個對象賦值操作,被賦值對象的原內容被清除,並用右邊對象的內容填充。
     C = A;
    return 0;
}
    也用到了"="號,但與上面的例子中語句“ CExample B=A;  不同“ CExample B=A;  語句中的 "=" 在對象聲明語句中,表示初始化。更多時候,這種初始化也可用括號表示。 例如 CExample B(A); 

    而本例子中,"=" 表示賦值操作。將對象 A 的內容複製到對象C;,這其中涉及到對象C 原有內容的丟棄,新內容的複製。 但"="的缺省操作只是將成員變量的值相應複製。舊的值被自然丟棄。 由於對象內包含指針,將造成不良後果:指針的值被丟棄了,但指針指向的內容並未釋放。指針的值被複制了,但指針所指內容並未複製。 因此,包含動態分配成員的類除提供拷貝構造函數外,還應該考慮重載"="賦值操作符號。 

類定義變爲: 
class CExample
{
public :
     CExample(){pBuffer=NULL; nSize=0;}  //構造函數
     ~CExample(){delete pBuffer;}   // 析構函數
     CExample(const CExample&); //拷貝構造函數
     CExample& operator = (const CExample&); //賦值符重載
    void Init(int n){ pBuffer=new char [n]; nSize=n;}
private :
    char *pBuffer; //類的對象中包含指針,指向動態分配的內存資源
    int nSize;
};
//賦值操作符重載
CExample & CExample::operator = (const CExample& RightSides)
{
     nSize=RightSides.nSize; //複製常規成員
    char *temp=new char [nSize]; //複製指針指向的內容 
     memcpy(temp,RightSides.pBuffer,nSize*sizeof (char ));

    delete []pBuffer; //刪除原指針指向內容   (將刪除操作放在後面,避免X=X特殊情況下,內容的丟失)
     pBuffer=temp;    //建立新指向
    return *this 
}
三、拷貝構造函數使用賦值運算符重載的代碼。 
CExample::CExample(const CExample& RightSides)
{
     pBuffer=NULL;
     *this =RightSides      //調用重載後的"="
}

    爲了更好地理解拷貝構造函數  
 1、爲什麼要有拷貝構造函數,它跟構造函數有什麼區別?
       答:拷貝構造函數其實也是構造函數,只不過它的參數是const 的類自身的對象的引用。如果類裏面沒有指針成員(該指針成員指向動態申請的空間),是沒有必要編寫拷貝構造函數的 。     我們知道,如果有一個類CObj,它已經產生了一個對象ObjA,現在又用CObj去創建ObjB,如果程序中使用語句ObjB = ObjA; 也就是說直接使用ObjA的數據給ObjB賦值。這對於一般的類,沒有任何問題,但是如果CObj裏面有個char * pStr的成員,用來存放動態申請的字符串的地址,在ObjA中使用new 方法動態申請了內存並讓ObjA.pStr指向該申請的空間,在OjbB = OjbA之後,ObjA.pStr和ObjB.pStr將同時指向那片空間,這樣到導致了誰也不知道到底該由誰來負責釋放那塊空間,很有可能導致同一塊內存被釋放兩次。     使用拷貝構造函數,先申請ObjA.pStr所指向的空間大小的空間,然後將空間內容拷貝過來,這樣就不會同時指向同一塊內存,各自有各自申請的內存,各自負責釋放各自申請的內存,從而解決了剛纔的問題。所以這裏的“拷貝”拷貝的是動態申請的空間的內容,而不是類本身的數據。另外注意到,拷貝構造函數的參數是對象的引用,而不是對象的指針。至於爲什麼要用引用,不能夠用指針暫時還沒有搞明白,等搞明白了再說。    
2、爲什麼要對=賦值操作符進行重載?
    答:接上面的例子,用戶在使用語句ObjB = ObjA的時候,或許ObjB的pStr已經指向了動態申請的空間,如果直接簡單將其指向的地址覆蓋,就會導致內存泄露,所以需要對=賦值操作符進行重載,在重載函數中判斷pStr如果已經指向了動態申請的空間,就先將其釋放。    
3、拷貝構造函數和=賦值操作符重載的關係。
    答:從原文的例子中可以看出,=賦值操作符重載比拷貝構造函數做得要多,它除了完成拷貝構造函數所完成的拷貝動態申請的內存的數據之外,還釋放了原本自己申請的內存空間。所以原文最後給出的拷貝構造函數的實現可以使用=賦值操作符的重載來完成。    
4、拷貝構造函數何時被調用?
    a.對象的直接賦值也會調用拷貝構造函數  ;
    b.函數參數傳遞只要是按值傳遞也調用拷貝構造函數;
    c.函數返回只要是按值返回也調用拷貝構造函數。 

 四、拷貝構造函數 和 賦值運算符重載 爲什麼要使用引用? 
    
首先先說下基類 和 派生類的關係:
    例如:
        class Derived:public Base
        {
            public:
              .....
            private:
                .......
        };
 

 不同繼承方式的基類和派生類特性

繼承方式 基類特性 派生類特性
公有繼承 public public
protected
private
protected
不可訪問
私有繼承 public private
protected
private
private
不可訪問
保護繼承 public protected
protected
private
protected
不可訪問

        首先,派生類對象的引用初始化基類引用。多態性的動態綁定中存在兩個條件:1,必須是virtual 函數(虛函數);2, 必須是通過基類的引用或基類的指針進行成員函數的調用。
     
    由於派生類中存在基類的成員,也就相當於一個派生類對象中包含了一個基類對象,所以可以採用一個基類引用來綁定一個派生類對象。引用實質上是針對一塊內存區域,引用是一個標號,是這塊內存區域的一個名字,一個引用與一塊內存區域綁定,因爲派生對象中存在基類部分,可以認爲派生對象的區域中存在基類對象,這時可用基類的引用來表明這塊內存區域,即採用一個基類的別名來表示(綁定)這段內存區域,派生對象的地址(這段內存)以及內容都沒有發生改變,也沒有重現創造出一個新的對象,基類的引用還是指向這個派生對象。對於指針的分析方式相似。因此可以採用基類的引用綁定派生類對象。
   
    但是如何實現派生類對象到基類的轉換呢?
    這時候的轉換與前面的綁定存在很大的差別,因爲這是重新分配一個基類對象,而不再是引用問題,不再是綁定問題,是依據一個派生類對象生成一個新的基類對象。因爲派生類對象中存在一個基類對象基本的信息,完全可以生成一個基類對象,完全將此過程看作是一個初始化或者賦值的問題。也就是採用派生類創建一個新的對象或者賦值一個對象。
    從上面的分析我們可以採用下面的形式來實現:
 
    Base(const Derived &);
     
Base &operator=(const Derived &);
    是在基類函數中採用構造函數基於派生類來重載一系列的構造函數,但是這也存在一個問題,如果存在很多派生類,這時候就要重載很多構造函數,這肯定不是我們需要的。
 
    這時候我們發現對於一個類而言,爲什麼複製構造函數和重載賦值操作符這麼重要了。因爲這兩個函數都是接受一個基類的引用,根據前面的分析我們知道一個基類引用完全可以綁定一個派生類的對象,而派生類對象中又包含了一個基類對象的基本信息。我們能夠實現一個從一個派生對象到基類的構造過程。
    我們用一個基類引用綁定一個派生對象,然後採用基類引用對基類成員進行訪問,完成了一個基類對象基本要素的填充操作,相當於完成了基類對象的創建,也就是構造問題。這樣也就能完成由派生類對象到基類對象的構造過程。
 
    總結起來說了,因爲在複製構造函數中,C++中的基類引用可以綁定一個派生類的對象,如果在允許訪問的情況下,採用基類引用可以訪問基類的成員以及派生類的其他成員,採用引用可以複製派生類對象中基類成員的值到新創建的基類成員中,完成一個基類成員數據的填充操作,這時候一個完整的基類對象就創建完成了。
 
    重載賦值操作符則是發生在使用一個派生對象來賦值一個基類對象時,這時候也是const基類引用綁定一個派生類對象,然後複製對應的基類成員到基類對象對於的成員中,完成一個基類對象成員的更新操作。
 
    複製構造函數不僅僅實現了同類型之間的初始化操作,同時也完成了採用一個派生類對象初始化一個基類對象的操作,重載賦值操作符實現了同類型之間的賦值操作,也完成了採用派生類對象賦值基類對象的操作。如果沒有這兩個函數的存在,也就不能完成派生類到基類的賦值和初始化操作。這也是爲什麼一定會存在這兩個函數的原因。

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