目录
前言
在前两章学习了 析构函数 和 构造函数
C++ ——— 类的 6 个默认成员函数之 析构函数-CSDN博客
C++ ——— 类的 6 个默认成员函数之 构造函数-CSDN博客
接下来学习拷贝构造函数
浅拷贝问题
日期类的赋值拷贝
void func(Data d)
{
d.Print();
}
int main()
{
Data d1(2024, 12, 12);
func(d1);
return 0;
}
关于日期类的代码请见构造函数的讲解
实例化了一个日期类 d1,并将 d1 值传递给 func 函数,func 函数中的同类型 d 接收,并打印数据
代码验证:
可以发现,打印没有任何问题,程序也是正常结束
栈类的赋值拷贝
void func(Stack s)
{
}
int main()
{
Stack s1;
func(s1);
return 0;
}
关于栈类的代码请见构造函数的讲解
同样实例化了一个栈类 s1,并且将 s1 值传递给 func 函数,func 函数中的同类型 s 接收
代码验证:
出现程序崩溃,问题出在栈的析构函数中
// 析构函数
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
我们知道程序运行结束时,编译器会自动调用类的析构函数,且是以栈的后进先出的特点调用的
所以会先调用 func 中 s 的析构函数,且 s1 是通过值传递给 s 的,那么 s 中的 _a 是直接赋值了 s1 中的 _a,先调用 func 中 s 的析构函数后,就把 _a 这块空间给释放掉了
再是 s 调用析构函数,但此时 s 中的 _a 的空间已经被释放了,_a 已经是野指针了,并且没有置空,所以 free 再次释放后就照成了二次释放问题,所以程序会崩溃
解决栈类的二次释放问题
1. 将值传递改为引用传递
void func(Stack& s)
{
}
int main()
{
Stack s1;
func(s1);
return 0;
}
s 作为 s1 的别名,对 s 的修改就会直接修改 s1 ,所以先对 s 析构时,s1 中的 _a 也同样置空了,就不会出现二次释放的问题
代码验证:
可以发现用引用能解决二次释放的问题,但是有些情况解决不了
比如用了引用,那么对 s 的改变就会改变 s1 ,但是不期望改变 s1
所以以上问题用一个函数来解决,就是拷贝构造函数
拷贝构造函数的概念
拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般是用 const 修饰)
在用已存在的类类型对象创建新对象时,由编译器自动调用
拷贝构造函数的特征
1. 拷贝构造函数时构造函数的一个重载形式
2. 拷贝构造函数的参数只有一个,且必须时类的类型对象的引用,使用值传递的方式编译器会直接报错,因为会引发无穷递归调用
日期类的拷贝构造函数
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
不论是传参还是赋值,都会先来调用拷贝构造函数,这是 C++ 强制要求的
所以拷贝构造函数不能写成值传递的形式,否则就会出现无穷递归的问题
因为不使用引用传参的话,在传参的时候又会调用拷贝构造,就会无穷递归调用下去
使用引用传参,就能避免这个问题,因为引用的底层逻辑还是指针,而指针只是地址,所以就不会出现无穷递归的问题
栈类的拷贝构造函数
Stack(const Stack& s)
{
// 深拷贝
//开辟同样大的空间
_a = (int*)malloc(sizeof(int) * s._capacity);
// 判断释放开辟成功
if (_a == nullptr)
{
perror("realloc fail");
return;
}
// 将内容赋值到开辟好的空间里
memcpy(_a, s._a, sizeof(int) * s._top);
_top = s._top;
_capacity = s._capacity;
}
避免二次释放的问题,所以开辟 s 中 _a 一样大的空间,并赋值内容,这就是深拷贝,各自指向各自的空间,且对 s 的改变不影响 s1
编译器默认生成的拷贝构造函数
若没有显示定义,编译器会生成默认的拷贝构造函数
默认的拷贝构造函数对象按内存存储的字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
那么默认生成的拷贝对于栈这个类就会有问题,所以还是手动写上拷贝构造较好