日期类的实现(C++)

目录

注意:

1.作用域

2.实例化        

3.this指针 

4.std 

1).std是什么?

2).为什么将cout放到名字空间std中?

3).std都是什么时候使用?

5.C语言“/”和“%”运算符详解

 6.运算符重载

一、实现日期类需要注意日期的合法性

二、日期类的实现

1.打印日期

 2.拷贝构造函数

3.获取每月的天数

4.判断日期是否合法

5.判断闰年

6.构造函数

7. ==运算符重载

8. <运算符重载

9. <=、>、>=、!= 运算符重载

10. +、+=运算符重载

11.-、-=运算符重载

12.前置++、--与后置的++、-- 

13.&运算符重载

 14.const

 15.日期相减

 16.流插入流提取

16.1 对“<<”和“>>”重载的函数形式如下:

16.2 为什么只能将重载“>>”和“<<”的函数作为友元函数或普通的函数,而不能将它们定义为成员函数?

16.3 为什么要声明友元呢?

16.4 为什么这个不写在Data.h里?

三、详细代码

1. Data.cpp

2 Data.h

 3.Test.h

注意:

1.作用域

2.实例化        

        person类是没有空间的,只有person类实例化出来的对象才具有存储类成员变量的实际物理空间 。

3.this指针 

        this指针的类类型是*const

 【C++】std::是什么? - mhq_martin - 博客园 (cnblogs.com)

4.std 

1).std是什么?

        std::    是个名称空间标示符,C++标准库中的函数或者对象都是在命名空间std中定义的,所以我们要使用标准函数库中的函数或对象都要使用std来限定。

         对象cout是标准函数库所提供的对象,而标准库在名字空间中被指定为std,所以在使用cout的时候要加上std::。这样编译器就会明白我们调用的cout是名字空间std中的cout。  

2).为什么将cout放到名字空间std中?

是因为像cout这样的对象在实际操作中或许会有好几个,比如说你自己也可能会不小心定义了一个对象叫cout,那么这两个cout对象就会产生冲突。

3).std都是什么时候使用?

   一般来说,std都是要调用C++标准库时,要写上std;

   使用非标准库文件iostream.h,不用写

5.C语言“/”和“%”运算符详解

原文链接:https://blog.csdn.net/admin_maxin/article/details/53330674

除法运算符"/":

    二元运算符,具有左结合性。参与运算的量均为整型时,结果为整型,舍去小数。如果运算量中有一个为实型,结果为双精度实型。除号的正负取舍和一般的算数一样,符号相同为正,相异为负。例如:

    5/2=2,1/2=0
    5/2.0=2.5   

取模运算符"%":

    二元运算符,具有左结合性。参与运算的量均为整型。并且参与运算的量可以为负数。取模运算的结果等于两个数相除后的余数。

    例如:5%2=1,1%2=1

          5%2.0和5.0%2//error C2297: “%”: 非法,右操作数包含“double”类型

          int a = 23%-3;//a = 2

          int b = -23%3;//b = -2//注明:求余符号的正负取舍和被除数符号相同

    当前面的数小于后面的数时,其实求余运算可以看成(如下),如果a<b的话,这样的商为0,余数就是a

    a%b=a-(int)(a/b)*b 
    1%2=1 
    2%5=2 
    a % b 

这个关系表达式a%b == a-(int)(a/b)*b 是这么解释的:先运算(a/b)然后a-((a/b的值)乘以b) 

例1. 50%2
     50除以2=25
     结果为整数,则取值为0 (原因就是50除以2的值是整数,余数为0) 
例2. 7%2
     7除以2=3.5
     则还是用3乘以2=6
     再用7-6,结果就是余数 1
C语言中,任意一个正整数对1求余结果为0还是1?是0,如24%1=0

 6.运算符重载

https://blog.csdn.net/qq_42683011/article/details/102087764

基础应用:
重载运算符最需要考虑的即为参数与返回值问题

(这里以operator = 为例):

参数:
        一般地,赋值运算符重载函数的参数是函数所在类的const类型的引用, 如:

MyStr& operator =(const MyStr& str);

加const是因为:

        我们不希望在这个函数中对用来进行赋值的“原版”做任何修改。
        加上const,对于const的和非const的实参,函数就能接受;如果不加,就只能接受非const的实参。
用引用是因为:

        这样可以避免在函数调用时对实参的一次拷贝,提高了效率
注意:
        上面的规定都只是推荐,可以不加const,也可以没有引用,甚至参数可以不是函数所在的对象。

返回值:
        一般地,返回值是被赋值者的引用(但有时返回左值还是右值需要相当的考虑),即*this

MyStr& operator =(const MyStr& str);

        这样在函数返回时避免一次拷贝,提高了效率。

        更重要的,根据赋值运算符的从左向右的结合律, 可以实现连续赋值,即类似a=b=c
        如果返回的是值,则执行连续赋值运算后后头得到的将是一个匿名副本, 为不可更改的右值,就是说是const类型, 再执行=c就会出错。

注意:
        这也不是强制的,完全可以将函数返回值声明为void,然后什么也不返回,只不过这样就无法连续赋值,所以具体的返回值与参数的设定完全取决于需求, 是非常值得设计者考量的。

一、实现日期类需要注意日期的合法性

#include<iostream>
using namespace std;
class Data
{
public:
(能被4整除却不能被100整除或能被400整除的年份就是闰年!
取模运算符"%"
除法运算符"/")
	int isLeapYear(int year)
	{
		if ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0))
		{
			return 1;
		}
		return 0;
	}
	int GetMonthDay(int year, int month)
	{
        (用0是为了让31是1月份)
		int MonthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (month == 2 && isLeapYear(year) == 1)
        //平年二月28,闰年29天
		{
			return 29;
		}
		else
		{
			return MonthDayArray[month];//下标与月份是一致的
		}
	}
	Data(int year = 1, int month = 1, int day = 1)
	{
       (考虑时间的合法性,一年只有12个月)
		if (year >= 1 && month <= 12 && month >= 1 && day >= 1
         && day <= GetMonthDay(year, month))
		{
			_year = year;
			_month = month;
			_day = day;
		}
	}
	Data(const Data& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void print()
	{
		cout << _year << "-" << _month << "-" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Data d1(2022, 10, 14);
	Data d2(2022, 10, 50);
	return 0;
}

二、日期类的实现

         如果我们函数定义与实现分开写一定要注意上面所讲的注意点:

 下面将根据我的思路一步步实现日期类:

1.打印日期

	void print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

 2.拷贝构造函数

	Data(const Data& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

3.获取每月的天数

        GetMonthDay会被频繁的调用,加上static,防止不停的开辟数组,直接放在静态区,再加上const,更安全,此外定义数组的时候用0让31是1月份,即让下标与月份一致。

int GetMonthDay(int year, int month);

int Data::GetMonthDay(int year, int month)
{
	assert(year >= 0 && month > 0 && month < 13);
	const static int MonthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	if (month == 2 && isLeapYear(year) == 1)
	{
		return 29;
	}
	else
	{
		return MonthDayArray[month];
	}
}

4.判断日期是否合法

	bool CheckDate()
	{
		if (_year < 1 || _month > 13 || _month < 1 ||
			_day < 1 || _day > GetMonthDay(_year, _month))
		{
			return false;
		}
		return true;
	}

5.判断闰年

	bool isLeapYear(int year)
	{
		if ((year % 400 == 0) || (year % 4 == 0 && year / 100 != 0))
		{
			return true;
		}
		else
		{
			return false;
		}
	}

6.构造函数

Data.h(在类里面声明)

Data(int year = 1, int month = 1, int day = 1);

 Data.cpp(在类外定义)

//声明给了缺省参数,定义就不用给了

Data::Data(int year, int month, int day)
{
	if (year >= 1 && month <= 12 && month >= 1 && day >= 1 
		&& day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
}

  7. ==运算符重载

        注意隐含的this指针:

bool operator==(const Data& d);
bool Data::operator==(const Data& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

为什么可以访问私有?

        因为这个函数虽然在类外面定义,在类里面声明,但是实际他还是在类里面的成员函数,类的成员函数是可以访问私有的,但其实该函数本质上访问的是成员,类里面那个private只是声明,就是说实际上访问的是d1或者d2,注意:变量的声明和定义的区别是是否开辟空间。

8. <运算符重载

        建议成员函数中不修改成员变量的成员函数,都可以加上const,这样普通对象和const对象都能调用。

bool Data::operator<(const Data& d) const//声明和定义分离需要注意
{
	if ((_year < d._year)
		|| (_year == d._year && _month < d._month)
		|| (_year == d._year && _month == d._month && _day < d._day))
	{
		return true;
	}
	else
	{
		return false;
	}
}

9. <=、>、>=、!= 运算符重载

         <=就是<或者=,举个例子,比如d1<d2,日期类比大小,那么实际上就是*this < d,那么<=的运算符重载,我们直接写成下代码即可。同理别的符号也可以复用。

bool operator<=(const Data& d)
	{
		return *this < d || *this == d;
	}
bool operator>=(const Data& d)
	{
		return !(*this < d);
	}
	bool operator!=(const Data& d)
	{
		return !(*this == d);
	}
	bool operator>(const Data& d)
	{
		return !(*this <= d);
	}

        像以上这种几行的代码,不超过三行,直接定义声明放在类里面即可,因为类里面的成员函数默认是内联函数。

10. +、+=运算符重载

	Data operator+(int day) const;

        对于d1+4的思路:比如2022.10.27+4,变成2022.11.1,月加1,日27+4=31-30=1,减掉的是10月的天数 。所以:先进行天数的加,在减去当月的天数,再让月加1

_day -= GetMonthDay(_year, _month);
		_month++;

        但是如果d1+40了,2022.10.27 +40,27+40-30=37>max(31),那么进位就不是进1了,所以需要加入while循环,此时变成2022.11.37,那么37-31=6,进1,月份再加1,变成2022.12.6

        那如果+70,那么相当于在上基础上还要+30,即2022.12.6+30,6+30变成36,36-31 =5,变成2022.13.5,那么肯定不对,月份最多12月,那么需要加上判断,如果月份等于13了,那么就让年进1,让month回位到1。

Data Data::operator+(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}
	return *this;
}

        但是如果上代码这么写,会同时改变d1和d2,d1 + 100  --》 d1.operator+(day),d1就是this,然后我们返回的也是*this,即返回的是d1,即我让d1+100,又赋值给了新的d1,所以d1和d2同时改变了。这样是不是刚好可以作为+=的运算符重载。但是可以优化一下:

思考:出了作用域*this还在不在?在
        可以这样理解:d1还在,,所以加上&,即引用返回会更合适。

//注意d1+=100,自己也会改变,而且有返回值,因为有些地方需要连续的+=
Data& Data::operator+=(int day)
{
    _day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}
	return *this;
}

那么实现+怎么修改了:

        我们先拷贝d1,保存不变,再改变拷贝的d1不就可以了吗?Data ret(*this),拷贝构造函数,一个存在的对象去初始化另一个要创建的对象,然后改变ret即可。

        注意:d1+100,是将d1的地址传给了this,*this得到的就是d1本身了。即调用成员函数时是将对象的地址作为实参传递给 this,比如Data print(Data const* this);


Data& Data::operator+(int day) 
{
	Data ret(*this);//把*this拷贝给ret,再改变ret
	ret._day += day;
	while (ret._day > GetMonthDay(ret._year, ret._month))
	{
		ret._day -= GetMonthDay(ret._year, ret._month);
		ret._month++;
		if (ret._month == 13)
		{
			++ret._year;
			ret._month = 1;
		}
	}
	return ret;
}

    注意传值返回,返回的不是ret,是ret的拷贝,是临时对象,是带有const属性的,传值返回就要调用拷贝构造,做Data(Data& d)拷贝构造的参数,是权限的放大。所以拷贝构造加上了const。

    若加上Data&如上代码一样,虽然可以避免拷贝构造,但是还是会出现问题:

    Data&作为返回值是不可以的,因为ret是创建的临时变量,出了作用域就栈帧销毁了,如果空间被占用,就会出错,如果没有占用倒是和Data做返回值是一样的。

  思考:但是我们已经写了+=了,又写了一边+,代码非常重复,可不可以封装了?可以。


Data& Data::operator+=(int day)
{
	*this = *this + day;//d1 += 100;//这里复用了+即Data Data::operator+(int day)
	return *this;
}

 于是我们再思考一个问题:我是用+去复用+=好,还是用+=复用+更好了?

以下即是用+=去复用+的代码实现:

Data Data::operator+(int day) const
{
	Data ret(*this);
	ret += day;
	return ret;
}

Data& Data::operator+=(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}
	return *this;
}

回答:用+去复用+=好。

        因为实现+,我们需要首先拷贝构造一个新的临时变量ret,在返回的时候又需要返回临时变量ret, 返回临时变量是会创建一个临时空间去保存临时变量的,即+有两次拷贝1.Data ret(*this);拷贝构造 2. return ret;传值返回,临时拷贝。

        故用+=来调用或者说复用+的时候要调用两次拷贝,因为+=本来是没有拷贝的,又平白多出了两次拷贝,费力不讨好。

11.-、-=运算符重载

        结合10对于+与+=运算符重载,这里也用-去复用-=,减少拷贝。

        思路也同上:比如2022.2.7-20,就是7+31(1月份或者说上个月的天数,所以这里和+的区别是先对月-1,而+是后对月+1,)-20 = 18,即变成20222.1.18

         这样下代码就是+的上个月的天数了。

        同样注意,当到1月份,再减少的时候,即若2022.1.18-20,那么月-1变成0,只要变成0,就让月变成12,同样年数也减一,变成2021.12,至于天数还是一样的计算,先18-20=-2,再加上12月的天数31,-2+31=29变成2021.12.29.

Data Data::operator-(int day) const
{
	Data ret = *this;
	ret -= day;
	return ret;
}

Data& Data::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}
	_day -= day;
	while (_day <= 0)
	{
		_month--;
		if (_month == 0)
		{
			_month = 12;
			_year--;
		}
		_day += GetMonthDay(_year, _month);
	}
	
	return *this;
}

         但是需要注意:如果是比如2022.10.27-(-20)的时候变成2022.10.47,这个日期是不合法的,这其实可以变成10.27+20,那么就可以复用+了,就是只要day<0,那么久变成d+(-(day)),因为day是负数,所以相当于d-day。

12.前置++、--与后置的++、-- 

         前置和后置因为都是一样的,所以用int i的参数来区分,(注意i可以不写,形参不写代表着不用这个值或者说不接收)直接写int也可以 规定无参为前置++ ,这里只是改变了函数名的修饰规则以用来区分。

        Data operator++(int i = 0);这样全缺省是错误的,因为i没有实际的意义,只是用来区分前置与后置++,或者说是全缺省和无参的函数在调用时区分不开。

Data& operator++();
Data operator++(int i);
//Data operator++(int);

Data& operator--();
Data operator--(int);

        而实现其实写了上面的+和-就很简单了,前置++是,先++再赋值,后置++是先赋值后++,返回值都是Data&,因为d还在。 所以后置++,需要先拷贝,再改变d,返回拷贝的d。--就同理可得了。

Data& Data::operator++()
{
	*this += 1;
	return *this;
}

//d1++
Data Data::operator++(int i)
{
	Data tmp(*this);
	*this += 1;
	return tmp;
}

Data& Data::operator--()
{
	*this -= 1;
	return *this;
}
Data Data::operator--(int)
{
	Data tmp(*this);
	*this -= 1;
	return tmp;
}

13.&运算符重载

        取地址运算符重载,这是默认成员函数,基本上不用自己写就够用了;

        return nullptr;//除非你不想让别人看到真实的地址或者想让别人看到你指定的地址。

        返回的是地址,所以直接返回this即可,注意用Data*接收就行,此外,注意&运算符重载需要写两个,有const修饰的。

	
	Data* operator&()
	{
	
		return this;
	}
	const Data* operator&() const
	{
		return this;
	}

 14.const

看下面三段测试用例:

void print()实际上是void print(Data* const this)
    对于TestData7中的d1.print()实际上式d.print(&d1),而其类型是Data*,而print上的类型是Data* const,是符合要求的。
    但是在Func中的d.print实际上虽然也是&d,但是类型是const Data*, 从const Data*变成Data* const,涉及到权限的放大,所以报错
    所以要想不报错就要加上const,但是Data* const this是隐含的,是不能显示的表示的,那么怎么加上const了?

         这样就运行完美了!        

         所以:建议成员函数中不修改成员变量的成员函数,都可以加上const,这样普通对象和const对象都能调用。

 笔试题:   

假设 AA 是一个类, AA* abc () const 是该类的一个成员函数的原型。若该函数返回 this 值,当用 x.abc ()调用该成员函数后, x 的值是(D)

A.可能被改变
B.已经被改变
C. 受到函数调用的影响
D.不变

A.将const修饰的类成员函数称之为const成员函数。const修饰类成员函数,实际修饰该成员函数隐含的 this 指针,表明在该成员函数中不能对类的任何成员进行修改。

(1)const 类成员函数中不能修改类的成员变量,const 修饰的是 this 指针指向空间的内容  

(2)四个问题:

(1)const对象可以调用非const成员函数吗?

(2)非const对象可以调用const成员函数吗?

(3)const成员函数内可以调用其它的非const成员函数吗?

(4)非const成员函数内可以调用其它的const成员函数吗

 B.错误,不能被改变

C.x的值在函数内部不受任何影响

D.正确

 15.日期相减

//日期相减,日期减日期返回天数
	int operator-(const Data& d) const;

        这里flag来确定正负,如果未来和现在相减,天数是正的,加上天数即可和大的相等,如果是现在和未来相减,天数是负的。
        //日期相减,日期减日期返回天数

        思路就是,相减就是有大有小,我们暂时不知道我么需要相减的两个日期谁更大,我们先假设现在的日期d为大的,那么就是Data max = *this,你如是2022.10.27-2022.11.11,传入的d是2022.11.11,而*this就是2022.10.27,如果不是*this大,就换一下max和min即可。这样以后就不用管谁大谁小了。

        两个差多少天其实就是min的加多少次可以达到max一样大。注意flag的符号就行。直接++就行,因为会调动运算符重载。


int Data::operator-(const Data& d) const
//Data Data::operator-(int day)构成函数重载,返回值和参数不同
{
	int flag = 1;
	Data max = *this;
	Data min = d;
	if (*this < d)
	{
		min = *this;
		max = d;
		flag = -1;
	}
	int n = 0;
	while (min != max)
	{
		++n;
		++min;
	}
	return n * flag;
}

16.流插入流提取

        在类库提供的头文件中已经对“<<”和“>>”进行了重载,使之作为流插入运算符和流提取运算符,能用来输出和输入C++标准类型的数据。因此,凡是用“cout<<”和“cin>>”对标准类型数据进行输入输出的,都要用#include 把头文件包含到本程序文件中。cin>>等价于cin.operator>>(),即调用成员函数operator>>()进行读取数据。

        当cin>>从缓冲区中读取数据时,若缓冲区中第一个字符是空格、tab或换行这些分隔符时,cin>>会将其忽略并清除,继续读取下一个字符,若缓冲区为空,则继续等待。但是如果读取成功,字符后面的分隔符是残留在缓冲区的,cin>>不做处理。
        而用户自己定义的类型的数据,是不能直接用“<<”和“>>”来输出和输入的。
        如果想用它们输出和输入自己声明的类型的数据,必须对它们重载。  
       C++的流插入运算符“<<”(从流中再读取,进行输出)和流提取运算符“>>”(提取后再进行输入)是C++在类库中提供的。

16.1 对“<<”和“>>”重载的函数形式如下:

        istream& operator>> (istream&, 自定义类&);
        ostream& operator<< (ostream&, 自定义类&);

        即重载运算符“>>”的函数的第一个参数和函数的类型都必须是istream&类型,第二个参数是要进行输入操作的类。
        重载“<<”的函数的第一个参数和函数的类型都必须是ostream&类型,第二个参数是要进行输出操作的类。
        因此,只能将重载“>>”和“<<”的函数作为友元函数或普通的函数,而不能将它们定义为成员函数。

16.2 为什么只能将重载“>>”和“<<”的函数作为友元函数或普通的函数,而不能将它们定义为成员函数?

        是因为一般情况下使用<<和>>时,将类写在后面。即:cout<<d1. 而用成员函数重载操作符规则是,符号左边是调用者,右边是参数(d1 + 100,Data operator+(int day) const;如果用成员函数来重载)。即这样d1<<cout,但是这样可以说是不对的;故使用友元函数,<<左边是第一个参数cout,右边是第二个参数d1。

        因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。

下面是我找到的一些解释

为什么只能将重载"<<"和">>"的函数作为友元函数或普通函数,而不能将它们定义为成员函数_百度知道 (baidu.com)

        <<有两个参数,一个是输出流对象(我们常用的cout),还有就是要输出的东西。
        例如:cout<<"haha";也就是说<<的第一个参数必须是输出流对象。在成员函数里实现<<重载,我们知道this会作为第一个参数,即重载双目操作符(即为类的成员函数),就只要设置一个参数作为右侧运算量,而左侧运算量就是对象本身。而 >>  或<< 左侧运算量是 cin或cout 而不是对象本身。(3条消息) 流运算符为什么不能重载为成员函数,只能用友元函数重载_拉轰小郑郑的博客-CSDN博客

如果一定要声明为成员函数,只能成为如下的形式:

ostream & operator<<(ostream &output)

{

  return output;

}

所以在运用这个<<运算符时就变为这种形式了:data<<cout;

不合符人的习惯。

        实际上流操作符左侧必须为cin或cout,即istream或ostream类,不是我们所能修改的类;(operator+=(int day))或者说因为流操作符具有方向性。

        这导致我们不能使用成员函数重载,只能使用类外的普通函数重载。又由于我们将类内部的私有成员进行输入和输出,所以重载函数必须有对内部成员访问的权限。这导致我们不能使用普通的函数重载,只能使用友元函数重载。

(3条消息) 为什么c++中重载流操作符要用友元函数_litsun的博客-CSDN博客_为什么流运算符要重载为友元函数

        定义为成员函数,那么就隐含this指针,重载其实也是一种函数,那么函数就有调用他的对象,如果是成员函数,那么调用他的对象就肯定是相对应的类对象,但是<<和>>调用的对象只能是cout或者cin,因为cout和cin是全局的对象,他们包含在iostream头文件中,所以我们写C++程序要包#include <iostream>,cin是istream的对象,cout是ostream的对象。那么就不能定义为成员函数了,只有定义成友元,那么就可以把cin,cout作为一个参数传进重载的操作符函数里面。

	//友元函数
	friend std::ostream& operator<<(std::ostream & out, const Data & d);
	friend std::istream& operator>>(std::istream & out, Data & d);

16.3 为什么要声明友元呢?

        友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

        因为我们在类里面定义的成员变量是私有的,直接访问是不行的。
        如果想直接在类外面访问成员变量的话,有三种方法:
        一是把私有的成员变量设置为公有,如果设置公有的话,那么就破坏了封装;
        二是可以提供访问成员变量的函数;
        三是可以定义为 友元 函数,就是告诉编译器这两个函数是Date类的朋友,可以去访问私有的成员变量。

关于友元函数:

        友元函数可访问类的私有和保护成员,但不是类的成员函数;
        友元函数不能用const修饰;
        友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
        一个函数可以是多个类的友元函数;
        友元函数的调用与普通函数的调用原理相同;

std::ostream& operator<<(std::ostream& out, const Data& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
	return out;
}

std::istream& operator>>(std::istream& in, Data& d)
{
	in >> d._year >> d._month >> d._day;
	assert(d.CheckDate());//判断输入的日期是否合法
	return in;
}

return out;的作用是什么?
        回答:能连续向输出流插入信息。out是ostream类的对象,它是实参cout的引用,也就是cout通过传送地址给out,使它们二者共享同一段存储单元,或者说output是cout的别名。
        因此,return out就是return cout,将输出流cout的现状返回,即保留输出流的现状。这样就可以cout << d1 << d2;(从左到右)类似d1 = d2 = d3,但这个是从右往左,就是cout<<d1<<d2,输出d1和d2,我们肯定希望cout<<d1之后是cout,只有这样才可以cout<<d2。

为什么<<参数中有const Data& d?
        cout<< 只是把内容打印输出到屏幕上,所以我们加上 const 防止成员在函数内部进行修改。
而 cin>> 我们会在函数内部对成员变量进行赋值。

16.4 为什么这个不写在Data.h里?

        因为如果写在里头会报链接错误,因为在Data.cpp和test.cpp里都需要展开Data.h,这样我们就不知道去调用哪里的这个流输入和流提取,但是为什么别的可以,因为别的是在类里面定义,类里面定义的成员函数默认是内联函数,内联函数编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,每当代码调用到内联函数,就在调用处直接插入一段该函数的代码,而这个是在类外面定义的,所以我们直接写在了Data.cpp中。

三、详细代码

1. Data.cpp

#include"Data.h"
/*类的两种定义方式:
1.声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
	(以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,
	没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。)
2.类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加(类名::)
一般情况下,更期望采用第二种方式*/

bool Data::operator==(const Data& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}//为什么可以访问私有,因为这个函数虽然在类外面定义,在类里面声明
//但是实际他还是在类里面的成员函数,类的成员函数是可以访问私有的
//但其实本质上访问的是成员,类里面那个private只是声明,就是说实际上访问的是d1或者d2
//注意:变量的声明和定义的区别是是否开辟空间

bool Data::operator<(const Data& d) const//声明和定义分离需要注意
{
	if ((_year < d._year)
		|| (_year == d._year && _month < d._month)
		|| (_year == d._year && _month == d._month && _day < d._day))
	{
		return true;
	}
	else
	{
		return false;
	}
}


int Data::GetMonthDay(int year, int month)
{
	assert(year >= 0 && month > 0 && month < 13);
	const static int MonthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	//GetMonthDay会被频繁的调用,加上static,防止不停的开辟数组,直接放在静态区
	//再加上const,更安全
	//用0让31是1月份
	if (month == 2 && isLeapYear(year) == 1)
	{
		return 29;
	}
	else
	{
		return MonthDayArray[month];
	}
}



Data::Data(int year, int month, int day)//声明给了缺省参数,定义就不用给了
{
	if (year >= 1 && month <= 12 && month >= 1 && day >= 1 
		&& day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
}


//d1 + 100  --》 d1.operator+(day),,d1就是this
//如果如下写,会改变d1和d2,变成一样
//Data Data::operator+(int day)
//{
//	_day += day;
//	while (_day > GetMonthDay(_year, _month))
//	{
//		_day -= GetMonthDay(_year, _month);
//		_month++;
//		if (_month == 13)
//		{
//			++_year;
//			_month = 1;
//		}
//	}
//	return *this;
//}



//上方法:
Data& Data::operator+(int day)这样不对,因为ret所在的空间出了作用域销毁了
我们让他返回ret的别名,如果ret的空间没被占用,还是返回的和下代码一致,但是如果被占用就不一样了
//Data Data::operator+(int day) 
//{
//	//应对问题于是先拷贝源日期
//	Data ret(*this);//把*this拷贝给ret,再改变ret
//	ret._day += day;
//	while (ret._day > GetMonthDay(ret._year, ret._month))
//	{
//		ret._day -= GetMonthDay(ret._year, ret._month);
//		ret._month++;
//		if (ret._month == 13)
//		{
//			++ret._year;
//			ret._month = 1;
//		}
//	}
//	return ret;//注意传值返回返回的不是ret,是ret的拷贝,是临时对象,是带有const属性的
//	//再做Data(Data& d)拷贝构造的参数,是权限的放大。所以拷贝构造加上了const
//	//这正好说明了为什么需要Data(const Data& d);
//}


注意d1+=100,自己也会改变,而且有返回值,因为有些地方需要连续的+=
//Data& Data::operator+=(int day)
//{
//	//有没有办法封装了?重复又要写一遍
//	*this = *this + day;//d1 += 100;//这里复用了+即Data Data::operator+(int day)
//	return *this;
//	/*_day += day;
//	while (_day > GetMonthDay(_year, _month))
//	{
//		_day -= GetMonthDay(_year, _month);
//		_month++;
//		if (_month == 13)
//		{
//			++_year;
//			_month = 1;
//		}
//	}
//	return *this;*/
//	//思考:出了作用域*this还在不在?在
//	//可以这样理解:d1还在,,所以加上&,即引用返回会更合适
//}




//思考:分析上下两种复用方式的区别?哪种复用更合适一点?
//对于上方法,+有两次拷贝1.Data ret(*this);拷贝构造 2. return ret;传值返回,临时拷贝
//故用+=来调用或者说复用+的时候要调用两次拷贝,但是+=本来是没有拷贝的
//对于下方法,+=没有拷贝构造,只有+还是有两次拷贝构造,但是+=不用再像上方法那种去调用拷贝了
//下方法:所以下方法更合适
Data Data::operator+(int day) const
{
	Data ret(*this);
	ret += day;
	return ret;
}

Data& Data::operator+=(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}
	return *this;
}



Data Data::operator-(int day) const
{
	Data ret = *this;
	ret -= day;
	return ret;
}

Data& Data::operator-=(int day)
{
	//针对测试4,要不就断言,是负数的-=就不进入函数
	if (day < 0)//要不就让其变成+=负号
	{
		return *this += -day;
	}
	_day -= day;
	while (_day <= 0)
	{
		_month--;
		if (_month == 0)
		{
			_month = 12;
			_year--;
		}
		_day += GetMonthDay(_year, _month);
	}
	
	return *this;
}



Data& Data::operator++()
{
	*this += 1;
	return *this;
}

//d1++
Data Data::operator++(int i)
{
	Data tmp(*this);
	*this += 1;
	return tmp;
}

Data& Data::operator--()
{
	*this -= 1;
	return *this;
}
Data Data::operator--(int)
{
	Data tmp(*this);
	*this -= 1;
	return tmp;
}


/*这里flag来确定正负,如果现在和未来比,天数是正的,加上天数即可和大的相等
如果是现在和过去比,天数是负的,现在+天数等于过去的时间*/
//日期相减,日期减日期返回天数
int Data::operator-(const Data& d) const
//Data Data::operator-(int day)构成函数重载,返回值和参数不同
{
	int flag = 1;
	Data max = *this;
	Data min = d;
	if (*this < d)
	{
		min = *this;
		max = d;
		flag = -1;
	}
	int n = 0;
	while (min != max)
	{
		++n;
		++min;
	}
	return n * flag;
}


std::ostream& operator<<(std::ostream& out, const Data& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
	return out;
}
/*return out;的作用是什么?
回答:能连续向输出流插入信息。out是ostream类的对象,它是实参cout的引用,
也就是cout通过传送地址给out,使它们二者共享同一段存储单元,或者说output是cout的别名。
因此,return out就是return cout,将输出流cout的现状返回,即保留输出流的现状。
这样就可以cout << d1 << d2;(从左到右)类似d1 = d2 = d3,但这个是从右往左
就是cout<<d1<<d2,输出d1和d2,我们肯定希望cout<<d1之后是cout,只有这样才可以cout<<d2*/


/*cout<< 只是把内容打印输出到屏幕上,所以我们加上 const 防止成员在函数内部进行修改。
而 cin>> 我们会在函数内部对成员变量进行赋值。*/
std::istream& operator>>(std::istream& in, Data& d)
{
	in >> d._year >> d._month >> d._day;
	assert(d.CheckDate());//判断输入的日期是否合法
	return in;
}
//至于为什么这个不写在Data.h里
//因为如果写在里头会报链接错误
//因为在Data.cpp和test.cpp里都需要展开Data.h
//这样我们就不知道去调用哪里的这个流输入和流提取
//但是为什么别的可以,因为别的是在类里面定义,类里面定义的成员函数默认是内联函数
//内联函数编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,
//每当代码调用到内联函数,就在调用处直接插入一段该函数的代码
//而这个是在类外面定义的,所以我们直接写在了Data.cpp中




/*对“<<”和“>>”重载的函数形式如下:
    istream& operator>> (istream&, 自定义类&);
    ostream& operator<< (ostream&, 自定义类&);
即重载运算符“>>”的函数的第一个参数和函数的类型都必须是istream&类型,第二个参数是要进行输入操作的类。
重载“<<”的函数的第一个参数和函数的类型都必须是ostream&类型,第二个参数是要进行输出操作的类。
因此,只能将重载“>>”和“<<”的函数作为友元函数或普通的函数,而不能将它们定义为成员函数*/

//why?
/*定义为成员函数,那么就隐含this指针,
重载其实也是一种函数,那么函数就有调用他的对象,
如果是成员函数,那么调用他的对象就肯定是相对应的类对象,
但是<<和>>调用的对象只能是cout或者cin,那么就不能定义为成员函数了,
只有定义成友元,那么就可以把cin,cout作为一个参数传进重载的操作符函数里面*/

/*是因为一般情况下使用<<和>>时,将类写在后面。即:cout<<d1. 
而用成员函数重载操作符规则是,符号左边是调用者,右边是参数。d1 + 100
Data operator+(int day) const;
如果用成员函数来重载,调用时就得写成:d1<<cout;反而使人更迷惑甚至可以说是不对的;
用友元函数时,<<左边是第一个参数cout,右边是第二个参数d1
//std::ostream& operator<<(std::ostream& out, const Data& d)*/

2 Data.h

#pragma once
#include<assert.h>
#include<iostream>
using std::cout;
using std::cin;
using std::endl;

class Data//类里面定义默认是内联
{
	//友元函数
	friend std::ostream& operator<<(std::ostream & out, const Data & d);
	friend std::istream& operator>>(std::istream & out, Data & d);
/*为什么要声明友元呢?因为我们在类里面定义的成员变量是私有的,直接访问是不行的。
如果想直接在类外面访问成员变量的话,有三种方法:
一是把私有的成员变量设置为公有,如果设置公有的话,那么就破坏了封装;
二是可以提供访问成员变量的函数;
三是可以定义为 友元 函数,就是告诉编译器这两个函数是Date类的朋友,可以去访问私有的成员变量。
*/
public:
	bool isLeapYear(int year)//能被4整除却不能被100整除或能被400整除的年份就是闰年!
	//取模运算符"%"
	//除法运算符"/"
	{
		if ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	//判断日期合法性
	bool CheckDate()
	{
		if (_year < 1 || _month > 13 || _month < 1 ||
			_day < 1 || _day > GetMonthDay(_year, _month))
		{
			return false;
		}
		return true;
	}
	int GetMonthDay(int year, int month);
	Data(int year = 1, int month = 1, int day = 1);
	Data(const Data& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	Data& operator=(const Data& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	bool operator==(const Data& d);
	bool operator<(const Data& d) const;
	//建议成员函数中不修改成员变量的成员函数,都可以加上const,这样普通对象和const对象都能调用
	//d1 <= d2,成员函数要成为inline,最好在类里面定义,类里面定义默认就是inline
	bool operator<=(const Data& d)
	{
		return *this < d || *this == d;
	}
	bool operator>=(const Data& d)
	{
		return !(*this < d);
	}
	bool operator!=(const Data& d)
	{
		return !(*this == d);
	}
	bool operator>(const Data& d)
	{
		return !(*this <= d);
	}
	Data operator+(int day) const;
	Data& operator+=(int day);
	Data operator-(int day) const;
	Data& operator-=(int day);
	
	Data& operator++();//规定无参为前置++
	Data operator++(int i);//注意i可以不写,形参不写代表着不用这个值或者说不接收
	//Data operator++(int);
	//这里只是改变了函数名的修饰规则以用来区分
	//Data operator++(int i = 0);//这样全缺省是错误的,因为i没有实际的意义,只是用来区分前置与后置++
	//因为全缺省和无参的函数在调用时区分不开

	Data& operator--();
	Data operator--(int);

	//日期相减,日期减日期返回天数
	int operator-(const Data& d) const;

	//取地址运算符重载,这是默认成员函数,基本上不用自己写就够用了
	/*Data* operator&()
	{
	//return nullptr;//除非你不想让别人看到真实的地址或者想让别人看到你指定的地址
		return this;
	}
	const Data* operator&() const
	{
		return this;
	}*/

	/*C++的流插入运算符“<<”(从流中再读取,进行输出)和流提取运算符“>>”(提取后再进行输入)
	是C++在类库中提供的,所有C++编译系统都在类库中提供输入流类istream和输出流类ostream。
	cin和cout分别是istream类和ostream类的对象。*/
	//流插入运算符<<是ostream类型的对象
	//标准库在名字空间中被指定为std,所以需要::指明ostream在std中
	//即std::ostream
	//这样out就是cout的别名
	//内置类型自动识别类型,支持流插入与流提取,本质上函数重载,库里面已经写好了
	//cin是istream类型的对象,cout是ostream类型的对象


private:
	int _year;
	int _month;
	int _day;
};

 3.Test.h

        这里就不展示验证效果了,小伙伴们可以自行验证。测试用例如下:

//写日期类的时候需要注意日期的合法性:
#include"Data.h"
//测试日期比大小
void TestData1()
{
	Data d1(2022, 5, 18);
	Data d2(2022, 3, 20);
	Data d3(2022, 3, 20);

	cout << (d1 > d2) << endl;//<<流运算符优先级很高//1
	cout << (d1 < d2) << endl;//0
	cout << (d1 == d2) << endl;//0
	cout << (d2 == d3) << endl;//1
	cout << (d1 >= d2) << endl;//1
	cout << (d2 <= d3) << endl;//1
	cout << (d3 <= d1) << endl;//1
}

//测试日期加是否正确
void TestData2()
{
	Data d1(2022, 5, 18);
	Data d2 = d1 + 15;
	//先调用了operator+,得到临时返回值ret,再调用拷贝构造给d2
	//所以Data d3 = d1与Data d3(d1)是等价的,编译器会识别出拷贝构造
	//这样写就是赋值:Data d3;d3 = d1+15;
	//两个已经存在的对象的才叫赋值比如d1 = d2,d2赋值给d1

	d2.print();
	d1.print();
}

//测试日期减
void TestData3()
{
	Data d1(2022, 5, 18);
	Data d2 = d1 - 30;
	d2.print();
	d1 -= 30;
	d1.print();
}

//测试结果5月118号,肯定不合法
void TestData4()
{
	Data d1(2022, 5, 18);
	d1 -= -100;
	d1.print();
}

//前置与后置
void TestData5()
{
	Data d2(2022, 5, 18);
	Data ret1 = ++d2;
	ret1.print();
	d2.print();
	Data ret2 = d2++;
	ret2.print();
	d2.print();
	Data ret3 = d2--;
	ret3.print();
	d2.print();
	Data ret4 = --d2;
	ret4.print();
	d2.print();
}

//日期相减
void TestData6()
{
	Data d1(2022, 5, 18);
	Data d2(2020, 2, 4);
	cout << (d2 - d1) << endl;
	cout << (d1 - d2) << endl;
}



	
	/*“void Data::print(void)”: 不能将“this”指针从“const Data”转换为“Data& ”
	void print(Data* const this)
	对于TestData7中的d1.print()实际上式d1.print(&d1)
	而其类型是Data*,而print上的类型是Data* const,是符合要求的
	但是在Func中的d.print实际上虽然也是&d,但是类型是const Data*
	从const Data*变成Data* const,涉及到权限的放大,所以报错
	所以要想不报错就要加上const,但是Data* const this是隐含的,是不能显示的表示的*/

/*所以规定这样写void print()const//成员函数后加const相当于在隐含的参数前加上const
即const Data* const this
那是不是所有的成员函数都需要加上const?并不是,const的意义只是让其修饰this指针指向的对象
但是加上const后会让this指向的对象不能够想改,因为this的类型变成了const Data*
所以发现日期类的比较都可加上const,*/
void Func(const Data& d)
{
	d.print();
	cout << &d << endl;
}
void TestData7()
{
	Data d1(2022, 5, 18);
	d1.print();
	Func(d1);
	cout << &d1 << endl;
}

void TestData8()
{
	Data d1(2022, 5, 18);
	(d1 + 100).print();//这样在linux环境下编译不通过,如果print后不加const的话
}

void TestData9()
{
	Data d1,d2;

	/*1.在类库提供的头文件中已经对“<<”和“>>”进行了重载,使之作为流插入运算符和流提取运算符,
	能用来输出和输入C++标准类型的数据。
	因此,凡是用“cout<<”和“cin>>”对标准类型数据进行输入输出的,
	都要用#include 把头文件包含到本程序文件中。
	
	2.而用户自己定义的类型的数据,是不能直接用“<<”和“>>”来输出和输入的。
	如果想用它们输出和输入自己声明的类型的数据,必须对它们重载。
	
	3.cout和cin是全局的对象,他们包含在iostream头文件中,
	所以我们写C++程序要包#include <iostream>.
	cin是istream的对象,cout是ostream的对象。
	因此它俩是两个类型,这个类型是库里面的,流插入和流提取的类型。*/
	cin >> d1 >> d2;
	cout << d1 << d2;
	//operator<<(cout, d1);

	int i = 1;
	double d = 2.2;
	cout << i;
	cout << d;
}


int main()
{
	//TestData1();
	//TestData2();
	//TestData3();
	//TestData4();
	//TestData5();
	//TestData6();
	TestData7();
	//TestData8();
	//TestData9();
	return 0;
}

//对于const注意:
/*成员函数是可以调用成员函数的
如果	void print() //没有加上const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
比如该成员函数的定义:
Data Data::operator+(int day) const
{
	print();//但是这样是会报错
	//因为这样编译器会默认为this->print();而此时因为加上了const,this是const Data*
	//而调用成员函数的本质是把this指针再传递过去,而print是非const的,所以权限放大,所以报错
	//所以const成员函数内不可以调用它的非const成员函数
	Data ret(*this);
	ret += day;
	return ret;
}*/

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值