三、拷贝构造函数
定义:把同类型的对象当做参数传给当前对象叫做拷贝构造函数,即类拿自己的一个对象去构造同类型的一个对象,完成对象的拷贝初始化。
1. #include<iostream> 2. using namespace std; 3. 4. class Date 5. { 6. public: 7. //构造函数 8. Date(int year = 2022, int month = 4, int day = 8) 9. { 10. _year = year; 11. _month = month; 12. _day = day; 13. } 14. 15. //析构函数:清理资源 16. ~Date() 17. { 18. cout << "~Date()" << endl;//在析构函数内打印 19. } 20. 21. private: 22. int _year; 23. int _month; 24. int _day; 25. }; 26. 27. int main() 28. { 29. Date d1(2022, 9, 6);//调用构造函数 30. Date d2;//调用构造函数 31. Date d3(); 32. 33. return 0; 34. }
d3的定义方式没有调用构造函数,构造不出来对象:
拷贝构造函数也是构造函数,函数名和类型名相同,参数是同类型对象的引用,由编译器自动调用。
特性:
(1)拷贝构造函数是构造函数的重载。
(2)拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
思考:为什么必须使用引用传参而不是传值传参?
①如果是内置类型的拷贝,传值拷贝即形参是实参的一份临时拷贝。
如下使用场景:用a和b初始化x和y,x和y是a和b的一份临时拷贝
1. #include<iostream> 2. using namespace std; 3. 4. void Swap(int x, int y) 5. { 6. int temp = x; 7. int y = x; 8. int y = temp; 9. } 10. 11. int main() 12. { 13. int a = 1; 14. int b = 2; 15. 16. //传值拷贝并不能实现真正的交换 17. Swap(a, b); 18. }
②如果是自定义类型的拷贝,那么传参使用传值,就是一个拷贝构造,而要调用拷贝构造就要先传参,如此循环往复,以至无穷。
如下,假如拷贝构造函数使用传值传参:
Date d4(d1);
为了解决自定义类型传值拷贝带来的的无穷递归问题,使用引用传参调用拷贝构造之前先传参,引用是参数对象内存的别名,当前对象就是this,把参数的值依次拷贝复制给当前对象。
1. #include<iostream> 2. using namespace std; 3. 4. class Date 5. { 6. public: 7. //构造函数 8. Date(int year = 2022, int month = 4, int day = 8) 9. { 10. _year = year; 11. _month = month; 12. _day = day; 13. } 14. 15. //拷贝构造函数 16. Date(Date& d)//Date(Date* this, Date& d) 17. //d4是this, 形参d是d1的引用,是d1内存的别名 18. { 19. _year = d._year; 20. _month = d._month; 21. _day = d._day; 22. } 23. 24. //析构函数:清理资源 25. ~Date() 26. { 27. cout << "~Date()" << endl;//在析构函数内打印 28. } 29. 30. private: 31. int _year; 32. int _month; 33. int _day; 34. }; 35. 36. int main() 37. { 38. Date d1(2022, 9, 6);//调用构造函数 39. Date d2;//调用构造函数 40. //Date d3();//没有调用构造函数,拷贝不出来对象 41. Date d4(d1);//Date(&d4,Date& d1) 42. 43. return 0; 44. }
如果不用引用传参,而用指针传参,能不能实现?虽然可以实现,但是用指针就不是拷贝构造了,因为传的参数不是同类型对象,而是同类型对象的地址,所以调用时必须取地址:
1. #include<iostream> 2. using namespace std; 3. 4. class Date 5. { 6. public: 7. //构造函数 8. Date(int year = 2022, int month = 4, int day = 8) 9. { 10. _year = year; 11. _month = month; 12. _day = day; 13. } 14. 15. //传指针 16. Date(Date* d)//Date(Date* this, Date* d) 17. { 18. _year = d->_year; 19. _month = d->_month; 20. _day = d->_day; 21. } 22. 23. //析构函数:清理资源 24. ~Date() 25. { 26. cout << "~Date()" << endl;//在析构函数内打印 27. } 28. 29. private: 30. int _year; 31. int _month; 32. int _day; 33. }; 34. 35. int main() 36. { 37. Date d1(2022, 9, 6);//调用构造函数 38. Date d2;//调用构造函数 39. //Date d3(); 40. Date d4(&d1);//调用时必须取地址 41. 42. return 0; 43. }
因此,对于自定义类型的对象,一般推荐使用引用传参,虽然传值传参也可以,但是要调用拷贝构造,如f1使用传值传参,f2使用引用传参:
1. #include<iostream> 2. using namespace std; 3. 4. class Date 5. { 6. public: 7. //构造函数 8. Date(int year = 2022, int month = 4, int day = 8) 9. { 10. _year = year; 11. _month = month; 12. _day = day; 13. } 14. 15. //拷贝构造函数 16. Date(Date& d)//Date(Date* this, Date& d) 17. //d4是this, 形参d是d1的引用,是d1内存的别名 18. { 19. _year = d._year; 20. _month = d._month; 21. _day = d._day; 22. 23. cout << "call copy constructors" << endl; 24. } 25. 26. //析构函数:清理资源 27. ~Date() 28. { 29. cout << "~Date()" << endl;//在析构函数内打印 30. } 31. 32. private: 33. int _year; 34. int _month; 35. int _day; 36. }; 37. 38. void f1(Date d) 39. { 40. cout << "f1" << endl; 41. } 42. 43. void f2(Date& d) 44. { 45. 46. } 47. 48. int main() 49. { 50. Date d1(2022, 9, 6);//调用构造函数 51. f1(d1); 52. //f2(d1); 53. 54. return 0; 55. }
发现f1使用传值传参时先调用了拷贝构造完成传参,再调用f1函数:
拷贝构造函数参数类型推荐加上const,是对形参权限的缩小,使得形参不能被修改,如果要修改参数的值。可以不加const。
若未显式定义,系统会生成默认拷贝构造函数。 同构造函数和析构函数不同:
(1)拷贝构造函数对内置类型依次按照字节序完成拷贝,即浅拷贝或值拷贝。
假如不写拷贝构造函数,打印d1和d4:
1. #include<iostream> 2. using namespace std; 3. 4. class Date 5. { 6. public: 7. //构造函数 8. Date(int year = 2022, int month = 4, int day = 8) 9. { 10. _year = year; 11. _month = month; 12. _day = day; 13. } 14. 15. //拷贝构造函数 16. //Date(Date& d)//Date(Date* this, Date& d) 17. // //d4是this, 形参d是d1的引用,是d1内存的别名 18. //{ 19. // _year = d._year; 20. // _month = d._month; 21. // _day = d._day; 22. // 23. // cout << "call copy constructors" << endl; 24. //} 25. 26. void Print() 27. { 28. cout << _year << "-" << _month << "-" << _day << endl; 29. } 30. 31. //析构函数:清理资源 32. ~Date() 33. { 34. cout << "~Date()" << endl;//在析构函数内打印 35. } 36. 37. private: 38. int _year; 39. int _month; 40. int _day; 41. }; 42. 43. int main() 44. { 45. Date d1(2022, 9, 6);//调用构造函数 46. Date d4(d1); 47. 48. d1.Print(); 49. d4.Print(); 50. 51. return 0; 52. }
发现d1和d4都被打印了,d4构造成功了,完成了拷贝构造,说明我们没写拷贝构造函数,但是编译器自动生成了一个拷贝构造函数。
(2)对于自定义类型,不显式定义拷贝构造函数,会引发两个问题:
①调用析构函数时,这块空间被free了两次
②其中一个对象插入删除数据,都会导致另一个对象也插入删除了数据
以栈为例, 不显式定义拷贝构造函数,会引发程序崩溃:
1. #include <stdlib.h> 2. #include <iostream> 3. using namespace std; 4. 5. typedef int STDataType; 6. 7. class Stack 8. { 9. public: 10. //构造函数 11. Stack(int capacity = 4) 12. { 13. _a = (STDataType*)malloc(sizeof(STDataType) * 4); 14. _size = 0; 15. _capacity = capacity; 16. } 17. 18. //析构函数:清理资源 19. ~Stack() 20. { 21. cout << this << endl; 22. free(_a); 23. _a = nullptr; 24. _size = _capacity = 0; 25. } 26. 27. private: 28. STDataType* _a; 29. int _size; 30. int _capacity; 31. 32. }; 33. 34. int main() 35. { 36. Stack st1; 37. Stack st2(st1); 38. 39. return 0; 40. }
程序崩了:
栈的3个成员变量类型都是内置类型int,但栈是一个管理资源的类,st2虽然什么值都没给,但是调用了它的构造函数,构造函数给capacity了一个默认值4,构造函数会拿这个默认值去开辟空间,_a就指向了这块空间,由于没有显式定义拷贝构造函数,因此对象st2的成员变量_a拷贝的是st1的成员变量_a指针,即把st1的_a指针的值,拷贝给了st2的_a,那么两个指针的值是一样的,st1的_a和st2的_a指向同一块空间。
造成程序崩溃的原因:调用析构函数,这块空间被free了两次:后定义的先析构,st2先析构,free(_a)就把这块空间释放了,这块空间就被归还给了操作系统,再把_a置空了。再析构st1时,free(_a)还要释放这块空间,同一块空间被释放了两次。
另外,由于共用同一块空间,st1和st2无论谁插入删除数据,都会导致对方也插入删除数据。
因此,像Stack这样的类,编译器默认生成的拷贝构造完成的是浅拷贝,不满足需求,需要我们自己实现深拷贝。
当没有显式定义拷贝构造函数时,对于内置类型成员会完成浅拷贝,对于自定义类型成员,会调用自定义类型的拷贝构造函数完成拷贝。
如下所示:
1. #include <iostream> 2. using namespace std; 3. 4. class A 5. { 6. public: 7. //构造函数 8. A(int a = 1) 9. { 10. _a = a; 11. } 12. 13. //拷贝构造函数 14. A(const A& a) 15. { 16. cout << "A(const A& a)" << endl; 17. _a = a._a; 18. } 19. 20. //析构函数 21. ~A() 22. { 23. cout << "~A()" << endl; 24. } 25. 26. private: 27. int _a; 28. }; 29. 30. class Date 31. { 32. public: 33. //构造函数 34. Date(int year = 2022, int month = 4, int day = 8) 35. { 36. _year = year; 37. _month = month; 38. _day = day; 39. 40. cout << "Date()" << endl; 41. } 42. 43. //拷贝构造函数 44. //Date(Date& d)//Date(Date* this, Date& d) 45. // //d4是this, 形参d是d1的引用,是d1内存的别名 46. //{ 47. // _year = d._year; 48. // _month = d._month; 49. // _day = d._day; 50. // 51. // cout << "call copy constructors" << endl; 52. //} 53. 54. void Print() 55. { 56. cout << _year << "-" << _month << "-" << _day << endl; 57. } 58. 59. //析构函数:清理资源 60. ~Date() 61. { 62. cout << "~Date()" << endl;//在析构函数内打印 63. } 64. 65. private: 66. int _year; 67. int _month; 68. int _day; 69. 70. A _a; 71. }; 72. 73. int main() 74. { 75. Date d1(2022, 9, 6);//调用构造函数 76. Date d4(d1); 77. 78. return 0; 79. }
经过监视和打印,发现d4被构造成功了,并且调用了A类的拷贝构造函数:
总结:
(1)像Date这样的类,需要的是浅拷贝,那么默认生成的拷贝构造就够用了,不需要自己写。
(2)像Stack这样的类,需要深拷贝,因为浅拷贝会导致析构两次,程序崩溃等问题,需要自己写。