C++拷貝構造函數詳解

一. 什麼是拷貝構造函數ios

首先對於普通類型的對象來講,它們之間的複製是很簡單的,例如:面試

 

int a = 100;  
int b = a;

 

而類對象與普通對象不一樣,類對象內部結構通常較爲複雜,存在各類成員變量。
下面看一個類對象拷貝的簡單例子。數組

#include <iostream>  
using namespace std;  
  
class CExample {  
private:  
     int a;  
public:  
      //構造函數  
     CExample(int b)  
     { a = b;}  
  
      //通常函數  
     void Show ()  
     {  
        cout<<a<<endl;  
      }  
};  
  
int main()  
{  
     CExample A(100);  
     CExample B = A; //注意這裏的對象初始化要調用拷貝構造函數,而非賦值  
      B.Show ();  
     return 0;  
} 

運行程序,屏幕輸出100。從以上代碼的運行結果能夠看出,系統爲對象 B 分配了內存並完成了與對象 A 的複製過程。就類對象而言,相同類型的類對象是經過拷貝構造函數來完成整個複製過程的。

下面舉例說明拷貝構造函數的工做過程。函數

#include <iostream>  
using namespace std;  
  
class CExample {  
private:  
    int a;  
public:  
    //構造函數  
    CExample(int b)  
    { a = b;}  
      
    //拷貝構造函數  
    CExample(const CExample& C)  
    {  
        a = C.a;  
    }  
  
    //通常函數  
    void Show ()  
    {  
        cout<<a<<endl;  
    }  
};  
  
int main()  
{  
    CExample A(100);  
    CExample B = A; // CExample B(A); 也是同樣的  
     B.Show ();  
    return 0;  
}   

CExample(const CExample& C) 就是咱們自定義的拷貝構造函數。可見,拷貝構造函數是一種特殊的構造函數,函數的名稱必須和類名稱一致,它必須的一個參數是本類型的一個引用變量。this

二. 拷貝構造函數的調用時機spa

在C++中,下面三種對象須要調用拷貝構造函數!
1. 對象以值傳遞的方式傳入函數參數3d

class CExample   
{  
private:  
 int a;  
  
public:  
 //構造函數  
 CExample(int b)  
 {   
  a = b;  
  cout<<"creat: "<<a<<endl;  
 }  
  
 //拷貝構造  
 CExample(const CExample& C)  
 {  
  a = C.a;  
  cout<<"copy"<<endl;  
 }  
   
 //析構函數  
 ~CExample()  
 {  
  cout<< "delete: "<<a<<endl;  
 }  
  
     void Show ()  
 {  
         cout<<a<<endl;  
     }  
};  
  
//全局函數,傳入的是對象  
void g_Fun(CExample C)  
{  
 cout<<"test"<<endl;  
}  
  
int main()  
{  
 CExample test(1);  
 //傳入對象  
 g_Fun(test);  
  
 return 0;  
}

調用g_Fun()時,會產生如下幾個重要步驟:
(1).test對象傳入形參時,會先會產生一個臨時變量,就叫 C 吧。
(2).而後調用拷貝構造函數把test的值給C。 整個這兩個步驟有點像:CExample C(test);
(3).等g_Fun()執行完後, 析構掉 C 對象。

2. 對象以值傳遞的方式從函數返回指針

 

class CExample   
{  
private:  
 int a;  
  
public:  
 //構造函數  
 CExample(int b)  
 {   
  a = b;  
 }  
  
 //拷貝構造  
 CExample(const CExample& C)  
 {  
  a = C.a;  
  cout<<"copy"<<endl;  
 }  
  
     void Show ()  
     {  
         cout<<a<<endl;  
     }  
};  
  
//全局函數  
CExample g_Fun()  
{  
 CExample temp(0);  
 return temp;  
}  
  
int main()  
{  
 g_Fun();  
 return 0;  
} 

當g_Fun()函數執行到return時,會產生如下幾個重要步驟:
(1). 先會產生一個臨時變量,就叫XXXX吧。
(2). 而後調用拷貝構造函數把temp的值給XXXX。整個這兩個步驟有點像:CExample XXXX(temp);
(3). 在函數執行到最後先析構temp局部變量。
(4). 等g_Fun()執行完後再析構掉XXXX對象。code

3. 對象須要經過另一個對象進行初始化;對象

CExample A(100);  
CExample B = A;   
// CExample B(A);

後兩句都會調用拷貝構造函數。


三. 淺拷貝和深拷貝
1. 默認拷貝構造函數

不少時候在咱們都不知道拷貝構造函數的狀況下,傳遞對象給函數參數或者函數返回對象都能很好的進行,這是由於編譯器會給咱們自動產生一個拷貝構造函數,這就是「默認拷貝構造函數」,這個構造函數很簡單,僅僅使用「老對象」的數據成員的值對「新對象」的數據成員一一進行賦值,它通常具備如下形式:

Rect::Rect(const Rect& r)  
{  
    width = r.width;  
    height = r.height;  
} 

固然,以上代碼不用咱們編寫,編譯器會爲咱們自動生成。可是若是認爲這樣就能夠解決對象的複製問題,那就錯了,讓咱們來考慮如下一段代碼:

class Rect  
{  
public:  
    Rect()      // 構造函數,計數器加1  
    {  
        count++;  
    }  
    ~Rect()     // 析構函數,計數器減1  
    {  
        count--;  
    }  
    static int getCount()       // 返回計數器的值  
    {  
        return count;  
    }  
private:  
    int width;  
    int height;  
    static int count;       // 一靜態成員作爲計數器  
};  
  
int Rect::count = 0;        // 初始化計數器  
  
int main()  
{  
    Rect rect1;  
    cout<<"The count of Rect: "<<Rect::getCount()<<endl;  
  
    Rect rect2(rect1);   // 使用rect1複製rect2,此時應該有兩個對象  
     cout<<"The count of Rect: "<<Rect::getCount()<<endl;  
  
    return 0;  
} 

這段代碼對前面的類,加入了一個靜態成員,目的是進行計數。在主函數中,首先建立對象rect1,輸出此時的對象個數,而後使用rect1複製出對象rect2,再輸出此時的對象個數,按照理解,此時應該有兩個對象存在,但實際程序運行時,輸出的都是1,反應出只有1個對象。此外,在銷燬對象時,因爲會調用銷燬兩個對象,類的析構函數會調用兩次,此時的計數器將變爲負數。

說白了,就是拷貝構造函數沒有處理靜態數據成員。

出現這些問題最根本就在於在複製對象時,計數器沒有遞增,咱們從新編寫拷貝構造函數,以下:

class Rect  
{  
public:  
    Rect()      // 構造函數,計數器加1  
    {  
        count++;  
    }  
    Rect(const Rect& r)   // 拷貝構造函數  
    {  
        width = r.width;  
        height = r.height;  
        count++;          // 計數器加1  
    }  
    ~Rect()     // 析構函數,計數器減1  
    {  
        count--;  
    }  
    static int getCount()   // 返回計數器的值  
    {  
        return count;  
    }  
private:  
    int width;  
    int height;  
    static int count;       // 一靜態成員作爲計數器  
};

2. 淺拷貝

所謂淺拷貝,指的是在對象複製時,只對對象中的數據成員進行簡單的賦值,默認拷貝構造函數執行的也是淺拷貝。大多狀況下「淺拷貝」已經能很好地工做了,可是一旦對象存在了動態成員,那麼淺拷貝就會出問題了,讓咱們考慮以下一段代碼:

class Rect  
{  
public:  
    Rect()      // 構造函數,p指向堆中分配的一空間  
    {  
        p = new int(100);  
    }  
    ~Rect()     // 析構函數,釋放動態分配的空間  
    {  
        if(p != NULL)  
        {  
            delete p;  
        }  
    }  
private:  
    int width;  
    int height;  
    int *p;     // 一指針成員  
};  
  
int main()  
{  
    Rect rect1;  
    Rect rect2(rect1);   // 複製對象  
    return 0;  
}

在這段代碼運行結束以前,會出現一個運行錯誤。緣由就在於在進行對象複製時,對於動態分配的內容沒有進行正確的操做。咱們來分析一下:

在運行定義rect1對象後,因爲在構造函數中有一個動態分配的語句,所以執行後的內存狀況大體以下:

在使用rect1複製rect2時,因爲執行的是淺拷貝,只是將成員的值進行賦值,這時 rect1.p = rect2.p,也即這兩個指針指向了堆裏的同一個空間,以下圖所示:

 

固然,這不是咱們所指望的結果,在銷燬對象時,兩個對象的析構函數將對同一個內存空間釋放兩次,這就是錯誤出現的緣由。咱們須要的不是兩個p有相同的值,而是兩個p指向的空間有相同的值,解決辦法就是使用「深拷貝」。

 



3. 深拷貝

在「深拷貝」的狀況下,對於對象中動態成員,就不能僅僅簡單地賦值了,而應該從新動態分配空間,如上面的例子就應該按照以下的方式進行處理:

 

class Rect  
{  
public:  
    Rect()      // 構造函數,p指向堆中分配的一空間  
    {  
        p = new int(100);  
    }  
    Rect(const Rect& r)  
    {  
        width = r.width;  
        height = r.height;  
        p = new int;    // 爲新對象從新動態分配空間  
        *p = *(r.p);  
    }  
    ~Rect()     // 析構函數,釋放動態分配的空間  
    {  
        if(p != NULL)  
        {  
            delete p;  
        }  
    }  
private:  
    int width;  
    int height;  
    int *p;     // 一指針成員  
};  

此時,在完成對象的複製後,內存的一個大體狀況以下:

 此時rect1的p和rect2的p各自指向一段內存空間,但它們指向的空間具備相同的內容,這就是所謂的「深拷貝」。

此時rect1的p和rect2的p各自指向一段內存空間,但它們指向的空間具備相同的內容,這就是所謂的「深拷貝」。

3. 防止默認拷貝發生

經過對對象複製的分析,咱們發現對象的複製大多在進行「值傳遞」時發生,這裏有一個小技巧能夠防止按值傳遞——聲明一個私有拷貝構造函數。甚至沒必要去定義這個拷貝構造函數,這樣由於拷貝構造函數是私有的,若是用戶試圖按值傳遞或函數返回該類對象,將獲得一個編譯錯誤,從而能夠避免按值傳遞或返回對象。

// 防止按值傳遞  
class CExample   
{  
private:  
    int a;  
  
public:  
    //構造函數  
    CExample(int b)  
    {   
        a = b;  
        cout<<"creat: "<<a<<endl;  
    }  
  
private:  
    //拷貝構造,只是聲明  
    CExample(const CExample& C);  
  
public:  
    ~CExample()  
    {  
        cout<< "delete: "<<a<<endl;  
    }  
  
    void Show ()  
    {  
        cout<<a<<endl;  
    }  
};  
  
//全局函數  
void g_Fun(CExample C)  
{  
    cout<<"test"<<endl;  
}  
  
int main()  
{  
    CExample test(1);  
    //g_Fun(test); 按值傳遞將出錯  
      
    return 0;  
}

四. 拷貝構造函數的幾個細節

1. 拷貝構造函數裏能調用private成員變量嗎?
解答:這個問題是在網上見的,當時一會兒有點暈。其時從名子咱們就知道拷貝構造函數其時就是一個特殊的構造函數,操做的仍是本身類的成員變量,因此不受private的限制。



2. 如下函數哪一個是拷貝構造函數,爲何?

X::X(const X&);      
X::X(X);      
X::X(X&, int a=1);      
X::X(X&, int a=1, int b=2); 

解答:對於一個類X, 若是一個構造函數的第一個參數是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且沒有其餘參數或其餘參數都有默認值,那麼這個函數是拷貝構造函數.

X::X(const X&);  //是拷貝構造函數      
X::X(X&, int=1); //是拷貝構造函數     
X::X(X&, int a=1, int b=2); //固然也是拷貝構造函數  

3. 一個類中能夠存在多於一個的拷貝構造函數嗎?
解答:類中能夠存在超過一個拷貝構造函數。

class X {   
public:         
  X(const X&);      // const 的拷貝構造  
  X(X&);            // 非const的拷貝構造  
};

注意,若是一個類中只存在一個參數爲 X& 的拷貝構造函數,那麼就不能使用const X或volatile X的對象實行拷貝初始化.

 

class X {      
public:  
  X();      
  X(X&);  
};      
  
const X cx;      
X x = cx;    // error 

若是一個類中沒有定義拷貝構造函數,那麼編譯器會自動產生一個默認的拷貝構造函數。
這個默認的參數可能爲 X::X(const X&)或 X::X(X&),由編譯器根據上下文決定選擇哪個。

 

1. 深拷貝和淺拷貝(拷貝構造函數的使用)

 

 

有時候須要本身定義拷貝構造函數,以免淺拷貝問題。

在什麼狀況下須要用戶本身定義拷貝構造函數:

通常狀況下,當類中成員有指針變量、類中有動態內存分配時經常須要用戶本身定義拷貝構造函數。

 

在什麼狀況下系統會調用拷貝構造函數:(三種狀況)

(1)用類的一個對象去初始化另外一個對象時

(2)當函數的形參是類的對象時(也就是值傳遞時),若是是引用傳遞則不會調用

(3)當函數的返回值是類的對象或引用時

 

簡單示例:

#include <iostream>  
using namespace std;  
  
class A  
{  
private:  
    int a;  
public:  
    A(int i){a=i;}  //內聯的構造函數  
    A(A &aa);  
    int geta(){return a;}  
};  
  
A::A(A &aa)     //拷貝構造函數  
{  
    a=aa.a;  
    cout<<"拷貝構造函數執行!"<<endl;  
}  
  
int get_a(A aa)     //參數是對象,是值傳遞,會調用拷貝構造函數  
{  
    return aa.geta();  
}  
  
int get_a_1(A &aa)  //若是參數是引用類型,自己就是引用傳遞,因此不會調用拷貝構造函數  
{  
    return aa.geta();  
}  
  
A get_A()       //返回值是對象類型,會調用拷貝構造函數。會調用拷貝構造函數,由於函數體內生成的對象aa是臨時的,離開這個函數就消失了。全部會調用拷貝構造函數複製一份。  
{  
    A aa(1);  
    return aa;  
}  
  
A& get_A_1()    //會調用拷貝構造函數,由於函數體內生成的對象aa是臨時的,離開這個函數就消失了。全部會調用拷貝構造函數複製一份。  
{  
    A aa(1);  
    return aa;  
}  
  
int _tmain(int argc, _TCHAR* argv[])  
{  
    A a1(1);  
    A b1(a1);           //用a1初始化b1,調用拷貝構造函數  
    A c1=a1;            //用a1初始化c1,調用拷貝構造函數  
  
    int i=get_a(a1);        //函數形參是類的對象,調用拷貝構造函數  
    int j=get_a_1(a1);      //函數形參類型是引用,不調用拷貝構造函數  
  
    A d1=get_A();       //調用拷貝構造函數  
    A e1=get_A_1();     //調用拷貝構造函數  
  
    return 0;  
}  

附:一個面試試題

修改下面程序中的錯誤:

#include <iostream>  
using namespace std;  
  
class NameStr  
{  
private:  
    char *m_pName;  
    char *m_pData;  
public:  
    NameStr()  
    {  
        static const char s_szDefaultName[]="Default name";  
        static const char s_szDefaultStr[]="Default string";  
        strcpy(m_pName,s_szDefaultName);  
        strcpy(m_pData,s_szDefaultStr);  
    }  
    ~NamedStr(){}  
    NameStr(const char* pName,const char* pData)  
    {  
        m_pData=new char[strlen(pData)];  
        m_pName=new char[strlen(pData)];  
    }  
  
    void Print()  
    {  
        cout<<"Name:"<<m_pName<<endl;  
        cout<<"String:"<<m_pData<<endl;  
    }  
};  
  
int _tmain(int argc, _TCHAR* argv[])  
{  
    NameStr* pDefNss=NULL;  
  
    pDefNss=new NameStr[10];  
    NameStr ns("hello","world");  
  
    delete pDefNss;  
  
    return 0;  
}  

分析:

1. 第1四、15行,strcpy(m_pName,s_szDefaultName) 對未分配內存空間的字符指針賦值會出現異常。

2. 第20行、21行,m_pData=new char[strlen(pData)] 應該爲m_pData=new char[strlen(pData)+1] ,而且應該爲最後一個字符賦值爲'\0'。

3. 析構函數中,應該處理字符指針內存空間的釋放。

4. 由於類的成員變量中有指針變量,所以應該編寫類的拷貝構造函數和賦值函數,防止淺拷貝。

5. pDefNss是一個對象數組,delete時應該是delete [ ]pDefNss。

#include <iostream>  
using namespace std;  
  
//NameStr類的聲明  
class NameStr  
{  
private:  
    char *m_pName;  
    char *m_pData;  
public:  
    NameStr();      //默認拷貝構造函數  
  
    ~NameStr(); //析構函數聲明  
  
    NameStr(const char* pName,const char* pData);   //帶參構造函數的聲明  
  
    NameStr(const NameStr& temp);   //拷貝構造函數的聲明  
  
    NameStr& operator= (const NameStr& temp);   //重載=運算符  
  
    void Print();   //輸出對象內容  
};  
  
//默認構造函數的實現  
NameStr::NameStr()    
{  
    static const char s_szDefaultName[]="Default name";  
    static const char s_szDefaultStr[]="Default string";  
  
    m_pData=new char[strlen(s_szDefaultStr)+1];     //不能爲爲分配內存空間的字符指針賦值  
    m_pName=new char[strlen(s_szDefaultName)+1];  
  
    strcpy(m_pName,s_szDefaultName);        //更規範的方式是使用strncpy函數進行拷貝  
    m_pName[strlen(s_szDefaultName)]='\0';  
    strcpy(m_pData,s_szDefaultStr);  
    m_pData[strlen(s_szDefaultStr)]='\0';  
}  
  
//析構函數的實現  
NameStr::~NameStr()  
{  
    delete []m_pData;  
    delete []m_pName;  
}  
  
//帶參構造函數的實現  
NameStr::NameStr(const char* pName,const char* pData)  
{  
    m_pData=new char[strlen(pData)+1];      //開闢內存空間  
    m_pName=new char[strlen(pName)+1];  
  
    strcpy(m_pData,pData);  
    m_pData[strlen(pData)]='\0';  
    strcpy(m_pName,pName);  
    m_pName[strlen(pName)]='\0';  
}  
  
//拷貝構造函數的實現  
NameStr::NameStr(const NameStr& temp)  
{  
    m_pData=new char[strlen(temp.m_pData)+1];         
    m_pName=new char[strlen(temp.m_pName)+1];  
  
    strcpy(m_pData,temp.m_pData);  
    m_pData[strlen(temp.m_pData)]='\0';  
    strcpy(m_pName,temp.m_pName);  
    m_pName[strlen(temp.m_pName)]='\0';  
}  
  
//重載=運算符的實現  
NameStr& NameStr::operator=(const NameStr& temp)      
{  
    //首先要進行檢查,防止自身複製  
    if(&temp==this) //this是一個指針,表示本對象的地址。&temp是temp對象的指針。  
    {  
        return *this;  
    }  
  
    //釋放原有的內存空間  
    delete []m_pData;  
    delete []m_pName;  
  
    //分配新的內存空間  
    m_pData=new char[strlen(temp.m_pData)+1];         
    m_pName=new char[strlen(temp.m_pName)+1];  
  
    //進行拷貝  
    strcpy(m_pData,temp.m_pData);  
    m_pData[strlen(temp.m_pData)]='\0';  
    strcpy(m_pName,temp.m_pName);  
    m_pName[strlen(temp.m_pName)]='\0';  
  
    //返回本對象的引用  
    return *this;  
}  
  
inline void NameStr::Print()  
{  
    cout<<"Name:"<<m_pName<<endl;  
    cout<<"String:"<<m_pData<<endl;  
}  
  
//程序入口  
int _tmain(int argc, _TCHAR* argv[])  
{  
    NameStr* pDefNss=NULL;  
  
    pDefNss=new NameStr[3];  
    NameStr ns("hello","world");  
  
    delete []pDefNss;  
  
    NameStr ns1=ns;  
  
    return 0;  
}  
相關文章
相關標籤/搜索