默认成员函数
日期类的框架是这样的:
class Date
{
public:
//获取某月的天数
int GetMonthDay(int year, int month);
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//全缺省的构造函数
Date(int year = 1990, int month = 1, int day = 1)
{
}
//拷贝构造函数
Date(const Date& d)
{
}
//赋值运算符重载函数
Date& operator=(const Date& d) //赋值运算符重载返回引用是为了能够连续赋值 -- d1 = d2 = d3
{
}
//析构函数
~Date()
{
}
//日期+=天数
Date& operator+=(int day); //是否返回引用
//日期+天数
Date operator+(int day); //是否返回引用
//日期-=天数
Date& operator-=(int day);
//日期-天数
Date operator-(int day);
//前置++返回++之后的值, 后置++返回++之前的值
//前置++
Date operator++();
//后置++
Date operator++(int);
//前置--
Date operator--();
//后置--
Date operator--(int);
bool operator>(const Date& d);
bool operator==(const Date& d);
bool operator>=(const Date& d);
bool operator<(const Date& d);
bool operator<= (const Date& d);
bool operator!=(const Date& d);
//日期-日期 返回天数
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
需要完成的函数如上所示。
但是,对于默认成员函数,内置类型编译器会自动生成。在日期类中,_year,_month,_day都是int类型,所以其实不需要写构造函数,拷贝构造函数,赋值运算符重载和析构函数。但是为了学习的必要,这里还是要显示一下。
构造函数
构造函数就是完成一个初始化的工作,在本例的代码中,是需要完成一个全缺省的构造函数。我们可以这样写
Date(int year = 1990, int month = 1, int day = 1)
{
//cout << "Date(int year = 1990, int month = 1, int day = 1)" << endl;
_year = year;
_month = month;
_day = day;
}
注意 : year,month,day都是形参,_year,_month,_day是实参。这就是一个很简单的函数赋值。当我们调用且传值的时候,例如这样:Date d1(2023,9,18),这时对象就是2023,9,18,但是如果我们不传值,就会自动使用缺省值,也就是1990,1,1。
拷贝构造函数
在写拷贝构造函数之前,我们首先需要知道拷贝构造函数是做什么的。
拷贝构造函数是用同一个类的不同实例去初始化这个类的另一个实例。
对于拷贝构造函数,与构造函数类似的:如果是内置类型,系统会自动生成一个拷贝构造函数,但是如果是自定义类型就需要自己写。这里涉及到一个深浅拷贝的问题:如果只是单纯的赋值,像本文章中的日期类一样,我们其实可以不需要自己写拷贝构造函数,而只需要系统自己生成的就可以。但是如果是对象还有指针、堆这样的内容时,我们就需要自己写拷贝构造函数,完成深拷贝。
拷贝构造函数是这样的Date (const Date& d),注意这里的const Date& d,中必须带有&。原因是:如果不带引用,每次使用拷贝构造的时候就会去调用新的拷贝构造,最后造成无穷递归。
说回最开始的内容,拷贝构造该怎么写呢?
因为这里是日期类,成员变量都是内置类型,所以就和构造函数一样写就行了。
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
赋值运算符重载函数
首先明白为什么要有赋值运算符重载函数 : 为了提高代码的可读性,用户可以让编译器按照指定的规则对自定义类型对象直接进行一些运算符操作。比如d1 = d2, d2 > d3这样的。
这里的赋值运算符重载和上面的同理,都可以使用编译器自动生成的。但是我们还是要显示的写。
//赋值运算符重载函数
Date& operator=(const Date& d) //赋值运算符重载返回引用是为了能够连续赋值 -- d1 = d2 = d3
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
针对上面的代码,回答两个问题 : 1.为什么返回的是Date& 2.为什么传参的时候需要传引用,也就是const Date& d
问题一回答:因为在返回的时候,我们返回的是*this,*this不随着函数的销毁而销毁,不会传回脏数据,所以可以使用引用返回,这样的效率比较高。
问题二回答:如果传的是cosnt Date&,那么就不需要生成临时对象,这样的传送的效率高。
析构函数
析构函数也是一样,如果不写,编译器会自动生成。但是只针对不需要销毁堆空间这样的内置类型。
~Date()
{
//cout << "~Date()" << endl;
_year = 0;
_month = 0;
_day = 0;
}
运算符重载
完成上面的默认成员函数之后,就可以开始写运算符重载函数了。
根据上面的要求,需要完成的运算符重载函数有+=、+、-=、-、前置++、后置++、前置--、后置--、>、<、==、!=、>=、<=以及日期-日期返回天数。
Date& operator+=(int day) 日期+=天数 Date operator+=(int day) 日期+天数
写一个函数,需要在意的就是,返回值,参数,函数体。
返回值:对于这个函数,需要思考的问题是应该返回Date&还是应该返回Date?或者说返回void
如果要回答这个问题,就需要知道返回Date&和返回Date有什么区别:其实返回Date&还是Date都是一样的,但是如果返回的是Date&,就不需要再创建临时对象而浪费空间了。虽然返回引用是有好处的,但是如果返回引用的时候,对象随着函数的销毁而销毁了,那么再在外部调用的时候,就是脏数据。所以我们需要格外注意的是:如果返回值会因为函数的销毁而销毁,就不能返回引用。
回答了上面的问题,现在返回函数本身。我们的需求是做一个日期+=天数的函数。那么是否可以连续+=呢?例如:是否有这样一种情况c+=9+=1;这样的情况完全存在。还有,在下一个例子中:c=0.c+=19;c=? 我们能很轻易的回答出来,c = 19。也就是说,在这个过程中,c的值改变了,之前的0改变为现在的19。 反观Date operator+(int day)函数,例如:c = 0. c + 19;在这个例子中,c还是0,并没有改变。而连续的+也存在。
上面说这么多是为了说明什么?
关于是否能连续+=、+是为了确定需要实现的函数是返回void 还是有另外的返回值。
关于是否改变参数本身,是为了确定返回的是临时变量(如果返回临时变量就意味着不改变参数本身,例如:c = 0,c+19,, --> c = 0)还是this指针(*this代表的是对象本身,如果返回对象本身,就代表着需要改变对象,例如c = 0,c+=19,, --> c = 19)
这里有一条逻辑链,具体可以看下面画的图
解释了这么多,现在到了揭晓答案的时候,如果写日期+=天数,需要返回的是Date&还是Date?返回的是临时变量还是*this?
写日期+=天数函数,返回Date&,*this。
写日期+天数函数,返回Date,临时变量。
解决掉上面的问题之后,函数很容易就写出来了
Date& Date::operator+=(int 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)
{
Date tmp(*this);
tmp += day;
return tmp;
}
Date& operator-=(int day) 日期-=天数 Date operator-=(int day) 日期-天数
此函数和日期+=天数函数是一样的,直接上代码
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
--_month;
_day += GetMonthDay(_year, _month);
if (_month == 0)
{
--_year;
_month = 12;
}
}
return *this;
}
Date Date::operator-(int day)
{
Date tmp = *this; // 等价于 Date tmp(*this)
tmp -= day;
return tmp;
}
前置++,后置++,前置--,后置--
c++中该如何区分函数重载中的前置++和后置++:运用函数重载。
在我们的日常使用中,是运用前置++多还是运用后置++多?毫无疑问的,是运用前置++多。所以基于这样的基础,在区分前置++和后置++中运用函数重载的时候,就是对后置++进行修改。
前置++ Date operator++()
后置++ Date operator++(int)
注意,在后置++中的int仅仅是用于占位,并不需要传值,还是像往常那样直接用后置++就行了,只不过这样编译器会自动识别。
在解决了函数重载的问题之后,还有一个问题等着我们。
前置++和后置++本质的区别是什么呢?
前置++返回++之后的值,后置++返回++之前的值。
例如: c = 1; int b = ++ c; 返回的结果是 b = 2; c = 2;
c = 1; int b = c ++; 返回的结果是 b = 1; c = 2。
前置++ :如何先改变自身再返回?
Date Date::operator++()
{
*this += 1;
return *this;
}
返回的值是先+=1之后再返回,这样返回的是就是++之后的值。
后置++:如何先返回再改变自身
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
先用tmp拷贝*this,再*this += 1; 最后返回的是tmp。
例如: c = 1; int b = c; c += 1; return b;
在上述代码中,b是c的拷贝,c改变了,但是b没有改变,最后返回的是b。
前置--和后置--都是一样的道理。
>=、==、>、<、<=、!=运算符重载
在这些运算符重载中,最需要注意的是复用。先写好>和==,>=就可以复用剩下所有的。
bool Date::operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year && _month > d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day > d._day)
{
return true;
}
else
{
return false;
}
}
bool Date::operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return true;
}
else
{
return false;
}
}
bool Date::operator>=(const Date& d)
{
return operator>(d) || operator==(d);
}
bool Date::operator<(const Date& d)
{
return !operator>=(d);
}
bool Date::operator<=(const Date& d)
{
return !operator>(d);
}
bool Date::operator!=(const Date& d)
{
return !operator==(d);
}
日期-日期---返回天数
int Date::operator-(const Date& d) // 日期-日期 -- 返回天数
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (max != min)
{
++min;
++n;
}
return n * flag;
}