C++中的拷貝構(gòu)造函數(shù)

技術(shù)交流QQ群:1027579432茫蛹,歡迎你的加入猖吴!

1.拷貝構(gòu)造函數(shù)

  • 拷貝構(gòu)造函數(shù)是一種特殊的構(gòu)造函數(shù)憔恳,它在創(chuàng)建對象時瓤荔,是使用同一類中之前創(chuàng)建的對象來初始化新創(chuàng)建的對象≡孔椋拷貝構(gòu)造函數(shù)通常用于:
    • a.當用類的一個對象去初始化該類的另一個對象(或引用)時系統(tǒng)自動調(diào)用拷貝構(gòu)造函數(shù)實現(xiàn)拷貝賦值
    • b.若函數(shù)的形參為類對象输硝,調(diào)用函數(shù)時,實參賦值給形參程梦,系統(tǒng)自動調(diào)用拷貝構(gòu)造函數(shù)
    • c.當函數(shù)的返回值是類對象時点把,系統(tǒng)自動調(diào)用拷貝構(gòu)造函數(shù)
  • 如果在類中沒有定義拷貝構(gòu)造函數(shù),編譯器會自行定義一個屿附。如果類帶有指針變量郎逃,并有動態(tài)內(nèi)存分配,則它必須有一個拷貝構(gòu)造函數(shù)挺份“玻拷貝構(gòu)造函數(shù)的最常見形式如下:
        類名 (const 類名 &obj){  // obj 是一個對象引用,該對象是用于初始化另一個對象的
            拷貝構(gòu)造函數(shù)的主體;
        }
    
  • 實例如下:
        #include "iostream"
    
        using namespace std;
    
    
        class Line{
            public:
                int getLength();
                Line(int len);  // 普通的構(gòu)造函數(shù)
                Line(const Line &obj); // 拷貝構(gòu)造函數(shù)
                ~Line();  // 析構(gòu)函數(shù)
            private:
                int *ptr;
        };
        // 構(gòu)造函數(shù)定義 
        Line::Line(int len){
            cout << "調(diào)用構(gòu)造函數(shù)..." << endl;
            ptr = new int;  // 為指針分配內(nèi)存
            *ptr = len;
        }
        // 拷貝構(gòu)造函數(shù)定義
        Line::Line(const Line &obj){  // &obj是對象line的一個引用匀泊,用這個對象的引用來初始化另一個對象
            cout << "調(diào)用拷貝構(gòu)造函數(shù)优训,并為指針ptr分配內(nèi)存" << endl;
            ptr = new int;
            *ptr = *obj.ptr;  // 拷貝值
        }
        // 析構(gòu)函數(shù)定義
        Line::~Line(){
            cout << "釋放內(nèi)存\n";
            delete ptr;
        }
        // 普通成員函數(shù)定義
        int Line::getLength(){
            return *ptr;
        }
    
        void display(Line obj){
            cout << "line大小: " << obj.getLength() << endl;
        }
    
    
        int main(){
            Line line(10);
            display(line);
            return 0;
        }
    
  • 下面的實例是對上面的例子進行修改,通過使用已有的同類型的對象來初始化新創(chuàng)建的對象:
        #include "iostream"
    
        using namespace std;
    
    
        class Line{
            public:
                int getLength();
                Line(int len);  // 普通的構(gòu)造函數(shù)
                Line(const Line &obj); // 拷貝構(gòu)造函數(shù)
                ~Line();  // 析構(gòu)函數(shù)
            private:
                int *ptr;
        };
        // 構(gòu)造函數(shù)定義 
        Line::Line(int len){
            cout << "調(diào)用構(gòu)造函數(shù)..." << endl;
            ptr = new int;  // 為指針分配內(nèi)存
            *ptr = len;
        }
        // 拷貝構(gòu)造函數(shù)定義
        Line::Line(const Line &obj){  // &obj是對象line的一個引用各聘,用這個對象的引用來初始化另一個對象
            cout << "調(diào)用拷貝構(gòu)造函數(shù)揣非,并為指針ptr分配內(nèi)存" << endl;
            ptr = new int;
            *ptr = *obj.ptr;  // 拷貝值
        }
        // 析構(gòu)函數(shù)定義
        Line::~Line(){
            cout << "釋放內(nèi)存\n";
            delete ptr;
        }
        // 普通成員函數(shù)定義
        int Line::getLength(){
            return *ptr;
        }
    
        void display(Line obj){
            cout << "line大小: " << obj.getLength() << endl;
        }
    
    
        int main(){
            Line line(10);
            display(line);
            cout << "-------------------------------\n";
            Line line2 = line;  // 調(diào)用拷貝構(gòu)造函數(shù)
            display(line2);
            return 0;
        }
    

2.什么是拷貝構(gòu)造函數(shù)

  • 首先對普通類型的對象來說,它們之間的賦值是簡單的伦吠,如:int a = 100;int b = a;而類的對象與普通對象是不同的妆兑,類的對象內(nèi)部結(jié)構(gòu)一般比較復雜魂拦,存在各種成員變量,下面是一個簡單的類對象拷貝的例子:
        // 類的對象拷貝的簡單例子
        class C{
            private:    
                int a;
            public:
            // 構(gòu)造函數(shù)
                C(int b): a(b){
                    cout << "這是構(gòu)造函數(shù)....\n";
                }
            // 拷貝構(gòu)造函數(shù):一種特殊的構(gòu)造的函數(shù)搁嗓,函數(shù)的名稱必須與類名一樣芯勘,它必須的一個參數(shù)是本類類型的一個引用變量
                C(const C &C_rename){  // 這里沒有自定義拷貝構(gòu)造函數(shù)的話,必須使用系統(tǒng)默認的拷貝構(gòu)造函數(shù)
                    a = C_rename.a;
                    cout << "這個是拷貝構(gòu)造函數(shù)......\n";
                }
                // 一般的成員函數(shù)
                void show(){
                    cout << "a = " << a << endl;
                }
        };
        C c1_obj(666);
        C c2_obj = c1_obj; // 等價于C c2_obj(c1_obj);
        c2_obj.show();
    

3.拷貝構(gòu)造函數(shù)的調(diào)用時機

3.1 對象以值傳遞的方式傳入作為函數(shù)參數(shù)

        // 1.對象以值傳遞的方式傳入函數(shù)作為參數(shù)
        class D{
            private:
                int a;
            public:
                // 構(gòu)造函數(shù)
                D(int b): a(b){
                    cout << "這是D的構(gòu)造對象...\n";
                }
                // 拷貝構(gòu)造函數(shù)
                D(const D& d_reference){
                    a = d_reference.a;
                    cout << "這是拷貝構(gòu)造函數(shù)...\n";
                }
                // 析構(gòu)函數(shù)
                ~D(){
                    cout << "這是析構(gòu)函數(shù),刪除a = " << a << endl;
                } 
                // 普通成員函數(shù)
                void show(){
                    cout << "a = " << a << endl;
                } 
        };

        // 全局函數(shù)腺逛,傳入的函數(shù)參數(shù)是D的某個對象
        void d_Fun(D d){
            cout << "test D\n";
        }
        D d_obj1(999);
        d_Fun(d_obj1);
  • 調(diào)用函數(shù)d_Fun()時荷愕,會產(chǎn)生以下幾個重要的步驟:
    • 對象d_obj1傳入d_Fun()的形參時,會先產(chǎn)生一個臨時變量temp;
    • 然后調(diào)用拷貝構(gòu)造函數(shù)會把d_obj1的值給temp,整個過程類似于D temp(d_obj1);
    • 等d_Fun()執(zhí)行完后棍矛,析構(gòu)掉temp對象

3.2 對象以值傳遞的方式從函數(shù)返回

       class E{
       private:
           int a;
       public:
           // 構(gòu)造函數(shù)
           E(int b) : a(b){
               cout << "這是E的構(gòu)造函數(shù)...\n";
           }
           // 拷貝構(gòu)造函數(shù)
           E(const E& e_reference){
               a = e_reference.a;
               cout << "這是E的拷貝構(gòu)造函數(shù)...\n";
           }
           // 析構(gòu)函數(shù)
           ~E(){
               cout << "這是析構(gòu)函數(shù),刪除a = " << a << endl;
           }
           // 普通成員函數(shù)
           void show(){
               cout << "a = " << a << endl;
           }
       };

       // 全局函數(shù)
       E e_fun(){
           E temp(3);
           return temp;
       }

       int main(){
           e_fun();
           return 0;
       }
  • 當調(diào)用e_fun()函數(shù)執(zhí)行到return時安疗,會產(chǎn)生幾個重要的步驟:
    • 先產(chǎn)生一個臨時變量xxx
    • 然后調(diào)用拷貝構(gòu)造函數(shù)把temp的值給xxx,整個過程類似于E xxx(temp);
    • 在函數(shù)執(zhí)行到最后先析構(gòu)temp局部變量
    • 等e_fun()執(zhí)行完后再析構(gòu)掉xxx對象

3.3 對象需要通過另一個對象來初始化

        C c_obj1(100);
        C c_obj2(c_obj1);  // 類似于C c_obj2 = c_obj1;

4.淺拷貝與深拷貝

4.1 默認拷貝構(gòu)造函數(shù)

  • 很多時候我們都不知道拷貝構(gòu)造函數(shù)的情況下,傳遞對象參數(shù)或從函數(shù)返回對象都能很好的進行够委,這是因為編譯器會給我們自動產(chǎn)生一個拷貝構(gòu)造函數(shù)荐类,這就是默認拷貝構(gòu)造函數(shù),這個構(gòu)造函數(shù)很簡單茁帽,僅僅使用老對象的數(shù)據(jù)成員的值來對新的對象的數(shù)據(jù)成員一一賦值玉罐,它具有以下的形式:
        Rect::Rect(const Rect& r){
            width = r.width;
            length = r.length;
        }
    
  • 當然,上面的代碼不用我們自己寫潘拨,可以由編譯器自動生成吊输。但是如果認為這樣就可以解決對象的復制問題,那就錯了铁追!看下面的代碼:
        class Rect{
            public:
                Rect(){
                    count++;
                }
                ~Rect(){
                    count--;
                }
                static int getCount(){  // 靜態(tài)成員函數(shù)
                    return count;
                }
            private:
                int width;
                int length;
                static int count;  // 靜態(tài)成員變量count來計數(shù)
        };
        
        Rect rect1;
        cout << "The count of Rect: " << Rect::getCount() << endl;
    
        Rect rect2(rect1);  // 新的對象需要使用老的對象來進行初始化
        cout << "The count of Rect: " << Rect::getCount() << endl;
    
    錯誤的結(jié)果.png
  • 上面的代碼對Rect類季蚂,加入了一個靜態(tài)成員,目的是進行計數(shù)琅束。在主函數(shù)中扭屁,首先創(chuàng)建對象rect1,輸出此時的對象個數(shù)涩禀,然后使用rect1復制出對象rect2疯搅,再輸出此時的對象個數(shù)。按照理解埋泵,此時應(yīng)該有兩個對象存在,但實際程序運行時罪治,輸出的都是1丽声,反應(yīng)出只有1個對象。此外觉义,在銷毀對象時雁社,由于會調(diào)用銷毀兩個對象,類的析構(gòu)函數(shù)會調(diào)用兩次晒骇,此時的計數(shù)器將變?yōu)樨摂?shù)霉撵。說白了磺浙,就是默認的拷貝構(gòu)造函數(shù)沒有處理靜態(tài)數(shù)據(jù)成員。出現(xiàn)這些問題最根本就在于在復制對象時徒坡,計數(shù)器沒有遞增撕氧,我們重新編寫拷貝構(gòu)造函數(shù),如下:
        class Rect{
            public:
                Rect(){
                    count++;
                }
                // 自定義拷貝構(gòu)造函數(shù)
                Rect(const Rect& r){
                    width = r.width;
                    length = r.length;
                    count++;
                }
                ~Rect(){
                    count--;
                }
                static int getCount(){  // 靜態(tài)成員函數(shù)
                    return count;
                }
            private:
                int width;
                int length;
                static int count;  // 靜態(tài)成員變量count來計數(shù)
        };
    
        // 靜態(tài)成員變量初始化
        int Rect::count = 0;
    
    正確的結(jié)果.png

4.2 淺拷貝

  • 所謂淺拷貝喇完,指的是在對象復制時伦泥,只對對象中的數(shù)據(jù)成員進行簡單的賦值,默認拷貝構(gòu)造函數(shù)執(zhí)行的也是淺拷貝锦溪。大多情況下“淺拷貝”已經(jīng)能很好地工作了不脯,但是一旦對象存在了動態(tài)成員,那么淺拷貝就會出問題了刻诊,讓我們考慮如下一段代碼:
        // 淺拷貝
        class BB{
            public:
                BB(){  // 構(gòu)造函數(shù)防楷,p指向堆中分配的一空間
                    p = new int(100);
                }
                ~BB(){  // 析構(gòu)函數(shù),釋放動態(tài)分配的空間
                    if(p!=NULL)
                        delete p;
                }
            private:
                int width;
                int length;
                int *p;  // 指針成員
        };
        BB rect1;
        BB rect2 = rect1;  // 復制對象
    
  • 在上面的代碼運行結(jié)束之前则涯,會出現(xiàn)一個運行錯誤复局。原因就在于在進行對象復制時,對于動態(tài)分配的內(nèi)容沒有進行正確的操作是整。我們來分析一下:
      1. 在運行定義rect1對象后肖揣,由于在構(gòu)造函數(shù)中有一個動態(tài)分配的語句,因此執(zhí)行后的內(nèi)存情況大致如下:


        定義rect1對象后.png
      1. 在使用rect1復制rect2時浮入,由于執(zhí)行的是淺拷貝龙优,只是將成員的值進行賦值,這時 rect1.p = rect2.p事秀,也即這兩個指針指向了堆里的同一個空間彤断,如下圖所示:


        rect1復制到rect2對象.png
    • 3.當然,這不是我們所期望的結(jié)果易迹,在銷毀對象時宰衙,兩個對象的析構(gòu)函數(shù)將對同一個內(nèi)存空間釋放兩次,這就是錯誤出現(xiàn)的原因睹欲。我們需要的不是兩個p有相同的值供炼,而是兩個p指向的空間有相同的值,解決辦法就是使用“深拷貝”窘疮。

4.3 深拷貝

  • 在深拷貝的情況下袋哼,對于對象中動態(tài)成員,就不能僅僅簡單地賦值了闸衫,而應(yīng)該重新動態(tài)分配空間涛贯,如上面的例子就應(yīng)該按照如下的方式進行處理:
        class DD{
        public:
            DD(){
                p = new int (100);
                cout << "這是DD的構(gòu)造函數(shù)\n";
            }
            // 自定義默認拷貝構(gòu)造函數(shù)
            DD (const DD& dd){
                width = dd.width;
                length = dd.length;
                p = new int;  // 為新對象重新動態(tài)分配空間
                *p = *(dd.p);
                cout << "這是DD的拷貝構(gòu)造函數(shù)\n";
            }
            ~DD(){
                if (p!=NULL)
                    delete p;
                    cout << "這是DD的析構(gòu)函數(shù)\n";
            }
        private:
            int width;
            int length;
            int *p;
        };
        DD rect1;
        DD rect2(rect1);
    
  • 此時,在完成對象的復制后蔚出,此時rect1的p和rect2的p各自指向一段內(nèi)存空間弟翘,但它們指向的空間具有相同的內(nèi)容虫腋,這就是所謂的“深拷貝”,內(nèi)存的一個大致情況如下:


    深拷貝.png

4.4 防止默認拷貝的發(fā)生

  • 通過對對象復制的分析,我們發(fā)現(xiàn)對象的復制大多在進行“值傳遞”時發(fā)生稀余,這里有一個小技巧可以防止按值傳遞即聲明一個私有拷貝構(gòu)造函數(shù)悦冀。甚至不必去定義這個拷貝構(gòu)造函數(shù),這樣因為拷貝構(gòu)造函數(shù)是私有的滚躯,如果用戶試圖按值傳遞或函數(shù)返回該類對象雏门,將得到一個編譯錯誤,從而可以避免按值傳遞或返回對象掸掏。
        // 防止默認拷貝的發(fā)生
        class FF{
            private:
                int a;
            public:
                FF(int b): a(b){
                    cout << "這是FF的構(gòu)造函數(shù)\n";
                }
            private:
                FF(const FF& ff){
                    a = ff.a;
                    cout << "這是FF的拷貝構(gòu)造函數(shù)\n";
                }
            public:
                ~FF(){
                    cout << "這是FF的析構(gòu)函數(shù),刪除a = " << a << endl;
                }
                // 普通成員函數(shù)
                void show(){
                    cout << "a = " << a << endl;
                }
        };
    
        // 全局函數(shù)
        void ff_fun(FF ff_param){
            cout << "test\n";
        }
        FF test(1);
        // ff_fun(test);  按值傳遞將會報錯!
    

5.拷貝構(gòu)造函數(shù)的幾個細節(jié)知識

  • 拷貝構(gòu)造函數(shù)里能調(diào)用private成員變量嗎?
    • 拷貝構(gòu)造函數(shù)其時就是一個特殊的構(gòu)造函數(shù)茁影,操作的還是自己類的成員變量,所以不受private的限制丧凤。
  • 以下函數(shù)哪個是拷貝構(gòu)造函數(shù),為什么?
        X::X(const X&);    
        X::X(X);    
        X::X(X&, int a=1);    
        X::X(X&, int a=1, int b=2);
    
    • 解釋:對于一個類X, 如果一個構(gòu)造函數(shù)的第一個參數(shù)是下列之一:
          a) X&
          b) const X&
          c) volatile X&
          d) const volatile X&
      
    且沒有其他參數(shù)或其他參數(shù)都有默認值,那么這個函數(shù)是拷貝構(gòu)造函數(shù).
    X::X(const X&); //是拷貝構(gòu)造函數(shù) X::X(X&, int=1); //是拷貝構(gòu)造函數(shù) X::X(X&, int a=1, int b=2); //當然也是拷貝構(gòu)造函數(shù)
  • 一個類中可以存在多于一個的拷貝構(gòu)造函數(shù)嗎?
    • 類中可以存在超過一個拷貝構(gòu)造函數(shù)
          class X { 
              public:       
              X(const X&);      // const 的拷貝構(gòu)造
              X(X&);            // 非const的拷貝構(gòu)造
              };
      
    • 注意,如果一個類中只存在一個參數(shù)為X&的拷貝構(gòu)造函數(shù),那么就不能使用const X或volatile X的對象實行拷貝初始化.
          class X{    
              public:
              X();    
              X(X&);
              };    
      
              const X cx;    
              X x = cx;    // error
      
    • 如果一個類中沒有定義拷貝構(gòu)造函數(shù),那么編譯器會自動產(chǎn)生一個默認的拷貝構(gòu)造函數(shù);這個默認的參數(shù)可能為 X::X(const X&)或 X::X(X&),由編譯器根據(jù)上下文決定選擇哪一個募闲。

參考文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市愿待,隨后出現(xiàn)的幾起案子浩螺,更是在濱河造成了極大的恐慌,老刑警劉巖仍侥,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件要出,死亡現(xiàn)場離奇詭異,居然都是意外死亡农渊,警方通過查閱死者的電腦和手機患蹂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砸紊,“玉大人传于,你說我怎么就攤上這事∽硗纾” “怎么了沼溜?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長游添。 經(jīng)常有香客問我系草,道長,這世上最難降的妖魔是什么唆涝? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任悄但,我火速辦了婚禮,結(jié)果婚禮上石抡,老公的妹妹穿的比我還像新娘。我一直安慰自己助泽,他們只是感情好啰扛,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布嚎京。 她就那樣靜靜地躺著,像睡著了一般隐解。 火紅的嫁衣襯著肌膚如雪鞍帝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天煞茫,我揣著相機與錄音帕涌,去河邊找鬼。 笑死续徽,一個胖子當著我的面吹牛蚓曼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播钦扭,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼纫版,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了客情?” 一聲冷哼從身側(cè)響起其弊,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎膀斋,沒想到半個月后梭伐,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡仰担,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年糊识,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惰匙。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡技掏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出项鬼,到底是詐尸還是另有隱情哑梳,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布绘盟,位于F島的核電站鸠真,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏龄毡。R本人自食惡果不足惜吠卷,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沦零。 院中可真熱鬧祭隔,春花似錦、人聲如沸路操。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至搞坝,卻和暖如春搔谴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背桩撮。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工敦第, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人店量。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓芜果,卻偏偏與公主長得像,于是被迫代替她去往敵國和親垫桂。 傳聞我的和親對象是個殘疾皇子师幕,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容