【C++】类的默认成员函数下(内含日期类的实现)
文章目录
4.拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
拷贝构造的特点:
1.拷贝构造函数是构造函数的一个重载(无返回值)。
2.拷贝构造函数的第一个参数必须是当前类类型对象的引用(最好加上const,防止权限放大的问题),使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
3.C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造函数。
4.若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
5.像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。
像Stack这样的类,虽然也都是内置类型,但是_a指向了资源(动态分配的空间),编译器自动生成的拷贝构造完成的值拷贝/浅拷贝会有两个缺点(1)一个对象的修改,会影响另一个对象(比如给Stack st2拷贝Stack st1的内容,如果对st1出栈/入栈操作,会影响st2)
(2)析构时,释放两次空间(st2释放_a指向的空间,st1会再次释放)
所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造。
这里还有一个小技巧,如果一个类显示实现了析构并释放资源(说明有资源需要显式释放),那么它就需要显式写拷贝构造,否则就不需要。
6.传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。
但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
#include<iostream>usingnamespace std;classDate{public:Date(int year =1,int month =1,int day =1){ _year = year; _month = month; _day = day;}// 编译报错:error C2652: “Date”: 非法的复制构造函数: 第一个参数不应是“Date”//Date(Date d)Date(const Date& d){ _year = d._year; _month = d._month; _day = d._day;}//用指针也可以,但是这只是一个普通的构造函数,不是拷贝构造函数Date(Date* d){ _year = d->_year; _month = d->_month; _day = d->_day;}voidPrint(){ cout << _year <<"-"<< _month <<"-"<< _day << endl;}private:int _year;int _month;int _day;};voidFunc1(Date d){ cout <<&d << endl; d.Print();}// Date Func2() Date&Func2(){ Date tmp(2024,7,5); tmp.Print();return tmp;}intmain(){ Date d1(2024,7,5);//C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里传值传参要调用拷贝构造//所以这里的d1传值传参给d要调用拷贝构造完成拷贝Func1(d1); cout <<&d1 << endl;// 这里可以完成拷贝,但是不是拷贝构造,只是一个普通的构造 Date d2(&d1); d1.Print(); d2.Print();//这样写才是拷贝构造,通过同类型的对象初始化构造,而不是指针 Date d3(d1); d2.Print();// Func2返回了一个局部对象tmp的引用作为返回值// Func2函数结束,tmp对象就销毁了,相当于了一个野引用 Date ret =Func2(); ret.Print();return0;}//以下都是调用拷贝构造 Stack st4(st3); Stack st5=st3;//由Date ret = Func2();得来#include<iostream>usingnamespace std;typedefint STDataType;classStack{public:Stack(int n =4){ _a =(STDataType*)malloc(sizeof(STDataType)* n);if(nullptr== _a){perror("malloc申请空间失败");return;} _capacity = n; _top =0;}Stack(const Stack& st){// 需要对_a指向资源创建同样大的资源再拷贝值 _a =(STDataType*)malloc(sizeof(STDataType)* st._capacity);if(nullptr== _a){perror("malloc申请空间失败!!!");return;}memcpy(_a, st._a,sizeof(STDataType)* st._top); _top = st._top; _capacity = st._capacity;}voidPush(STDataType x){if(_top == _capacity){int newcapacity = _capacity *2; STDataType* tmp =(STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if(tmp ==NULL){perror("realloc fail");return;} _a = tmp; _capacity = newcapacity;} _a[_top++]= x;}~Stack(){ cout <<"~Stack()"<< endl;free(_a); _a =nullptr; _top = _capacity =0;}private: STDataType* _a; size_t _capacity; size_t _top;};// 两个Stack实现队列classMyQueue{public:private: Stack pushst; Stack popst;};intmain(){ Stack st1; st1.Push(1); st1.Push(2);// Stack不显式实现拷贝构造,用自动生成的拷贝构造完成浅拷贝// 会导致st1和st2里面的_a指针指向同一块资源,析构时会析构两次,程序崩溃 Stack st2 = st1; MyQueue mq1;// MyQueue自动生成的拷贝构造,会自动调用Stack拷贝构造完成pushst/popst的拷贝,只要Stack拷贝构造自己实现了深拷贝,他就没问题 MyQueue mq2 = mq1;return0;}5.赋值运算符重载
5.1 运算符重载
• 当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
• 运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
• 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
• 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
• 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
• 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
•

注意以上5个运算符不能重载。(选择题里面常考,大家要记一下)
.* 符号补充演示:
voidfunc(){ cout<<"func()"<<endl;}classA{public:voidfunc2(){ cout<<"A::func()"<<endl;}};intmain(){void(*pf1)()=func1;//调用(*pf)();void(A::*pf2)()=&A::func2;//调用 A aa;(aa.*pf2)();return0;}• 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)
• 一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意义,但是重载operator*就没有意义。
• 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
• 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。
#include<iostream>usingnamespace std;// 编译报错:“operator +”必须至少有一个类类型的形参intoperator+(int x,int y){return x - y;}#include<iostream>usingnamespace std;classDate{public:Date(int year =1,int month =1,int day =1){ _year = year; _month = month; _day = day;}voidPrint(){ cout << _year <<"-"<< _month <<"-"<< _day << endl;}//private:int _year;int _month;int _day;};booloperator==(const Date& d1,const Date& d2){return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;}intmain(){ Date d1(2024,7,5); Date d2(2024,7,6);// 运算符重载函数可以显示调用operator==(d1, d2);// 编译器会转换成 operator==(d1, d2); d1 == d2;return0;}重载函数作用域在全局时无法访问私有成员变量
有几种方法可以解决:
1、成员放公有
这个方法得不偿失
#include<iostream>usingnamespace std;classDate{public:Date(int year =1,int month =1,int day =1){ _year = year; _month = month; _day = day;}voidPrint(){ cout << _year <<"-"<< _month <<"-"<< _day << endl;}booloperator==(const Date& d){return _year == d._year && _month == d._month && _day == d._day;} Date&operator++(){ cout <<"前置++"<< endl;//...return*this;} Date operator++(int){ Date tmp; cout <<"后置++"<< endl;//...return tmp;}private:int _year;int _month;int _day;};intmain(){ Date d1(2026,3,6); Date d2(2024,3,6);// 运算符重载函数可以显示调用 d1.operator==(d2);// 编译器会转换成 d1.operator==(d2); d1 == d2;// 编译器会转换成 d1.operator++();++d1;// 编译器会转换成 d1.operator++(0); d1++;return0;}5.2 赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
赋值运算符重载的特点:
1.赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引用,否则会传值传参会有拷贝
2.有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
3.没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
4.像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。
像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载,也不需要我们显示实现MyQueue的赋值运算符重载。
这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。
classDate{public:Date(int year =1,int month =1,int day =1){ _year = year; _month = month; _day = day;}Date(const Date& d){ cout <<" Date(const Date& d)"<< endl; _year = d._year; _month = d._month; _day = d._day;}// 传引用返回减少拷贝// d3 = d5; Date&operator=(const Date& d){// 检查自己给自己赋值的情况if(this!=&d){ _year = d._year; _month = d._month; _day = d._day;}// d3 = d5表达式的返回对象应该为d1,也就是*thisreturn*this;}voidPrint(){ cout << _year <<"-"<< _month <<"-"<< _day << endl;}private:int _year;int _month;int _day;};intmain(){ Date d1(2024,7,5); Date d2(d1); Date d3(2024,7,6); d1 = d3= d5;// 需要注意这里是拷贝构造,不是赋值重载 Date d4 = d1;return0;}6.取地址运算符重载
6.1 const成员函数
1.将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。const不能修饰全局函数(可以理解为没有this指针
2.1 const修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
2.2 const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this
总结:不修改成员变量的成员函数都应该加上const,这样普通对象和const对象都可以调用这个const成员函数
#include<iostream>usingnamespace std;classDate{public:Date(int year =1,int month =1,int day =1){ _year = year; _month = month; _day = day;}// void Print(const Date* const this) constvoidPrint()const{ cout << _year <<"-"<< _month <<"-"<< _day << endl;}private:int _year;int _month;int _day;};intmain(){// 这里非const对象也可以调用const成员函数是一种权限的缩小 Date d1(2024,7,5); d1.Print();const Date d2(2024,8,5); d2.Print();return0;}6.2 取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。
除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。
classDate{public://d2调用这个 Date*operator&(){returnthis;// return nullptr;}//d1调用这个const Date*operator&()const{returnthis;// return nullptr;}private:int _year ;// 年int _month ;// 月int _day ;// 日};intmain(){const Date d1(2025,9,1);const Date* p1=&d1; Date d2(2025,9,8); Date* p2=&d2; cout<<p1<<""<<p2<<endl;return0;}7. 日期类实现
//0+2次拷贝 Date& Date::operator+=(int day){if(day <0){return*this-=-day;} _day += day;while(_day >GetMonthDay(_year, _month)){ _day -=GetMonthDay(_year, _month);++_month;if(_month ==13){++_year; _month =1;}}return*this;} Date Date::operator+(int day)const{ Date tmp =*this; tmp += day;return tmp;}//反过来 1+2次拷贝 Date& Date::operator+=(int day){*this=*this+day;return*this;} Date Date::operator+(int day){if(day <0){return*this-=-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._year; tmp._month=1;}}return tmp;}所以选上面那种方式
d1-d2(日期相减)
1.最简单直接办法:二者差的年数*365+跨越闰年数+(二者距自己当前年1月1号差的天数相减)
2.小的日期不断++,直到和大的日期相等,++了多少次,他们就相差多少天
总的代码演示:
//Data.h#pragmaonce#include<iostream>usingnamespace std;#include<assert.h>classDate{// 友元函数声明(也可以写在public,也可以写在private,它实质不属于这个类)// 友元函数可以访问私有变量friend ostream&operator<<(ostream& out,const Date& d);friend istream &operator>>(istream & in, Date & d);public:Date(int year =1900,int month =1,int day =1);voidPrint()const;// 直接定义类里面,他默认是inline// 频繁调用intGetMonthDay(int year,int month)const{assert(month >0&& month <13);//用static修饰更好staticint monthDayArray[13]={-1,31,28,31,30,31,30,31,31,30,31,30,31};// 365天 5h +if(month ==2&&(year %4==0&& year %100!=0)||(year %400==0)){return29;}else{return monthDayArray[month];}}boolCheckDate();booloperator<(const Date& d)const;booloperator<=(const Date& d)const;booloperator>(const Date& d)const;booloperator>=(const Date& d)const;booloperator==(const Date& d)const;booloperator!=(const Date& d)const;// d1 += 天数 Date&operator+=(int day); Date operator+(int day)const;//不修改本身的值,可以加const// d1 -= 天数 Date&operator-=(int day); Date operator-(int day)const;// d1 - d2intoperator-(const Date& d)const;// ++d1 -> d1.operator++() Date&operator++();// d1++ -> d1.operator++(0)// 为了区分,构成重载,给后置++,强行增加了一个int形参// 这里不需要写形参名,因为接收值是多少不重要,也不需要用// 这个参数仅仅是为了跟前置++构成重载区分 Date operator++(int); Date&operator--(); Date operator--(int);//不能直接cout<<d;// 流插入//调用代码:d1.operator<<(cout);或者d1<<cout;// 不建议,因为Date* this占据了一个参数位置,使用d<<cout不符合//void operator<<(ostream& out)//{// out << _year << "/" << _month << "/" << _day << '\n';//}private:int _year;int _month;int _day;};// 重载,放成全局函数,这样就不隐含this指针 ostream&operator<<(ostream& out,const Date& d);//注意不加const istream&operator>>(istream& in, Date& d);// Date.cpp#include"Date.h"boolDate::CheckDate(){if(_month <1|| _month >12|| _day <1|| _day >GetMonthDay(_year, _month)){returnfalse;}else{returntrue;}}Date::Date(int year,int month,int day){ _year = year; _month = month; _day = day;//检查必须在赋值之后,因为检查函数只有隐含的this指针,会访问_day等if(!CheckDate()){ cout <<"日期非法"<< endl;}}voidDate::Print()const{ cout << _year <<"-"<< _month <<"-"<< _day << endl;}//d1=d2bool Date::operator==(const Date& d)const{return _year == d._year && _month == d._month && _day == d._day;}// d1 < d2//把所有为真的情况找出来,剩下的就是假的bool Date::operator<(const Date& d)const{if(_year < d._year){returntrue;}elseif(_year == d._year){if(_month < d._month){returntrue;}elseif(_month == d._month){return _day < d._day;}}returnfalse;}// d1 <= d2bool Date::operator<=(const Date& d)const{return*this< d ||*this== d;}//可以把<改为>,但是下面这样更好,复用度更高bool Date::operator>(const Date& d)const{return!(*this<= d);}bool Date::operator>=(const Date& d)const{return!(*this< d);}bool Date::operator!=(const Date& d)const{return!(*this== d);}// d1 += 50// d1 += -50 Date& Date::operator+=(int day){if(day <0){return*this-=-day;} _day += day;while(_day >GetMonthDay(_year, _month)){ _day -=GetMonthDay(_year, _month);++_month;if(_month ==13){++_year; _month =1;}}return*this;} Date Date::operator+(int day)const{ Date tmp =*this; tmp += day;return tmp;}// d1 -= 100 Date& Date::operator-=(int day){if(day <0){return*this+=-day;} _day -= day;//day=0也不合法while(_day <=0){--_month;if(_month ==0){ _month =12; _year--;}// 借上一个月的天数 _day +=GetMonthDay(_year, _month);}return*this;} Date Date::operator-(int day)const{ Date tmp =*this; tmp -= day;return tmp;}// 为了区分,构成重载,给后置++,强行增加了一个int形参//++d1,返回的是拷贝后,尽量用前置++ Date& Date::operator++(){*this+=1;return*this;}// 这里不需要写形参名,因为接收值是多少不重要,也不需要用// 这个参数仅仅是为了跟前置++构成重载区分// d1++,返回的是拷贝前,因为是局部对象不能用引用返回 Date Date::operator++(int){ Date tmp(*this);*this+=1;return tmp;} Date& Date::operator--(){*this-=1;return*this;} Date Date::operator--(int){ Date tmp(*this);*this-=1;return tmp;}// d1 - d2int Date::operator-(const Date& d)const{//假设前一个大于后一个 Date max =*this; Date min = d;int flag =1;//如果前一个小,最后结果为负if(*this< d){ max = d; min =*this; flag =-1;}int day =0;while(min != max){++min;++day;}return day * flag;}//第一个参数也可以写成_cout,不建议写成cout,与库里面的cout最好区分开来 ostream&operator<<(ostream& out,const Date& d){ out << d._year <<"年"<< d._month <<"月"<< d._day <<"日"<< endl;return out;//返回out可以连续输出:cout<<d1<<d2;} istream&operator>>(istream& in, Date& d){while(1){ cout <<"请依次输入年月日:>"; in >> d._year >> d._month >> d._day;if(d.CheckDate()){break;}else{ cout <<"日期非法,请重新输入:"<< endl;}}return in;}// Test.cpp#include"Date.h"voidTestDate1(){//Date d1(2025,9,8);//d1+=100;//d1.Print();//Date d2=d1+100;//d2.Print();//d1-=50;//d1.print();//Date d3=d1-50;//d3.print();// 这里需要测试一下大的数据+和- Date d1(2025,9,8); Date d2 = d1 +2000; d1.Print(); d2.Print(); Date d3(2024,4,14); Date d4 = d3 -5000; d3.Print(); d4.Print(); Date d5(2024,4,14); d5 +=-5000; d5.Print();}voidTestDate2(){ Date d1(2025,9,8); Date d2 =++d1; d1.Print(); d2.Print(); Date d3 = d1++; d1.Print(); d3.Print(); Date d5=--d1;//d1.operator--(); d1.Print(); d5.Print(); Date d4 = d1--;//d1.operator--(10); d1.Print(); d4.Print();}voidTestDate3(){ Date d1(2026,3,4); Date d2(2026,7,15);int n = d1 - d2; cout << n << endl;}voidTestDate4(){ Date d1(2026,3,4); Date d2 = d1 +3000;// operator<<(cout, d1) cout << d1; cout << d2; cin >> d1 >> d2; cout << d1 << d2;}voidTestDate5(){const Date d1(2024,4,14); d1.Print();//d1 += 100; d1 +100; Date d2(2024,4,25); d2.Print(); d2 +=100; d1 < d2; d2 < d1;}intmain(){//TestDate1();//TestDate2();//TestDate3();TestDate4();//TestDate5();return0;}