C++中的拷贝构造函数

技术交流QQ群:1027579432,欢迎你的加入!

1.拷贝构造函数

  • 拷贝构造函数是一种特殊的构造函数它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
    • a.当用类的一个对象去初始化该类的另一个对象(或引用)时系统自动调用拷贝构造函数实现拷贝赋值
    • b.若函数的形参为类对象,调用函数时,实参赋值给形参,系统自动调用拷贝构造函数
    • c.当函数的返回值是类对象时,系统自动调用拷贝构造函数
  • 如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下:
        类名 (const 类名 &obj){  // obj 是一个对象引用,该对象是用于初始化另一个对象的
            拷贝构造函数的主体;
        }
    
  • 实例如下:
        #include "iostream"
    
        using namespace std;
    
    
        class Line{
            public:
                int getLength();
                Line(int len);  // 普通的构造函数
                Line(const Line &obj); // 拷贝构造函数
                ~Line();  // 析构函数
            private:
                int *ptr;
        };
        // 构造函数定义 
        Line::Line(int len){
            cout << "调用构造函数..." << endl;
            ptr = new int;  // 为指针分配内存
            *ptr = len;
        }
        // 拷贝构造函数定义
        Line::Line(const Line &obj){  // &obj是对象line的一个引用,用这个对象的引用来初始化另一个对象
            cout << "调用拷贝构造函数,并为指针ptr分配内存" << endl;
            ptr = new int;
            *ptr = *obj.ptr;  // 拷贝值
        }
        // 析构函数定义
        Line::~Line(){
            cout << "释放内存\n";
            delete ptr;
        }
        // 普通成员函数定义
        int Line::getLength(){
            return *ptr;
        }
    
        void display(Line obj){
            cout << "line大小: " << obj.getLength() << endl;
        }
    
    
        int main(){
            Line line(10);
            display(line);
            return 0;
        }
    
  • 下面的实例是对上面的例子进行修改,通过使用已有的同类型的对象来初始化新创建的对象:
        #include "iostream"
    
        using namespace std;
    
    
        class Line{
            public:
                int getLength();
                Line(int len);  // 普通的构造函数
                Line(const Line &obj); // 拷贝构造函数
                ~Line();  // 析构函数
            private:
                int *ptr;
        };
        // 构造函数定义 
        Line::Line(int len){
            cout << "调用构造函数..." << endl;
            ptr = new int;  // 为指针分配内存
            *ptr = len;
        }
        // 拷贝构造函数定义
        Line::Line(const Line &obj){  // &obj是对象line的一个引用,用这个对象的引用来初始化另一个对象
            cout << "调用拷贝构造函数,并为指针ptr分配内存" << endl;
            ptr = new int;
            *ptr = *obj.ptr;  // 拷贝值
        }
        // 析构函数定义
        Line::~Line(){
            cout << "释放内存\n";
            delete ptr;
        }
        // 普通成员函数定义
        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;  // 调用拷贝构造函数
            display(line2);
            return 0;
        }
    

2.什么是拷贝构造函数

  • 首先对普通类型的对象来说,它们之间的赋值是简单的,如:int a = 100;int b = a;而类的对象与普通对象是不同的,类的对象内部结构一般比较复杂,存在各种成员变量,下面是一个简单的类对象拷贝的例子:
        // 类的对象拷贝的简单例子
        class C{
            private:    
                int a;
            public:
            // 构造函数
                C(int b): a(b){
                    cout << "这是构造函数....\n";
                }
            // 拷贝构造函数:一种特殊的构造的函数,函数的名称必须与类名一样,它必须的一个参数是本类类型的一个引用变量
                C(const C &C_rename){  // 这里没有自定义拷贝构造函数的话,必须使用系统默认的拷贝构造函数
                    a = C_rename.a;
                    cout << "这个是拷贝构造函数......\n";
                }
                // 一般的成员函数
                void show(){
                    cout << "a = " << a << endl;
                }
        };
        C c1_obj(666);
        C c2_obj = c1_obj; // 等价于C c2_obj(c1_obj);
        c2_obj.show();
    

3.拷贝构造函数的调用时机

3.1 对象以值传递的方式传入作为函数参数

        // 1.对象以值传递的方式传入函数作为参数
        class D{
            private:
                int a;
            public:
                // 构造函数
                D(int b): a(b){
                    cout << "这是D的构造对象...\n";
                }
                // 拷贝构造函数
                D(const D& d_reference){
                    a = d_reference.a;
                    cout << "这是拷贝构造函数...\n";
                }
                // 析构函数
                ~D(){
                    cout << "这是析构函数,删除a = " << a << endl;
                } 
                // 普通成员函数
                void show(){
                    cout << "a = " << a << endl;
                } 
        };

        // 全局函数,传入的函数参数是D的某个对象
        void d_Fun(D d){
            cout << "test D\n";
        }
        D d_obj1(999);
        d_Fun(d_obj1);
  • 调用函数d_Fun()时,会产生以下几个重要的步骤:
    • 对象d_obj1传入d_Fun()的形参时,会先产生一个临时变量temp;
    • 然后调用拷贝构造函数会把d_obj1的值给temp,整个过程类似于D temp(d_obj1);
    • 等d_Fun()执行完后,析构掉temp对象

3.2 对象以值传递的方式从函数返回

       class E{
       private:
           int a;
       public:
           // 构造函数
           E(int b) : a(b){
               cout << "这是E的构造函数...\n";
           }
           // 拷贝构造函数
           E(const E& e_reference){
               a = e_reference.a;
               cout << "这是E的拷贝构造函数...\n";
           }
           // 析构函数
           ~E(){
               cout << "这是析构函数,删除a = " << a << endl;
           }
           // 普通成员函数
           void show(){
               cout << "a = " << a << endl;
           }
       };

       // 全局函数
       E e_fun(){
           E temp(3);
           return temp;
       }

       int main(){
           e_fun();
           return 0;
       }
  • 当调用e_fun()函数执行到return时,会产生几个重要的步骤:
    • 先产生一个临时变量xxx
    • 然后调用拷贝构造函数把temp的值给xxx,整个过程类似于E xxx(temp);
    • 在函数执行到最后先析构temp局部变量
    • 等e_fun()执行完后再析构掉xxx对象

3.3 对象需要通过另一个对象来初始化

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

4.浅拷贝与深拷贝

4.1 默认拷贝构造函数

  • 很多时候我们都不知道拷贝构造函数的情况下,传递对象参数或从函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是默认拷贝构造函数,这个构造函数很简单,仅仅使用老对象的数据成员的值来对新的对象的数据成员一一赋值,它具有以下的形式:
        Rect::Rect(const Rect& r){
            width = r.width;
            length = r.length;
        }
    
  • 当然,上面的代码不用我们自己写,可以由编译器自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了!看下面的代码:
        class Rect{
            public:
                Rect(){
                    count++;
                }
                ~Rect(){
                    count--;
                }
                static int getCount(){  // 静态成员函数
                    return count;
                }
            private:
                int width;
                int length;
                static int count;  // 静态成员变量count来计数
        };
        
        Rect rect1;
        cout << "The count of Rect: " << Rect::getCount() << endl;
    
        Rect rect2(rect1);  // 新的对象需要使用老的对象来进行初始化
        cout << "The count of Rect: " << Rect::getCount() << endl;
    
    错误的结果.png
  • 上面的代码对Rect类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数。按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。说白了,就是默认的拷贝构造函数没有处理静态数据成员。出现这些问题最根本就在于在复制对象时,计数器没有递增,我们重新编写拷贝构造函数,如下:
        class Rect{
            public:
                Rect(){
                    count++;
                }
                // 自定义拷贝构造函数
                Rect(const Rect& r){
                    width = r.width;
                    length = r.length;
                    count++;
                }
                ~Rect(){
                    count--;
                }
                static int getCount(){  // 静态成员函数
                    return count;
                }
            private:
                int width;
                int length;
                static int count;  // 静态成员变量count来计数
        };
    
        // 静态成员变量初始化
        int Rect::count = 0;
    
    正确的结果.png

4.2 浅拷贝

  • 所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码:
        // 浅拷贝
        class BB{
            public:
                BB(){  // 构造函数,p指向堆中分配的一空间
                    p = new int(100);
                }
                ~BB(){  // 析构函数,释放动态分配的空间
                    if(p!=NULL)
                        delete p;
                }
            private:
                int width;
                int length;
                int *p;  // 指针成员
        };
        BB rect1;
        BB rect2 = rect1;  // 复制对象
    
  • 在上面的代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:
      1. 在运行定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下:


        定义rect1对象后.png
      1. 在使用rect1复制rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间,如下图所示:


        rect1复制到rect2对象.png
    • 3.当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。

4.3 深拷贝

  • 在深拷贝的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:
        class DD{
        public:
            DD(){
                p = new int (100);
                cout << "这是DD的构造函数\n";
            }
            // 自定义默认拷贝构造函数
            DD (const DD& dd){
                width = dd.width;
                length = dd.length;
                p = new int;  // 为新对象重新动态分配空间
                *p = *(dd.p);
                cout << "这是DD的拷贝构造函数\n";
            }
            ~DD(){
                if (p!=NULL)
                    delete p;
                    cout << "这是DD的析构函数\n";
            }
        private:
            int width;
            int length;
            int *p;
        };
        DD rect1;
        DD rect2(rect1);
    
  • 此时,在完成对象的复制后,此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”,内存的一个大致情况如下:


    深拷贝.png

4.4 防止默认拷贝的发生

  • 通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递即声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。
        // 防止默认拷贝的发生
        class FF{
            private:
                int a;
            public:
                FF(int b): a(b){
                    cout << "这是FF的构造函数\n";
                }
            private:
                FF(const FF& ff){
                    a = ff.a;
                    cout << "这是FF的拷贝构造函数\n";
                }
            public:
                ~FF(){
                    cout << "这是FF的析构函数,删除a = " << a << endl;
                }
                // 普通成员函数
                void show(){
                    cout << "a = " << a << endl;
                }
        };
    
        // 全局函数
        void ff_fun(FF ff_param){
            cout << "test\n";
        }
        FF test(1);
        // ff_fun(test);  按值传递将会报错!
    

5.拷贝构造函数的几个细节知识

  • 拷贝构造函数里能调用private成员变量吗?
    • 拷贝构造函数其时就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受private的限制。
  • 以下函数哪个是拷贝构造函数,为什么?
        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); //当然也是拷贝构造函数
  • 一个类中可以存在多于一个的拷贝构造函数吗?
    • 类中可以存在超过一个拷贝构造函数
          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&),由编译器根据上下文决定选择哪一个。

参考文章

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容