拷贝构造函数
来接着来介绍这位默认成员函数
在创建对象时,难免不去创建一个一样的对象,这个时候就需要拷贝构造函数的帮助了
还是接着用Date类来举例子
Date d1(2023, 2, 19);
Date d2(d1); // d2的值跟d1一样
上面d2的就用了拷贝构造来复制d1的值
拷贝构造函数:
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用。
特性
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
拷贝构造函数的用法:
#include <iostream>
class Date {
public:
Date(int year = 1970, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
/* Date(Date d); 会无穷递归下去*/
Date(Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2022, 3, 9);
Date d2(d1);
return 0;
}
当自定义函数产生拷贝时,必需要调用拷贝构造函数,但是要先需要传参数,然后传值传参又需要调用拷贝构造,周而复始无限循环,直接无限月读(doge)
传参时,用了const修饰,是为了防止下面的情况:
/* Date d2(d1); */
Date(Date& d)
{
d._year = _year;
d._month = _month;
d._day = _day;
}
这个时候,编译器不会报错,但是结果会变为随机值,加了const的话就不会出现上面的情况了,因为当初出现了上面的这种情况,编译器就不会放你编译通过
默认生成的拷贝构造
- 对于内置类型的成员,会完成按字节序的拷贝(把每个字节依次拷贝过去)。
- 对于自定义类型成员,会再调用它的拷贝构造。
深拷贝和浅拷贝了解下:
内置类型拷贝就是浅拷贝(把每个字节依次拷贝过去)
深拷贝源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响
编译器自动生成的默认拷贝构造函数,对于内置类型和自定义类型都会拷贝处理。但是处理的细节是不一样的,这其中的细节要深究
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 0, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// 注释掉拷贝构造,让编译器自动生成
// Date(Date& d) {
// _year = d._year;
// _month = d._month;
// _day = d._day;
// }
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(1970, 1, 1);
// 调用默认拷贝构造
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
结果:
像上述中的日期类中的成员变量无需去认真销毁,那是不是拷贝构造就没必要自己写了呢?
答案是当然不行的, 比如关于数组栈类中的会向堆上申请空间,而编译器只会傻瓜式的进行浅拷贝, 实列化一个 stack s1, 再去stack s2(s1), 此时若是只用默认生成的拷贝构造, 就会发生s1中的数组指针和s2中的数组指针指向同一块空间.
可想而知, s1 和 s2 进行的增删改都会互相影响, 而且再后绪去堆上释放s1,s2的指向的空间,此时同一块空间释放两次.当多次使用 free () 且内存地址作为输入时,会发生双重释放错误。 在同一个变量上调用 free () 两次可能会导致内存泄漏
- 总结:对于常见的类,比如日期类,默认生成的拷贝构造能用。但是对于栈这样需要动态申请空间的类,则需要我们自己去针对性的编写拷贝构造。
析构函数
既然有初始化的构造函数,就有销毁、清理数据的析构函数.
构造函数也是默认成员函数,不写编译器也会自己生成一份,写了编译器就不会再自动生成了。
- 对于内置类型成员变量不处理,而对于自定义类型的成员变量会调用它的默认构造函数。
析构函数完成对象中成员空间的清理。如果类对象中的空间是向堆申请的空间需要进行资源清理,才需要自己实现析构函数。
析构函数在对象生命周期到了以后自动调用,什么时候生命周期到了?如果是局部变量,出了作用域。全局和静态变量,整个程序结束。
- 注意: 匿名对象的生命周期的就在当前行