【C++】-- 构造函数、析构函数、拷贝构造函数、赋值运算符重载函数(二)

简介: 【C++】-- 构造函数、析构函数、拷贝构造函数、赋值运算符重载函数

三、拷贝构造函数

1.拷贝构造函数定义及特性

定义:把同类型的对象当做参数传给当前对象叫做拷贝构造函数,即类拿自己的一个对象去构造同类型的一个对象,完成对象的拷贝初始化。

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。

2.编译器自动生成的拷贝构造函数

若未显式定义,系统会生成默认拷贝构造函数。 同构造函数和析构函数不同:

(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这样的类,需要深拷贝,因为浅拷贝会导致析构两次,程序崩溃等问题,需要自己写。

相关文章
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
80 4
|
2月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
2月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
79 6
|
2月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
37 0
C++ 多线程之线程管理函数
|
24天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
38 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
86 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
31 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
26 1