4.拷贝构造函数
1.概念
首先要知道,拷贝构造函数也是构造函数的重载。
那在创建对象时,可否创建一个与已存在对象一模一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用。
举例:日期类:
class Date { public: //构造函数 Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void print() { cout << _year << " " << _month << " " << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2022, 10, 10); Date d2(d1); //拷贝d1 d1.print(); d2.print(); return 0; }
2.特性
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。函数名相同,参数不同。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
发生递归的原因:
Date(Date d) //去掉引用,就是传值调用
{
_year = d._year;
_month = d._month;
_day = d._day;
}
传值调用:当调用拷贝构造函数时,形参是实参的一份临时拷贝
调用拷贝构造Date d2(d1);,先得传参,传参调用完拷贝构造以后,本身又要调用拷贝构造,而后又要传参,然后一直自己调用自己,导致一直递归下去......
那么传地址可不可以?可以,但不就麻烦了吗?
Date(Date *d) //去掉引用,就是传值调用
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
所以还是引用是最好的方式!
3.拷贝构造函数只有单个形参,该形参是对本类类型对象的引用,一般常用const修饰
为什么要加const呢?
Date(const Date & d) //去掉引用,就是传值调用
{
_year = d._year;
d._month=_month ; //有时候会写反,而改变了原本对象的值。
_day = d._day;
}
Date d2(d1); //拷贝d1
所以加了const以后,就不能改变d1原对象了
4. 若未显示定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
义类型是调用其拷贝构造函数完成拷贝的。所以内置类型处理,自定义类型不处理,自定义类型可能复杂多变,需要自己写拷贝函数,这样就叫 深拷贝。
浅拷贝: 自己没有写,使用默认的 (日期类)
class Date { public: //构造函数 Date(int year = 1900, int month = 1, int day = 1) { cout << "构造函数" << endl; //验证调用所需 _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1(2022, 10, 10); Date d2(d1); //拷贝d1 return 0; }
是可以实现我们想要的功能的,所以不需要我们自己写。
举例:(栈类)
class Stack { public: Stack(int capacity = 4) { cout << "Stack(int capacity = 4)" << endl; _a = (int*)malloc(sizeof(int)*capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } _top = 0; _capacity = capacity; } //析构函数 ~Stack() { cout << "~Stack()" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } void Push(int x) { // .... // 扩容 _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); Stack s2(s1); }
当使用默认拷贝函数时,是否能实现我们想要的需求:
这是怎么了??编译器自己提供的默认拷贝构造函数居然把s1拷贝到s2类中时,两个类中的指针变量指向了同一块空间!!
那么,栈的特性就是先进后出,所以s2先调用析构函数,把空间清理了,然后s1再去调用析构函数,又去清理,这不是瞎搞嘛,造成内存重复释放!
所以默认的拷贝构造函数解决不了我们的需求,需要我们自己写。
//st2(st1)拷贝构造 Stack(const Stack& st) { cout << "Stack(const Stack& st)" << endl; _a = (int*)malloc(sizeof(int)*st._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, sizeof(int)*st._top); _top = st._top; _capacity = st._capacity; }
即解决了指针重复指向一块空间,又把已经创建好的s1对象和其中的内容都拷贝到了s2。
总结:需要写析构函数的类,都需要写深拷贝的拷贝构造,如:Stack
不需要写析构函数的类,默认生成的浅拷贝的拷贝构造就可以用 。如:Date/MyQueue
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
5.运算符重载
1.为什么要重载运算符
为了让自定义类型可以使用 运算符
对于日期类,我们会经常用到,某个日期加多少天来得到另一个日期。
所以日期需要加减,也需要比较,那么使用+ = - ,在C++中,就需要运算符重载,与函数重载没有关系。
2.赋值运算符重载格式
返回值+operator+运算符 +(所需参数),参数个数就是参与运算符的参数个数
class Data { public: Data(int year=1,int month=1,int day=1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; bool operator==(Data& d1,Data& d2) { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; } int main() { Data d1(2022, 10, 11); Data d2(2023, 1, 1); d1==d2;//可以显式调用 operator==(d1,d2); cout << (d1 == d2) << endl; cout << (d1 > d2) << endl; }
1.定义运算符重载时,参数为引用的好处是,不需要调用拷贝函数,若是传值,参数为对象,则需要拷贝对象,调用拷贝函数。
2.定义在类外,需要将类的成员变量设置为公开,会造正麻烦,那么我们直接就定义到了类的内部。
3.若直接放入类的内部定义运算符重载,则会出现问题:
class Data { public: Data(int year=1,int month=1,int day=1) { _year = year; _month = month; _day = day; } bool operator==(Data& d) { return this->_year == d2._year && this->_month == d2._month && this->_day == d2._day; } private: int _year; int _month; int _day; }; int main() { Data d1(2022, 10, 11); Data d2(2023, 1, 1); d1==d2;//可以显式调用 operator==(d1,d2); cout << (d1 == d2) << endl; cout << (d1 > d2) << endl; }
那为什么在类中,显示的是一个参数呢?
bool operator==(Data& d)
{
return this->_year == d2._year
&& this->_month == d2._month
&& this->_day == d2._day;
}