目录
一、 概念
1. 拷贝构造函数是什么?
2. 为什么要有拷贝构造函数?
3. 怎么用拷贝构造函数?
3.1 创建拷贝构造函数
3.2 调用拷贝构造函数
二、特征
三、编译器生成的默认拷贝构造函数
四、什么时候需要显示的写拷贝构造函数?
拓、在C++中实现深拷贝
1. 自己开辟一个新空间,然后再赋值。
2. 借助浅拷贝的构造函数来实现深拷贝。
3. 重载=
一、 概念
1. 拷贝构造函数是什么?
拷贝构造函数是一个特殊的构造函数,也是用来初始化对象的,不过它是用已经存在的对象来初始化同类对象。
2. 为什么要有拷贝构造函数?
在创建新对象时,可否用已经存在的同类对象来初始化这个新对象呢?能否快速拷贝出一个对象的副本呢?为解决以上问题,C++中引入了拷贝构造函数:拷贝构造函数用于实现对象的复制和初始化。
3. 怎么用拷贝构造函数?
3.1 创建拷贝构造函数
和构造函数一样,函数名和类名相同,且没有返回值,但拷贝构造函数的参数必须是本类类型的引用或是指向本类类型的指针。
class Date { public: Date(int year = 2022, int month = 10, int day = 1) { _year = year; _month = month; _day = day; } //Date(const Date d) // 错误写法:编译报错,会引发无穷递归。 // 正确写法,拷贝构造函数的参数必须是本类类型的引用或是指向本类类型的指针。 Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } Date(const Date* d) { _year = d->_year; _month = d->_month; _day = d->_day; } // 拷贝构造函数可以有多个参数,但一般只以一个对象为副本进行拷贝,所以只写一个参数。 Date(const Date& d1,const Date& d2,const Date* d3) { _year = d1._year; _month = d2._month; _day = d3->_day; } private: int _year; int _month; int _day; };
所以拷贝构造函数的参数必须是本类类型的引用或是指向本类类型的指针,不然会引起以下拷贝构造函数传值传参带来的无穷递归问题:
拷贝构造函数如使用传值传参的方式,会引发无穷递归调用编译器会直接报错。因为使用传值传参时,编译器要调用拷贝构造函数用实参初始化形参,使用拷贝构造函数就必须得先传参,又会调用拷贝构造函数,这样就造成了造成无穷递归。
在传引用传参或指针传参时,对于像拷贝构造函数的形参这类不需要修改的参数,建议加上一个const。这样做即能避免误操作导致实参被修改,也能误操作时给我们一个提示,让我们快速定位错误。
3.2 调用拷贝构造函数
a. 调用拷贝参数是本类类型的引用的拷贝构造函数
int main() { Date a(1,1,1); Date b(a); return 0; }
b. 调用拷贝参数是指向本类类型的指针的拷贝构造函数
int main() { Date c(3, 3, 3); Date d(&c); return 0; }
c. 调用多个参数的拷贝构造函数
拷贝构造函数可以有多个参数,但一般只以一个对象为副本进行拷贝只写一个参数。
int main() { Date a(1, 1, 1); Date b(2, 2, 2); Date c(3, 3, 3); Date d(a, b, &c); return 0; }
二、特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数必须是本类类型的引用或是指向本类类型的指针。使用传值传参方式编译器会直接报错,因为会引发无穷递归调用。
3. 拷贝构造函数可以有多个参数,但一般只以一个对象为副本进行拷贝,所以只写一个参数。
4. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
三、编译器生成的默认拷贝构造函数
若未显式定义,编译器会生成默认的拷贝构造函数。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
编译器生成的默认拷贝构造函数引发的“双重删除”问题:
编译器生成的默认拷贝构造函数只能进行浅拷贝,无法对申请的资源(如动态开辟的空间)进行拷贝,看下面的例子:
typedef int DataType; class Stack { public: Stack(size_t capacity = 10) { _array = (DataType*)malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } ~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; }
在以上例子中我们发现,默认的拷贝构造函数会令拷贝的类和被拷贝的类中的指针变量指向同一块空间,这样会造成同一块空间被析构函数析构两次,这通常被称为“双重删除”或“重复删除”,这是一个严重的问题,会导致程序崩溃。这个时候需要显示的写一个构造函数,并在里面完成深拷贝。
四、什么时候需要显示的写拷贝构造函数?
编译器生成的默认拷贝构造函数只能进行浅拷贝,无法对申请的资源(如动态开辟的空间)进行拷贝。
所以类中如果没有涉及资源申请时,拷贝构造函数是否写都可以。一旦涉及到资源申请时,一定要显示的写拷贝构造函数,否则就是浅拷贝,可能导致“双重删除”问题。
拓、在C++中实现深拷贝
实现深拷贝的关键是自己开辟一个新空间,然后再赋值。以下以string类为例,演示如何实现深拷贝:
1. 自己开辟一个新空间,然后再赋值。
2. 借助浅拷贝的构造函数来实现深拷贝。
但这有个小问题,就是当前对象未初始化,直接交换数值可能会导致程序崩溃,所以加上初始化列表,在交换数值前先初始化。
3. 重载=
如果不重载,依靠编译器自动生成的 operator=,还是会出现浅拷贝问题,这次是拷贝对象中的str指向和原对象的str的同一块空间。(析构仍会出问题)
注意,更改指针指向前一定要释放原空间,不然会造成内存泄漏。
上面样写没有考虑到,自己给自己赋值的问题,所以还要加一个判断。
这样写就完美了吗?不,还没有考虑new失败后直接跳出函数同时抛出异常。(需要的内存过大可能会导致new失败)
这里我们先开辟空间给一个临时变量,这样如果new失败了,原来的str也还没有被销毁。
当然也可以写成:
类似于拷贝构造,直接用目标对象构造一个临时变量,再彻底交换,临时变量结束时也会释放原有的空间。(注,这里的swap是需要自己实现的对象交换函数)
注,里面的swap是库里定义的全局域中的swap函数,故要在前面加上 : : 表明是全局域中的函数。
那在实现函数时为什么不像下面一样直接调用库里的swap函数进行交换呢?
因为库中的swap也会用到 = ,这样会造成死循环
再简洁一点就是
------------------------END-------------------------
才疏学浅,谬误难免,欢迎各位批评指正。