目录
前言
本文主要给大家讲解有关于拷贝构造函数的重点以及给后面运算符重载的知识开个小头,这两个都是很重要的内容,并且它们使用的情况并不明显,常常出现在细微末节的地方。
一.拷贝构造函数
1,1抛砖引玉
class Date { public: //用于给定数字的初始化构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; //设置一个专门用来接收类的函数 void fun(Date d) { d.Print(); } int main() { Date d1(2024, 5, 24); fun(d1);//2024-5-24 return 0; }
上述代码是一个很常见的类Date,唯一有点不同的就是我们是用类来作为参数进行函数的调用~
这也印证了一件事,类或结构体都是可以进行传参的,而在以前的学习中之所以都用指针去替代它们无非是效率问题,传参时指针不是4字节就是8字节,而结构体得看自身大小。
其次是指针传参是传址拷贝,而结构体与类是传值拷贝,离开作用域就销毁了,无法改变外面的实参。
回归到我们的代码中可以看到,d1作为实参传递给d,这是典型的传值拷贝又称浅拷贝,在Date类中无伤大雅,但若是换作Stack类那就会出现运行崩溃了~
对于栈而言,里面的成员变量无非就以上几个,但其中的a是指向动态开辟空间。如果栈也跟Date一样执行传值拷贝,这意味着完全拷贝的st里也会有a指向该处空间。当我们离开作用域时会自动调用析构函数,这时候两个a指向的空间相当于被析构了2次,最后肯定会运行失败~
因此对于栈类而言,这种浅拷贝(传值拷贝)是有很大风险的,如果有深拷贝(连带动态空间也能拷贝)在最后析构时就不会重复析构同一处空间了。而这也是我们学习拷贝构造函数的意义。
1.2特性
- 拷贝构造函数是构造函数的一个重载形式(说白了拷贝构造还是一种初始化,只不过是适用于单一传参的类)——拷贝构造是使用同类对象去初始化创建对象
- 拷贝构造的参数只有一个且必须是类类型对象的引用,若使用传值的方式会引发无穷递归调用
class Date { public: //用于给定数字的初始化构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } //拷贝构造 Date(Date d) { _year = d._year; _month = d._month; _day = d._day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; //设置一个专门用来接收类的函数 void fun(Date d) { d.Print(); } int main() { Date d1(2024, 5, 24); fun(d1);//2024-5-24 Date d2(d1);//编译报错 return 0; }
这里我们有两种拷贝实况(目的都是初始化),其中一种是通过同类对象去初始化所创建的对象。而这本来很好理解,自动去调用构造函数呗(初始化),把d1的成员变量都拷贝给d2。但问题在于当d2准备进入拷贝构造函数时第一步是传参数d1,在Date d接收实参d1的过程何尝不也是一种拷贝?然后刚好它们也都是类对象,这下惨了这时候的拷贝构造函数异常灵敏,反正只要是有拷贝那就通通绕不过它~就这样在Date d接收实参d1这种只能是传值拷贝的情况下它们进入了无限循环,谁也走不出去了。
而另一个fun(d1)也是因为传值问题导致拷贝构造无限递归。所以要想使用拷贝构造一定一定不要用传值拷贝(其实用了也没事,反正语法会检测出错误)。
拷贝构造的正确用法:
//拷贝构造 Date(Date& d) { _year = d._year; _month = d._month; _day = d._day; }
当我们有了引用之后就不会触发拷贝构造的无限递归了,d1传值给d触发拷贝构造,由于有了引用dd变成了d1的别名,这个过程不算是拷贝。然后通过this指针来实现d的初始化。
在c语言中传参可以就只是单纯拷贝,而在c++中规定自定义类型对象传参拷贝必须得调用拷贝构造函数,所以才会搞出这么多"幺蛾子"。
那么我们在明白拷贝构造的基本规则后就来开始解决实际问题咯,比如如何用拷贝构造处理栈类空间重复析构问题~
浅拷贝只适用于日期类,对于栈而言我们要在拷贝构造里用到深拷贝。一般拷贝构造也是去解决深拷贝问题~
错误示范,这样还是浅拷贝,解决不了问题。
正确示范:stt中的a同样开辟一处空间,达到空间拷贝并且确保空间大小与st1一致。然后再进行内容的拷贝,确保a指向的内容与st1一致。最后就是正常的数值拷贝了。
深拷贝所做到的就是在浅拷贝的基础上兼顾空间上的拷贝。
1.3默认拷贝构造
讨论完自定义的拷贝构造自然无可避免要谈到默认拷贝构造了,当我们没有写拷贝构造函数时,编译器对这些类(Date类,Stack类,MyQueue类)又能否调用出它们的默认拷贝构造呢?
当我们开始由编译器去调用默认拷贝构造函数发现对日期类(Date)而言是可以成功拷贝的。
我们没有去自定义MyQueue的拷贝构造发现编译器也可以去调用它的默认拷贝构造。
不过Date与MyQueue不同,对于内置类型成员编译器会调用默认拷贝构造,对自定义类型成员编译器会调用我们自己为其定义的拷贝构造。
所以在MyQueue里其实是间接调用了关于栈的拷贝构造以达成其成员的初始化,因此并不用定义自己的类拷贝构造。
总结一下:栈比较特殊,因为它有在堆上开辟空间因此只能用自定义(手写)的拷贝构造,MyQueue也有在堆上开辟空间,但只要我们不把栈类的拷贝构造屏蔽它就会自行去调用。这样反而还不用我们自己再写一个关于MyQueue的拷贝构造了。
1.4小细节
假如有人把初始化写反了,虽然这样不会出现语法报错,但是拷贝的对象会变成随机值。为了防止需要拷贝的对象被篡改,我们给其加一道保险——const Date& dd。
我们还可以结合引用返回来提高效率,这样可以减少一次拷贝构造
二.赋值运算符重载(略讲)
2.1运算符重载基础内容
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 _year; int _month; int _day; }; int main() { Date d1; Date d2(2024, 5, 24); d1 > d2; d1 == d2; return 0; }
当我们对自定义类型对象使用运算符的时候会出现语法报错。
而我们对内置类型对象却可以使用各种运算符~
为了能使自定义类型对象可以实现对运算符的使用,c++引入了运算符重载~
在正式学习运算符之前我们可以先尝试自己自定义与日常类相关的运算函数:
案例:
bool Equal(Date x, Date y) { return x._year == y._year && x._month == y._month && x._day == y._day; } bool Greater(Date x, Date y) { if (x._year > y._year) { return true; } else if (x._year == y._year && x._month > y._month) { return true; } else if (x._year == y._year && x._month == y._month && x._day > y._day) { return true; } return false; } int main() { Date d1(2024,7,9); Date d2(2024,7,9); cout << Equal(d1, d2) << endl;//1 cout << Greater(d1, d2) << endl;//0 Date d3(2024, 8, 9); Date d4(2024, 7, 9); cout << Equal(d3, d4) << endl;//0 cout << Greater(d3, d2) << endl;//1 return 0; }
而在我们使用了运算符重载后就会变成这样:
案例:
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; } bool operator==(const Date &x, const Date &y) { return x._year == y._year && x._month == y._month && x._day == y._day; } bool operator>(const Date &x, const Date &y) { if (x._year > y._year) { return true; } else if (x._year == y._year && x._month > y._month) { return true; } else if (x._year == y._year && x._month == y._month && x._day > y._day) { return true; } return false; } int _year; int _month; int _day; }; int main() { Date d1(2024, 7, 10); Date d2(2024, 7, 9); bool ret1 = d1 == d2;//0 bool ret2 = d1 > d2;//1 cout << ret1 << ret2 << endl; cout << (d1==d2) << endl;//0 return 0; }
函数名字统一用关键字operator命名然后在其后面接需要重载的的运算符符号,不过还有一点要补充,放在类里面时运算符重载还是会编译不了。
这是因为在成员参数里都会有一个隐藏的参数,this指针。而运算符重载的规则又是参数个数必须匹配,所以我们还要做一下修改。
bool operator==(const Date &y) { return _year == y._year && _month == y._month && _day == y._day; } bool operator>(const Date &y) { if (_year > y._year) { return true; } else if (_year == y._year && _month > y._month) { return true; } else if (_year == y._year && _month == y._month && _day > y._day) { return true; } return false; } int main() { Date d1(2024, 7, 10); Date d2(2024, 7, 9); bool ret1 = d1 == d2;//d1.operator==(d2)->d1.operator==(&d1,d2) bool ret2 = d1 > d2;//d1.operator>(d2)->d1.operator>(&d1,d2) cout << ret1 << ret2 << endl; cout << (d1==d2) << endl;//0 return 0; }
我们所设的运算符只需要2个参数,所以除了隐藏的this指针最后只保留一个即可。
2.2扩展日期类的运算符重载
实现d1+=50的运算符重载
int GetMonthDay(int year, int month) { int montharr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if ((month == 2)&&(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) { return 29; } return montharr[month]; } Date& operator+=( int day) { _day += day;//记录天数 while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); ++_month; if (_month == 13) { _month = 1; _year++; } } return *this; }
这里的返回值也很有讲究,this为d1的地址要用Date*接收,而*this则是d1本身,用Date接收即可。传值返回会遇到拷贝构造,而*this出了作用域还在,生命周期在mian函数里,为防止拷贝所以使用Date& 。(少一次拷贝构造)
如果我们要构造d1+50:
Date operator+(int day) { Date tmp(*this); tmp._day += day;//记录天数 while (tmp._day > GetMonthDay(tmp._year, tmp._month)) { tmp._day -= GetMonthDay(tmp._year, tmp._month); ++tmp._month; if (tmp._month == 13) { tmp._month = 1; tmp._year++; } } return tmp; }
既然我们不追求改变本身,那我们直接拷贝构造一个tmp进行日期类的加法,最后返回的也是tmp,而不是this,tmp出了作用域就会被销毁,因此这里不用引用返回。
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; } bool operator==(const Date &y) { return _year == y._year && _month == y._month && _day == y._day; } bool operator>(const Date &y) { if (_year > y._year) { return true; } else if (_year == y._year && _month > y._month) { return true; } else if (_year == y._year && _month == y._month && _day > y._day) { return true; } return false; } int GetMonthDay(int year, int month) { int montharr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if ((month == 2)&&(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) { return 29; } return montharr[month]; } Date &operator+=( int day) { _day += day;//记录天数 while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); ++_month; if (_month == 13) { _month = 1; _year++; } } return *this; } //可以复用+= Date operator+(int day) { Date tmp(*this); tmp += day; return tmp; } //Date operator+(int day) //{ // Date tmp(*this); // tmp._day += day;//记录天数 // while (tmp._day > GetMonthDay(tmp._year, tmp._month)) // { // tmp._day -= GetMonthDay(tmp._year, tmp._month); // ++tmp._month; // if (tmp._month == 13) // { // tmp._month = 1; // tmp._year++; // } // } // return tmp; //} int _year; int _month; int _day; }; int main() { Date d1(2023, 10, 22); Date d2(2023, 1, 1); d1 +=50; d1.Print();//2023,10,22 d2 + 50; d2.Print();//2023,1,1 Date ret = d2 + 50; ret.Print();//2023,2,20 return 0; }