拷贝构造
前面我们已经了解到了构造函数和析构函数, 这里介绍拷贝构造
1. 概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
-
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型的对象创建新对象时由编译器自动调用
-
拷贝构造 (属于构造函数的一种重载形式)
创建一个和原有已经存在的对象完全一样的新对象,所以参数是一个对象。
2. 特征
-
拷贝构造函数也是特殊的成员函数,其特征如下:
- 当构造函数的参数是一个对象时就是拷贝构造
- 没有返回值, 函数名也和类名相同
- 参数类型为引用,不能是值
- 属于构造函数的一种重载形式
- 如果不显示定义,编译器会自动生成一个拷贝构造, 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,即浅拷贝
-
Data(Data date){} 这种传值形式,首先编译器不会通过, 而且会造成无限递归而导致程序崩掉,因为在调拷贝构造时会有传值的过程, 而传值其实就是一个拷贝的过程,所以所当我们去调用拷贝构造函数体时,首先会进行值拷贝,也就是在传参时还需要再调一次拷贝构造,
Data(Data& date){} 参数类型必须为引用, 不能是值(且不会通过, 引发无穷递归)
Data(const Data& date){} 加了const 有个好处, 就是不管是匿名对象还是普通对象他都可以接收, 且由于匿名对象具有常性, 也只能用const接收, 他的初始化 要用对象加.来实现, _year = d._year;
3.拷贝构造的使用
class Date{
public:
Date(int year = 1, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
Date(const Date& date){// 实参传进来 内容是不变的, 用const修饰,
// 既可接收普通对象也可接收匿名对象
// 加const的好处是也可以接收匿名对象,匿名对象是临时的,具有常性,只能用const接收
_year = date._year;
_month = date._month;
_day = date._day;
}
private:
int _year;
int _month;
int _day;
};
int main(){
Date d1;
Date d2(d1);// 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
return 0;
}
传值的拷贝构造:
// 若为传值的拷贝构造
Date(const Date date){ // 由于成员函数的第一个参数地this指针,
// 则相当拷贝构造函数是Date(const Date* const this, Date date)
}
Date copy(d); // 而传参时,其实就是Date(©, d), this传递就是copy的地址,把d传给date
// 由于是值拷贝,就要调一次拷贝构造,
这是整个拷贝传值的过程:
因为把 d 传给 date 是传值,也叫值传递, 而值传递不是本身(不是引用),必须要进行值拷贝来传递, 且值拷贝需要调拷贝函数,而调了拷贝构造函数时, 由于调拷贝构造函数需要传参, 所以不得不又得发生值拷贝,然后又继续调拷贝构造, 才导致无穷递归调用。 所以拷贝构造不能传值, 得传本身 --> 即引用。
void test(){
Date d(2020, 5, 5);
Date copy(d); //拷贝构造
Date copy2 = d; //也会调拷贝构造, 对于copy2本身不存在 通过d去创造一个新的对象
// 也为拷贝构造 d = copy = copy2
}
- 如果不显示定义, 编译器会自动生成一个拷贝构造(完成的是字节拷贝(也叫内容浅拷贝), 对象模型多大 就拷多大),d = copy = copy2, 但是需要拷贝资源(深拷贝) 时, 则需要自己定义拷贝构造
下面我们给一个顺序表:
class seqList{
public:
seqList(int n = 10){
_array = (int*)malloc(sizeof(int)* n); // 开空间
_size = 0;
_capacity = n;
}
// 析构函数
~seqList(){
if (_array)
free(_array);
_size = 0;
_capacity = 0;
_array = nullptr;
cout << "~seqList()" << endl;
}
private: // 这为顺序表的对象模型. 但是资源并不在这里
int* _array; // 顺序表中会定义指针 4个字节
int _size;// 大小 4个字节
int _capacity;// 容量 4个字节
// 总共12个字节, 默认拷贝构造函数实现的就是12个字节的拷贝
}
void test(){
seqList lst; // 构造函数
seqList copy(lst); // 调拷贝构造
}
不显式定义拷贝构造的前提下, 实现的是字节拷贝, 此时经调试发现 lst 和 copy 两个对象的_array指针指向同一个地址, 说明两个对象共享同一片资源, 在函数退出时需要调 2 次析构函数, 导致释放 2 次空间, 程序崩掉.
- 事实证明: 浅拷贝并没有把资源拷过去, 只拷贝了成员变量的内容, 而资源并不在顺序表的对象模型中; 指针 _array 实际指向的才是资源 ; 浅拷贝只拷贝了对象模型的内容(成员变量的值), 并不拷贝资源.
小题:
以下代码共调用多少次拷贝构造函数?
Widget f(Widget u){
Widget v(u);
Widget w=v;
return w;
}
int main(){
Widget x;
Widget y=f(f(x));
}
调用拷贝构造函数的时机,从代码上看会有三种情形,1.对象初始化对象 2.函数参数以对象传递 3.函数的返回值以对 象返回,因此乍眼一看,超过1次
整个程序分别在,x初始化u、u初始化v、v初始化w、w返回时,注意w返回时初始u不在调用拷贝构造函数,第二次调用 f() 函数时,相当于u的结合会少调用一次,其他不变,所以总体次数为4+3 = 7次