概念
拷贝,我们都知道
那么,对象拷贝,该如何拷贝呢?
像这样吗?
代码:
class Date
{
public:
//构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//打印
void print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
Date d1(2023, 7, 31);
Date d2;
d2 = d1;//将d1赋给d2
d2.print();
return 0;
}
我们看到,d1确实拷贝给了d2,但是对象之间的拷贝真的是这样吗?
拷贝构造函数又是怎么写的呢?
拷贝构造函数:
这里 d 相当于要被拷贝的对象的别名,也就是下面d1的引用,然后我们将d 中的成员依次赋值给创建的对象。
那为什么要用引用呢?
这样不可以吗?
答:这样会发生无穷递归的问题
所以一定要用引用!!!
我们说,拷贝构造函数时默认成员函数,那么若未显式定义,编译器会生成默认的拷贝构造函数。
这就是我们一开始为什么没写拷贝构造,d2仍然拷贝成功了的原因:
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定 义类型是调用其拷贝构造函数完成拷贝的。
那,既然默认生成,我们还需要写吗?不麻烦吗?
答:当然要写,对于日期类来说,可以不写,默认生成的就可以了。但是对于一些需要开空间的,就需要自己写。
比如,像栈类这种的:
代码:
typedef int DataType;
class Stack
{
public:
//构造
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
//压栈
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
//析构
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
像这种,没有写拷贝构造,用默认的就会崩溃!
崩溃在这:
在调用析构函数时崩溃,为什么呢?
我们来看:
两者指向同一块空间,那s1,s2在销毁时,都要调用析构函数,这块空间就相当于释放了两次,所以程序崩溃了!一块内存空间多次释放,导致程序崩溃。
这就需要写一个拷贝构造来解决这个问题
代码:
Stack(const Stack& st)
{
//新开一块一样大小的空间
_array = (DataType*)malloc(st._capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
//拷贝里面的数据
memcpy(_array, st._array, st._capacity * sizeof(DataType));
_size = st._size;
_capacity = st._capacity;
}
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
总结
特征:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
- 一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 尽量使用引用。