美文网首页
C++中的拷贝构造函数

C++中的拷贝构造函数

作者: CurryCoder | 来源:发表于2021-01-10 00:15 被阅读0次

    技术交流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&),由编译器根据上下文决定选择哪一个。

    参考文章

    相关文章

      网友评论

          本文标题:C++中的拷贝构造函数

          本文链接:https://www.haomeiwen.com/subject/vhzhaktx.html