C++拷贝构造

一、C++中调用拷贝构造函数的情形有:
1、类对象的按值初始化,即:用已有的类对象本身去初始化新的类对象。(注意,只有在用已有类对象本身去初始化新的类对象时,才会调用拷贝构造函数。当对象A已被创建,再用对象B给对象A赋值,就不会调用拷贝构造函数,因为这个过程已不是初始化的过程,而是普通的赋值过程。);
2、类对象的按值传参,即函数的形参为类对象(而不是指针或引用);
3、类对象的按值返回,即函数的返回值直接为类对象(而不是指针或引用)。
代码示例(类和函数的定义):

  #include<iostream>
  using namespace std;

  class Scientist
  {
  public:
      int age;
      int* soul;
  public:
      Scientist(int hisAge, int* hisSoul)
      {
          cout << "The constructor is called !" << endl;
          soul = (int*)malloc(sizeof(hisSoul) + 1);
          *soul = *hisSoul;
          age = hisAge;
      }
      //自定义一个拷贝构造函数
      Scientist(const Scientist& myScientist)
      {
          cout << "The deep-copy constructor is called !" << endl;
          soul = (int*)malloc(sizeof(myScientist.soul) + 1);
          *soul = *(myScientist.soul);
          age = myScientist.age;
      }
      ~Scientist()
      {
          cout << "The destructor is called !" << endl;
          if (soul != NULL)
          {
              free(soul);
              soul = NULL;
          }
      }
  };

  void displaySoul(const Scientist myScientist)
  {
      cout << "His soul is : " << myScientist.soul << endl;
  }
  const Scientist getScientist_1(const Scientist& myScientist)
  {
      return myScientist;
  }
  const Scientist getScientist_2(const Scientist myScientist)
  {
      return myScientist;
  }

情形1:类对象的按值初始化

  int main(int argc, char* argv[])
  {
      int hisSoul = 10000;
      Scientist scientistA(22,&hisSoul);
      Scientist scientistB(scientistA);
  }
  /*
  打印结果:
  The constructor is called !
  The deep-copy constructor is called !
  The destructor is called !
  The destructor is called !
  */

情形1的main函数中,语句Scientist scientistB(const Scientist& )表明,scientistB是用scientistA按值初始化的。打印结果表明,按值初始化类对象会导致拷贝构造函数的调用。
情形2:类对象的按值传参

int main(int argc, char* argv[])
{
    int hisSoul = 10000;
    Scientist scientistA(22,&hisSoul);
    displaySoul(scientistA);
}
/*
  打印结果:
  The constructor is called !
  The deep-copy constructor is called !
  His soul is : 012D12F8
  The destructor is called !
  The destructor is called !
  */

情形2的main函数调用了void displaySoul(const Scientist )函数,该函数的形参列表为Scientist类型,表明为按值传递类对象参数。打印结果表明,类对象的按值传参会导致拷贝构造函数的调用。
情形3:类对象的按值返回

  int main(int argc, char* argv[])
  {
      int hisSoul = 10000;
      Scientist scientistA(22,&hisSoul);
      getScientist_1(scientistA);
  }
/*
  打印结果:
  The constructor is called !
  The deep-copy constructor is called !
  The destructor is called !
  The destructor is called !
  */

情形3的main函数调用了const Scientist getScientist_1(const Scientist& )函数,该函数按值返回类对象scientistA。打印结果表明,类对象的按值返回会导致拷贝构造函数的调用。
补充示例:

  int main(int argc, char* argv[])
  {
      int hisSoul = 10000;
      Scientist scientistA(22,&hisSoul);
      getScientist_2(scientistA);
  }
/*
  打印结果:
  The constructor is called !
  The deep-copy constructor is called !
  The deep-copy constructor is called !
  The destructor is called !
  The destructor is called !
  The destructor is called !
  */

补充示例中的main函数调用了const Scientist getScientist_1(const Scientist)函数,该函数既按值传递Scientist类对象,又按值返回Scientist类对象,因此对它的调用会引起拷贝构造函数的2次调用。打印结果正是如此。

二、浅拷贝与深拷贝
上述示例已经展示了拷贝构造函数被调用的三种情形。然而,拷贝构造又有浅拷贝和深拷贝之分,上述示例使用的为深拷贝。简单而言,拷贝者若与被拷贝者指向同一内存地址,则为浅拷贝,否则为深拷贝。在对象属性中包含指向堆区内存的数据成员时(即用new关键字或malloc函数创建的数据成员),浅拷贝会导致堆区内存的重复释放,以致程序运行崩溃。
1、错误代码示例(此段代码运行会导致程序崩溃!!!)

  #define _CRT_SECURE_NO_WARNINGS//防止遇到strcpy函数而报错
  #include<iostream>
  using namespace std;

  class Person
  {
  private:
      char* name;
  public:
      Person(char* hisName)
      {
          cout << "The constructor is called !" << endl;
          //注意数组对象的new创建方法
          name = new char[strlen(hisName)+1];
          strcpy(name, hisName);//将hisName复制给name
      }
      ~Person()
      {
          cout << "The destructor is called !" << endl;
          if (name != NULL)
          {
              //注意数组的delete销毁方法
              delete[] name;
              name = NULL;//防止野指针
          }
      }
      inline const char* getName()
      {
          return name;
      }
  };

  int main(int argc, char* argv[])
  {
      Person personA((char*)("PhaseLee"));
      cout << personA.getName()<<endl;
      /*
      注意:personA为临时变量。进入此条代码之前,personA生命周期已结束,
      因此已被释放,导致personA.name已被释放!!!
      此语句发生了浅拷贝:即调用默认拷贝构造函数,也就是简单的按值拷贝,并未
      对name属性开辟新的堆区内存,personB.name和personA.name指向相同的内存地址。
      */
      Person personB(personA);
      cout << personB.getName() << endl;
      /*
      此步执行完后,临时变量personB被释放,会导致personB.name指向的堆区内
      存被释放。而personB是用personA按值初始化的,调用了默认拷贝构造函数,
  导致personB.name与personA.name指向同一片堆区。此前,personA.name已被释
      放,则personB的释放会导致该堆区被重复释放,从而导致程序崩溃。
      */
      return 0;
  }

在上述的基础上,注释掉“delete[] name;”语句,程序会正常运行,只不过name属性的内存不会被释放。
2、注释掉“delete[] name;”,并打印出personA.name和personB.name的地址为:

  #define _CRT_SECURE_NO_WARNINGS//防止遇到strcpy函数而报错
  #include<iostream>
  #include<cstdio>

  using namespace std;
  class Person
  {
  private:
      char* name;
  public:
      Person(char* hisName)
      {
          cout << "The constructor is called !" << endl;
          name = new char[strlen(hisName)+1];
          strcpy(name, hisName);
      }
      ~Person()
      {
          cout << "The destructor is called !" << endl;
          if (name != NULL)
          {
              //delete[] name;
              name = NULL;//防止野指针
          }
      }
      inline const char* getName()
      {
          return name;
      }
  };

  int main(int argc, char* argv[])
  {
      Person personA((char*)("PhaseLee"));
      cout << personA.getName()<<endl;
      Person personB(personA);
      cout << personB.getName() << endl;
  //这里使用printf打印是因为cout对[]运算符进行了重载
      printf("The address of personA.name is %d\n", &(personA.getName()[0]));
      printf("The address of personB.name is %d\n", &(personB.getName()[0]));
      return 0;
  }
  /*
  输出结果:
  The constructor is called !
  PhaseLee
  PhaseLee
  The address of personA.name is 19862144
  The address of personB.name is 19862144
  The destructor is called !
  The destructor is called !
  */

上述打印结果表明,通过浅拷贝,personA.name与personB.name确实指向相同的内存地址。
3、解决浅拷贝的方法就是深拷贝,即避免默认拷贝构造函数,需要自己提供拷贝构造函数。自己提供了拷贝构造函数的代码示例如下:

  #define _CRT_SECURE_NO_WARNINGS//防止遇到strcpy函数而报错
  #include<iostream>
  #include<cstdio>
  using namespace std;

  class Person
  {
  private:
      char* name;
  public:
      Person(char* hisName)
      {
          cout << "The constructor is called !" << endl;
          //注意数组对象的new创建方法
          name = new char[strlen(hisName)+1];
          strcpy(name, hisName);//将hisName复制给name
      }
      //自定义拷贝构造函数
      Person(const Person& myPerson)
      {
          //开辟新的堆区空间
          cout << "The deep-copy constructor is called !" << endl;
          name = new char[strlen(myPerson.name) + 1];
          strcpy(name, myPerson.name);
      }
      ~Person()
      {
          cout << "The destructor is called !" << endl;
          if (name != NULL)
          {
              //注意数组的delete销毁方法
              delete[] name;
              name = NULL;//防止野指针
          }
      }
      inline const char* getName()
      {
          return name;
      }
  };

  int main(int argc, char* argv[])
  {
      Person personA((char*)("PhaseLee"));
      Person personB(personA);
      printf("The address of personA.name is %d\n", &(personA.getName()[0]));
      printf("The address of personB.name is %d\n", &(personB.getName()[0]));
      return 0;
  }
  /*
  打印结果:
  The constructor is called !
  The deep-copy constructor is called !
  The address of personA.name is 21172424
  The address of personB.name is 21173768
  The destructor is called !
  The destructor is called !
  */

自定义拷贝构造函数需要按引用传参。在上述代码中的自定义拷贝构造函数中,为name属性分配了新的堆区空间,因此personA.name和personB.name不再指向同一片堆区内存,释放personA和释放personB对应的name属性的释放是对不同的堆区内存进行释放,不会导致堆区内存的重复释放。

结论:默认拷贝构造函数往往是被隐式调用的,而且默认拷贝构造函数的调用为浅拷贝。为了避免浅拷贝带来的问题,最好自定义拷贝构造函数,使某些情形下发生深拷贝,以避免堆区内存被重复释放。以C++为开发语言时,多数难以发现的隐晦bug往往就出现在构造函数/拷贝构造函数的隐式调用上。而导致这些函数的隐式调用往往就与类对象的按值初始化、按值传参、按值返回,以及类对象副本和临时类对象的隐式创建有关。因此,在编写C++程序时,要重视引用变量或指针变量的意义!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容