📖定义
拷贝构造函数是构造函数的一个重载,它的本质还是构造函数,那就意味着,只有在创建对象的时候,编译器才会自动调用它,那他和普通的构造函数有什么区别呢?
拷贝构造函数,是创建对象的时候,用一个已存在的对象,去初始化待创建的对象。简单来说,就是在我们创建对象的时候,希望创建出来的对象,和一个已存在的对象一模一样,此时就应该用拷贝构造函数,而不是普通的构造函数。拷贝构造函数有一点类似于克隆技术。
Data d1(2023, 7, 20);//定义一个日期类对象d1 Data d2(d1);//会去调用拷贝构造函数 int a = 10; int b = a;//不会调用拷贝构造
上面代码,首先定义了一个日期类对象d1,接着想创建第二个日期类对象d2,并且希望d2和d1一模一样,也就是用d1去克隆出d2,d2相当于是d1的一份拷贝。所以在创建d2对象的时候,参数列表直接传递了d1。
小Tips:拷贝构造函数是针对自定义类型的,自定义类型的对象在拷贝的时候,C++规定必须要调用拷贝构造函数。内置类型不涉及拷贝构造函数,如上,用a去创建b,是由编译器直接把a所表示的空间中的内容直接拷贝到b所表示的空间,并不涉及拷贝构造函数。
📖拷贝构造函数的错误写法
有了上面的分析,可能很多朋友会觉得,那我直接在类里面再写一个构造函数,把它的形参设置成日期类对象,不就行了嘛,于是便得到了下面的代码:
Data(Data d)//错误的拷贝构造 { _year = d._year; _month = d._month; _day = d._day; }
是不是觉得很简单?创建d2对象的时候,实参把d1传过来,然后用d接收,最后再把d的所有值赋值给this指针(当前this指针就指向d2),这一切堪称完美,但是我想告诉你,这种写法是大错特错的。
📖为什么是错的
问题出现在传参,就是实参d1传递给形参d的时候,上面代码中的形参d,既不是指针也不是引用,说明是值传递,值传递就意味着,形参d是实参d1的一份拷贝,注意:是拷贝,就是说,形参d要和实参d1一模一样,怎么才能让d和d1一摸一样?调用拷贝构造函数呀。
形参d在接收实参d1的时候,又要去调用拷贝构造来创建d,这次调用拷贝构造,又会有一个形参d,这个形参d又需要调用拷贝构造才能创建,相信到这里,小伙伴们已经看出问题所在了———无穷递归,形参在接收的时候,会无穷无尽的去调用拷贝构造函数,就像套娃一样。
为了避免出现这种无穷递归,编译器会自行检查,如果拷贝构造函数的形参是值传递,编译时会直接报错。
📖必须是引用
为了打破上面的魔咒,拷贝构造函数的形参只能有一个,并且必须是类类型对象的引用。下面才是正确的拷贝构造函数:
Data(Data& d)//正确的拷贝构造 { _year = d._year; _month = d._month; _day = d._day; } Data d1(2023, 7, 20);//定义一个日期类对象d1 Data d2(d1);//
此时创建d2的时候,传递d1调用拷贝构造函数,形参d是一个日期类的引用,因为引用时区别名,意味着d是d1的一个别名,此时就不会再去无穷无尽的调用拷贝构造啦。
📖建议加const
因为存在用一个const对象去初始化创建一个新对象这种场景,所以建议在拷贝构造函数的形参前面加上const,此时普通的对象能用,const对象也能用。
Data(const Data& d)//正确的拷贝构造 { _year = d._year; _month = d._month; _day = d._day; } const Data d1(2023, 7, 20);//定义一个日期类对象d1 Data d2(d1);
📖编译器生成的拷贝构造干了什么?
上一节提到,拷贝构造是一种默认成员函数,我们不写编译器会自动生成。编译器生成的默认拷贝构造函数,对内置类型按照字节方式直接拷贝(也叫值拷贝或浅拷贝),对自定义类型是调用其拷贝构造函数完成拷贝。
class Time//定义时间类 { public: Time()//普通构造函数 { _hour = 1; _minute = 1; _second = 1; } Time(const Time& t)//拷贝构造函数 { _hour = t._hour; _minute = t._minute; _second = t._second; cout << "Time::Time(const Time&)" << endl; } private://成员变量 int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d1; // 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数 // 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数 Date d2(d1); return 0; }
📖什么是浅拷贝
上面提到,编译器生成的拷贝构造函数,会对内置类型完成浅拷贝,浅拷贝就是以字节的方式,把一个字节里的内容直接拷贝到另一个字节中。
📖拷贝构造函数可以不写嘛?
通过上面的分析可以得出:编译器自己生成的构造函数对内置类型和自定义类型都做了处理。那是不是意味着我们就可以不写拷贝构造函数了呢?答案是否定的,对于日期类,我们确实可以不写,用编译器自己生成的,但是对于一些需要深拷贝的对象,构造函数是非写不可的。栈就是一个典型的需要我们自己写构造函数的例子
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; } void Push(const DataType& data) { // CheckCapacity(); _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; }
上面定义了一个栈类Stack,我们没有写它的拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,栈中的成员变量都是内置类型,默认的拷贝构造函数会对这三个成员变量都完成值拷贝(浅拷贝)。
此时浅拷贝的问题在于:对象s1和对象s2中的_array存的是同一块空间的地址,他俩指向了同一块空间,当程序退出,往s1或s2中的任意一个对象push值,另一个也会跟着改变。s1和s2要销毁,s2先销毁,s2销毁时调用析构函数,已经将0X11223344这块空间释放了,但是s1并不知道,到s1销毁的时候,会将0X11223344这块空间再释放一次,一块内存空间多次释放,最终就会导致程序崩溃。
📖深拷贝
通过上面的分析可以看出,简单的浅拷贝不能满足栈的需求,因此,对于栈,我们需要自己写一个拷贝构造函数,来实现深拷贝,深拷贝就是去堆上重新申请一块空间,把s1中_array指向的空间中的内容,拷贝到新申请的空间,再让s2中的_array指向该空间。
//自己写的拷贝构造函数,实现深拷贝 Stack(const Stack& st) { DataType* tmp = (DataType*)malloc(sizeof(DataType) * st._capacity); if (nullptr == tmp) { perror("malloc申请空间失败"); return; } memcpy(tmp, st._array, sizeof(DataType) * st._size); _array = tmp; _size = st._size; _capacity = st._capacity; }
📖总结:
类中如果没有涉及资源申请时,拷贝构造函数写不写都可以;一旦涉及到资源申请时,拷贝构造函数是一定要写的,否则就是浅拷贝,最终析构的时候,就会释放多次,造成程序崩溃。
📖拷贝构造函数典型的调用场景:
使用已存在对象创建新对象。
函数参数类型为类类型对象。
函数返回值为类类型对象。
class Data { public: Data(int year = 1, int month = 1, int day = 1) { cout << "调用构造函数:" << this << endl; cout << endl; _year = year; _month = month; _day = day; } Data(const Data& d) { cout << "调用拷贝构造:" << this << endl; cout << endl; _year = d._year; _month = d._month; _day = d._day; } ~Data() { cout << "~Data()" << this << endl; cout << endl; } private: int _year; int _month; int _day; //可以不用写析构,因为全是自定义类型,并且没有动态申请的空间,这三个成员变量会随着对象生命周期的结束而自动销毁 }; Data Text(Data x) { Data tmp; return tmp; } int main() { Data d1(2023, 4, 29); Text(d1); return 0; }
📖总结:
自定义类型在传参的时候,形参最好用引用来接收,这样可以避免调用拷贝构造函数,尤其是深拷贝的时候,会大大的提高效率,函数返回时,如果返回的对象在函数栈帧销毁后还在,最好也用引用返回。
🎁结语:
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!