拷贝构造函数与赋值构造函数的区别

构造函数、析构函数、赋值构造函数是每个类最基本的的函数。每个类只有一个析构函数和一个赋值构造函数。但是有很多构造函数(一个为拷贝构造函数,其他为普通构造函数)。
对于一个类A,如果不编写上述四个函数,c++编译器将自动为A产生四个默认的函数,即:

  1. A(void) //默认无参数构造函数
  2. A(const A &a) //默认拷贝构造函数
  3. ~A(void); //默认的析构函数
  4. A & operator = (const A &a); //默认的赋值构造函数

既然能自动生成函数,为什么还需要自定义?原因之一是默认的拷贝构造函数和默认的赋值构造函数均采用”位拷贝“而非”值拷贝“

位拷贝 v.s. 值拷贝

为便于说明,以自定义String类为例,先定义类,而不去实现。

  1. #include <iostream>
  2. using namespace std;
  3. class String
  4. {
  5. public:
  6. String(void);
  7. String(const String &other);
  8. ~String(void);
  9. String & operator =(const String &other);
  10. private:
  11. char *m_data;
  12. int val;
  13. };

位拷贝拷贝的是地址,而值拷贝拷贝的是内容

如果定义两个String对象a, b。当利用位拷贝时,a=b,其中的a.val=b.val;但是a.m_data=b.m_data就错了:a.m_data和b.m_data指向同一个区域。这样出现问题:

  • a.m_data原来的内存区域未释放,造成内存泄露
  • a.m_data和b.m_data指向同一块区域,任何一方改变,会影响到另一方
  • 当对象释放时,b.m_data会释放掉两次

因此当类中还有指针变量时,拷贝构造函数和赋值构造函数就隐含了错误。此时需要自己定义。

结论

如果类需要析构函数,则它也需要赋值操作符和拷贝构造函数

注意

  • 如果没定义拷贝构造函数(别的不管),编译器会自动生成默认拷贝构造函数
  • 如果定义了其他构造函数(包括拷贝构造函数),编译器绝不会生成默认构造函数
  • 即使自己写了析构函数,编译器也会自动生成默认析构函数

因此此时如果写String s是错误的,因为定义了其他构造函数,就不会自动生成无参默认构造函数。

拷贝构造函数 v.s. 赋值构造函数

  1. #include <iostream>
  2. #include <cstring>
  3. using namespace std;
  4. class String
  5. {
  6. public:
  7. String(const char *str);
  8. String(const String &other);
  9. String & operator=(const String &other);
  10. ~String(void);
  11. private:
  12. char *m_data;
  13. };
  14. String::String(const char *str)
  15. {
  16. cout << "自定义构造函数" << endl;
  17. if (str == NULL)
  18. {
  19. m_data = new char[1];
  20. *m_data = '\0';
  21. }
  22. else
  23. {
  24. int length = strlen(str);
  25. m_data = new char[length + 1];
  26. strcpy(m_data, str);
  27. }
  28. }
  29. String::String(const String &other)
  30. {
  31. cout << "自定义拷贝构造函数" << endl;
  32. int length = strlen(other.m_data);
  33. m_data = new char[length + 1]; //申请空间,因为原来没有空间
  34. strcpy(m_data, other.m_data);
  35. }
  36. String & String::operator=(const String &other)
  37. {
  38. cout << "自定义赋值构造函数" << endl;
  39. if (this == &other)
  40. {
  41. return *this; //防止自复制
  42. }
  43. else
  44. {
  45. delete [] m_data; //赋值需要先删除原来的空间
  46. int length = strlen(other.m_data);
  47. m_data = new char[length + 1];
  48. strcpy(m_data, other.m_data);
  49. return *this;
  50. }
  51. }
  52. String::~String(void)
  53. {
  54. cout << "自定义析构函数" << endl;
  55. delete [] m_data;
  56. }
  57. int main()
  58. {
  59. cout << "a(\"abc\")" << endl;
  60. String a("abc");
  61. cout << "b(\"cde\")" << endl;
  62. String b("cde");
  63. cout << " d = a" << endl;
  64. String d = a;
  65. cout << "c(b)" << endl;
  66. String c(b);
  67. cout << "c = a" << endl;
  68. c = a;
  69. cout << endl;
  70. }

说明几点

  1. 赋值构造函数中,上来比较 this == &other 是很必要的,因为防止自复制,这是很危险的,因为下面有delete []m_data,如果提前把m_data给释放了,指针已成野指针,再赋值就错了
  2. 赋值构造函数中,接着要释放掉m_data,否则就没机会了(下边又有新指向了)
  3. 拷贝构造函数是对象被创建时调用,赋值构造函数只能被已经存在了的对象调用

注意:

String a(“hello”); String b(“world”); 调用自定义构造函数.
String c=a;调用拷贝构造函数,因为c一开始不存在,最好写成String c(a);

构造函数调用构造函数问题

看下面例子

  1. #include <iostream>
  2. #include <string>
  3. using namespace std;
  4. class A
  5. {
  6. public:
  7. A(string _s1, string _s2)
  8. {
  9. s1=_s1;
  10. s2=_s2;
  11. }
  12. A(string _s1)
  13. {
  14. A(_s1, "");//调用另一个构造函数
  15. }
  16. void printme()
  17. {
  18. cout<<s1<<" "<<s2<<endl;
  19. }
  20. private:
  21. string s1;
  22. string s2;
  23. };
  24. int main()
  25. {
  26. A* a=new A("barret");
  27. a->printme();
  28. delete a;
  29. return 0;
  30. }

上面代码的输出结果是空的,barret并不会被显示。

原因需要从构造函数的流程讲起,当定义一个对象时,会按顺序做2件事情:

  1. 分配好内存(非静态数据成员是未初始化的)
  2. 调用构造函数(构造函数的本意就是初始化非静态数据成员)

code中14行已经为a对象分配了内存,但是构造函数还没有执行完,在15行调用了另一个构造函数,15行执行完相当于产生了一个匿名的A临时对象(临时对象中s1=barret,s2=””)。而输入参数barret并没有赋值到a对象的s1数据成员中。所以,构造函数执行完,a对象s1,s2成员的都是默认的内容。

在构造函数里调用另一个构造函数的关键是让第二个构造函数在第一次分配好的内存上执行,而不是分配新的内存,这个可以用标准库的placement new做到。
第15行修改如下:

  1. A(string _s1)
  2. {
  3. new (this)A(_s1, "");
  4. }