CPP从入门到入土之类和对象Ⅲ
拷贝构造函数
拷贝构造函数是一个已经存在的对象去初始化一个新的对象时,调用的函数
例如:
假设我有一个盒子,里面装了一个苹果
拷贝构造函数的特点
- 拷贝构造函数是构造函数的一个重载
- 拷贝构造函数的第一个参数必须是类类型对象的引用,例如一个日期类的拷贝构造:
// err: “Date”: 非法的复制构造函数: 第一个参数不应是“Date”
// Date(Date d);
// 拷贝构造函数
Date(Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
- 如果使用传值调用的方式,编译器会直接报错,因为会引发无穷递归
拷贝构造函数也可以有多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须要有缺省值
- cpp规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以上图的自定义类型传值传参和传值返回都会调用拷贝构造
- 如果没有显式定义拷贝构造,编译器会默认生成拷贝构造函数 自动生成的拷贝构造对内置类型成员变量会完成浅拷贝(又叫值拷贝) 即一个字节一个字节拷贝,对自定义类型成员变量会调用它的拷贝构造
我们需要详细讲一下浅拷贝和深拷贝
浅拷贝与深拷贝
- 默认拷贝构造函数的行为
// 浅拷贝和深拷贝
class Stack {
public:
// 没有显式写拷贝构造,默认生成的拷贝构造:
Stack(Stack& ST) {
_a = ST._a; // 直接复制指针地址,浅拷贝
_size = ST._size;
_capacity = ST._capacity;
}
private:
int* _a;
int _size;
int _capacity;
};
// 假设有以下代码,其实编译器已经报错了
int main() {
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2 = st1; // 调用默认拷贝构造函数(浅拷贝)
}
- 问题分析
这是报错信息:
我们不管报错,来分析一下
此时,st1
和st2
的成员变量状态如下:
st1._a
和st2._a
指向同一块内存地址(浅拷贝直接复制了指针值)
st1._size
和st2._size
相同。st1._capacity
和st2._capacity
相同
- 析构时崩溃的原因
当st1
和st2
离开作用域时,它们的析构函数会被依次调用:
~Stack() {
free(_a); // 释放 _a 指向的数组
_a = nullptr;
}
具体过程:
-
st2
先析构:
st2._a
指向的内存被释放。 -
st1
再析构:
st1._a
现在指向一块已经被释放的内存,再次调用free()
会导致 重复释放(double free),引发程序崩溃
-
图解
-
** 解决方案:深拷贝**
为Stack
类显式实现深拷贝构造函数,让每个对象拥有独立的资源:
//1. 分配内存
_a = (int*)malloc(sizeof(ST.capacity));
if(_a == nullptr){
perror("malloc() err");
return;
}
// 2. 复制数据
memcpy(_a, ST._a, sizeof(int) * ST._size);
// 3. 复制其他成员
_size = ST._size;
_capacity = ST._capacity;
}
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成的
6. 总结
- 如果没有需要管理的资源,一般情况下不写拷贝构造函数默认生成的即可,例如日期类
- 如果都是自定义类型成员,内置类型成员没有指向资源,默认生成的即可
- 一般情况下,不用显式写析构函数,就不用写拷贝构造
- 如果内部有指针或者一些值指向的资源,需要显式写析构释放,就要显式写构造完成深拷贝
野引用
传值返回会产生一个临时对象调用拷贝构造,传引用返回,返回的时返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束之后就销毁了,那么使用引用返回就是有问题的,这时的引用相当于一个野引用,类似野指针。传引用返回会减少拷贝,但是一定要确保返回对象在函数结束之前还存在,才能传引用返回
// 传值返回
Date Func1() {
Date tmp(2024, 7, 5);
tmp.Print();
return tmp;
}
// 传引用返回
Date& Func2(){
Date tmp(2024, 7, 5);
tmp.Print();
return tmp;
}
int main(){
// Func返回了一个局部对象tmp的引用作为返回值
// Func2函数结束,tmp对象被销毁,相当于野引用
Date ret2 = Func2();
ret2.Print();
cout << "*******************************" << endl;
Date ret1 = Func1();
ret1.Print();
return 0;
}
输出结果:
运算符重载
未完待续~~