C++ 写一个日期类的注意事项

目录

成员变量的命名风格

类的实例化

拷贝构造

解决方法

引用

Data类拷贝构造

指针

Stack的拷贝构造

ps

运算符重载

ps

operator==  operator>=  operator!=

题目

用运算符重载函数写一个日期相加

那我们如何正确实现一个operator+呢?

空容器

复用+=


成员变量的命名风格

下文这样来初始化一个日期类的成员变量

#include<iostream>
using namespace std;


class Data
{
public:
	void Init(int year,int month,int day)
	{
		year = year;
		month = month;
		day = day;

	}


private:
	int year;
	int month;
	int day;
};
int main()
{

	Data T;
	T.Init(2003,11,29);
	return 0;
}

我们发现并没有初始化上:

 原因:

这样写加以区分:

#include<iostream>
using namespace std;


class Data
{
public:
	void Init(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;

	}


private:
	int _year;
	int _month;
	int _day;
};
int main()
{

	Data T;
	T.Init(2003,11,29);
	return 0;
}

类的实例化

有这样一个日期类:



class Data
{
public:
	void Init(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;

	}


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

即然有类域,那我们可以这样访问吗?
 

Data::_year;
Data._year;

不可以,因为我们私有的成员变量只是一个声明:

 只有声明是不能直接拿来用的,我们需要定义:

	Data T;

现在定义了一个变量T,就开辟了一块空间,_year,_month,_day的空间也被一把开出来了。

拷贝构造

有如下这么两个类,一个日期类,一个栈类

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		cout << "Stack()" << endl;
	}

	// s1(s)
	Stack(const Stack& s)
	{
		cout << "Stack(Stack& s)" << endl;
	
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
	}
private:
	// 内置类型
	DataType* _array;
	int _capacity;
	int _size;
};
class Data
{
public:
	void Init(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;

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

现在还有一个fun函数,我们把栈类的值拷贝给这个fun函数:

void fun(Data a)
{

}

int main()
{
	
	Data T;
	T.Init(2003,11,29);
	fun(T);


	return 0;
}

如果我们把日期类拷贝给fun函数就不会蹦:


void fun1(Data s)
{

}


int main()
{
	Data T;
	T.Init(2003,11,29);
	fun1(T);

return 0;
}

如果我们把栈来拷贝给fun函数就会蹦:

void fun2(Stack s)
{

}



int main()
{
   Stack s1;
	
	fun2(s1);

return 0;

}

我们可以画图解析一下:

如图,stack的int*_a是在堆上面开辟的空间,内容存在堆上的,_a指向堆。

fun是stack的浅拷贝,fun的_a也指向同一块地址。

 fun后调用,先结束生命周期,先调用析构。此时就把这块空间释放掉了,那么轮到stack的时候会再次调用析构,会再把这块空间释放一次。

看下面这个图,a和s1的_array都指向堆上的同一块地址:

(ps:调试技巧:先给fun2()调用打断点,跳到fun2()调用之后再f11跳fun2()定义)

fun函数先析构:

stack再去析构的时候就会报错,因为stack的_a此时是一个野指针,指向了一块已经被释放了的空间:

解决方法

引用

void fun2(Stack& a)
{

}

int main()
{
Stack s1;
	
fun2(s1);

return 0;
}

引用的话我就是你,也就不存在析构两次的问题了。

但是这个方法仍然有弊端,那就是如果我想通过a的值的改变但是不影响s1呢?

下面这段代码 apush值肯定会改变s1

void fun2(Stack& a)
{
	a.Push(1);
	a.Push(2);
}

int main()
{
Stack s1;
	
fun2(s1);

return 0;
}

c++提供了拷贝构造函数来解决这个问题

Data类拷贝构造

拷贝构造函数和函数名和类名一致,参数类型也和类名一致,就是自己拷贝自己。

如下,我们现在就可以不用fun了,直接d2拷贝d1,通过拷贝构造函数:

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

调拷贝构造有两种方法

d2,d3分别用不同方法拷贝d1:


	Data d1(2003,11,29);
	

	Data d2(d1);

    Data d3=d1;
	//fun1(d2);

但是会有报错:

也就是说拷贝构造函数参数必须为引用,这是因为如果不引用就会无穷无尽的拷贝。

怎么理解这句话呢?

内置类型可以直接传参,但是c++有自定义类型,列入栈这种,如果直接传参会面临析构两次的问题

于是c++规定自定义类型传参需要调用拷贝构造,如果直接传参会造成无限拷贝:

 所以我们要通过引用传参:

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

引用是一种别名,所以引用没有拷贝。

指针

同样的,用指针也可以解决这个问题,因为指针只是传个地址嘛,并没有拷贝值


void fun1(Data* s)
{

}

int main()
{
fun1(&d1);
return 0;
}

Stack的拷贝构造

栈类的拷贝构造就没有日期类的拷贝构造那么简单了,因为涉及到了扩容的问题。

栈对拷贝构造实际上就是把栈的构造函数给copy一份,包括空间和内容,这个也叫做深拷贝。

//构造函数


	Stack(size_t capacity = 3)
	{
		cout << "Stack()" << endl;

		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}
//拷贝构造


Stack(const Stack& s)
	{
		//cout << "Stack(Stack& s)" << endl;
		// 深拷贝
		_array = (DataType*)malloc(sizeof(DataType) * s._capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		memcpy(_array, s._array, sizeof(DataType) * s._size);
		_size = s._size;
		_capacity = s._capacity;
	}

 没写拷贝构造时的:

写了拷贝构造之后的:

 小结:

比如MYqueue类,我们什么都不写,直接让编译器用默认生成的,全去拿栈的用。

构造函数:默认生成,析构函数:默认生成,拷贝构造函数:默认生成。


class MYqueue
{
	Stack pushst;
	Stack Popst;
};


typedef int DataType;
class Stack
{
public:

	
	Stack(size_t capacity = 3)
	{
		cout << "Stack()" << endl;

		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

	 //s1(s)
	Stack( Stack& s)
	{
		cout << "Stack(Stack& s)" << endl;
		// 深拷贝
		_array = (DataType*)malloc(sizeof(DataType) * s._capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		memcpy(_array, s._array, sizeof(DataType) * s._size);
		_size = s._size;
		_capacity = s._capacity;
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;

		free(_array);
		_array = nullptr;
		_size = _capacity = 0;
	}
private:
	// 内置类型
	DataType* _array;
	int _capacity;
	int _size;
};

int main()
{
	
	
	MYqueue a;
	MYqueue d = a;
return 0;

}

ps

拷贝构造我们一般加const。

例如:

	Data(const Data& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "拷贝构造" << endl;
	}

这是因为万一有人写反了,把拷贝构造变成被别的成员变量赋值,编译器还看不出来错误:
而加个const就是权力的缩小,拷贝构造不能被修改,它只能修改别人:

运算符重载

比如我们想定义两个日期类的大小,我们像下面这样比较:

 编译器就会报错,因为Data是自定义类型,比较复杂,编译器没与办法直接比较。

我们只能自己手动比较:

C++给给所有的运算符比较的函数起了个统一的名称:运算符重载函数,就好比给所有初始化函数起名为构造函数,所有destory函数起名为析构函数一样。运算符重载函数用operator来表示.

运算符重载是什么意思呢?

就是说我们两个自定义类型不能直接比较,只能通过函数比较,再通过函数的返回值得到比较结果。

但是我们可以把这个比较函数重载为一个操作符,我们直接拿这个操作符用就可以实现这个函数的功能。

如下,重载为“>"操作符:

operator<

bool operator>(Data  d1, Data d2)

 这个时候我们就可显示调用operato>函数,或者直接比较d1,d2的值,都可以:

加括号之后:

如果我们不用运算符重载,而就用函数名的话就不能直接比较,只能通过调用函数的方式来比较:

原理:

ps

如果我们把operator>写为Data内部成员函数需要注意几点,如果我们直接放进去会报错:

 C++规定运算符重载函数作为类内部成员函数时,看起来的参数要比实际用到的参数少一个。

什么意思呢?但是这样写就不能这样显示调用operator>了只能这样调用:

 当然,使用运算重载后的操作符照样可以:

复用 operator<实现operator==  operator>=  operator!=

ok,接下来我们再写operator==

	bool operator==(Data d2)
	{
		return _year == d2._year
		     && _month == d2._month
			 && _day == d2._day;
	}

再来写operator>=

我们把将才写的operator<=和operator==改一下符号吗?变成下面这样:

bool operator<=( Data d2)
	{
		if (_year <=d2._year)
		{
			return true;
		}
		if (_year<=d2._year && _month <=d2._month)
		{
			return true;
		}
		if (_year<= d2._year && _month <= d2._month && _day <= d2._day)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

可以时可以,但是太麻烦,下面还要operator!= ,operator>,每个都这样拿代码就太冗余了。

我们可以复用,

我这里要写operator>=,我直接调用operator<和operator==就可以了。

怎么调用呢?用this指针,this是省略值的地址,解引用得到省略值,我们可以看下列代码:

bool operator>=(Data d2)
	{
		return *this > d2 || *this == d2;
	}

同样,写operator<,只需要对operator<=取反就行了:

	bool operator<(Data d2)
	{
		return !(*this >= d2);
	}

写operator!=,对operator==取反即可:

bool operator!=(Data d2)
	{
		return !(*this == d2);
	}

赋值运算符重载

如下,d1的日期为非法日期,d3为合法日期,我把d3的日期赋值给d1,让d1也成为合法日期:

Data d1(2003,11,33);
Data d3(2003,11,30);
	d1 = d3;
	d3.print();
	d1.print();

但是两个类之家是不能之间进行赋值的,这个可以运行是因为编译器默认生成了赋值运算符重载,将d3的成员变量的值逐个复制给d1的成员变量。需要注意的是,当类中存在指针类型的成员变量时,使用默认的赋值运算符重载函数可能会导致浅拷贝问题。在这种情况下,你需要自己编写赋值运算符重载函数,以确保进行深拷贝操作,避免出现内存错误。

我们自己可以写一下赋值运算符重载:

 赋值运算符重载和拷贝构造区别

内置类型我们可以像这样进行赋值:它的原理实际上是这样:注意:优先级问题,还需要再加个括号:但是自定义类型就不可以了:

那我们可以按照内置类型的思路:先让d4复制给d3,再返回一个值,把这个值再赋值给d1.

返回的这个值也要是Data类型,因为要返回一个日期,把这个日期再赋值给d1. 

Data& Data::operator=(const Data& d3)
{
	this->_year = d3._year;
	this->_month = d3._month;
	this->_day = d3._day;

	return *this;
}
Data d1(2003, 11, 33);
	Data d3(2003, 11, 22);
	Data d4(2000, 12, 3);
	d1 = d3 = d4;
	d1.print();
	d3.print();
	d4.print();

解析:

题目

用运算符重载函数写一个日期相加

思路:日期类相加,先day和day相加,满28/29/30/31就往月进,月满了往下个月进,月满13了往年进。

闰年二月是29填,需要特殊处理:


	bool operator+()
	{

	}

	int Getmonth(int year,int month)
	{
		int GetArry[13] = {0, 31,28,30,31,30,31,30,31,30,31,30,31 };
		
		if (month==2&&(month % 4 == 0 && month % 100 != 0 || month % 400 == 0))
		{
			return 29;
		}
	return GetArry[month];
	}
	

 看一下operator+怎么写:

Data d1(2003,11,29);

d1+100;
Data(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

 所以此时_year=2003,_month=11,_day=29.

bool operator+(int  day)
	{
//天数相加
		_day += day;

//如果月满了
		while (_day > Getmonth(_year, _month))
		{

//天数减去满月的天数
			_day -= Getmonth(_year, _month);

//从当前月进入下一个月
			++_month;
		}


//如果12个月都满了
		if (_month == 13)
		{

//当前年进入下一年
			++_year;

//把月置为下一年的一月
			_month = 1;
		}


	}

把我们的传参再拿过来看看:

Data d1(2003,11,29);

d1+100;

d1+100是一个什么类型,日期+日期还是一个日期

所以:

Data ret=d1+100;

那operator+也应该用Data类型:

   Data operator+(int  day)
	{
		_day += day;
		while (_day > Getmonth(_year, _month))
		{
			_day -= Getmonth(_year, _month);
			++_month;
		}

		if (_month == 13)
		{
			++_year;
			_month = 1;
		}


	}

operator+该返回什么呢?

返回*this

Data d1(2003,11,29);
	Data ret = d1 + 50;
	d1.Print();
	ret.Print();

 我们发现结果虽然运算正确,但是d1的值也被改为了ret的值,这说明

这是一个operator+=,而不是一个operator+

那我们如何正确实现一个operator+呢?

空容器

我们可以用用拷贝构造拷贝一份到空类里面,改变这个空容器,最后返回这个空容器就可以了。

 Data operator+(int  day)
	{
		Data temp(*this);
		temp._day += day;
		while (temp._day > Getmonth(_year, _month))
		{
			temp._day -= Getmonth(_year, _month);
			++temp._month;


			if (temp._month == 13)
		{
			++temp._year;
			temp._month = 1;
		}



		}

		
		return temp;

	}

复用+=

 Data operator+=(int  day)
	{
		
		_day += day;
		while (_day > Getmonth(_year, _month))
		{
			_day -= Getmonth(_year, _month);
			_month++;


			if (_month == 13)
		{
			_year++;
			_month = 1;
		}



		}

		
		return *this;

	}
	 Data operator+(int  day)
	 {
		 Data temp(*this);
		 temp += day;
		 return temp;

	 }

operator-=

实现两个日期类相减:

-=是一种借位思想,如果2020 7 27-27还好,如果day-200呢,那不成了-173,欠债了。

 这个时候就往前一个月借,当前月的已经光了,还欠了债,需要上个月来补,--_month.

当day<=0时都是不合法日期,day<0不合法是欠债了,需要往上一个月借,=0不合法是因为一个月没有0号。

当month=0时说明月借光了,再问年借,--_year;

再把month重置为去年的12月份。

_day要借的是上个月的day,借到_day.value不为负值,说明还完债了。

这时候把还完债后的年月日返回。

Date& Date:: operator-=(int  day)
{
	_day -= day;
	while (_day <=0)
	{
		--_month;

		if (_month == 0)
		{
			_year;

			_month = 12;
		}
		_day += Getmonth(_year, _month);
	}

	return *this;
}

	Date d1(2003, 11, 29);
	d1 -= 100;
	d1.print();

 把数变大一点再测:


	Date d1(2003, 11, 29);
	d1 -= 20000;
	d1.print();

这肯定错了,20000多天都50多年了

 肯定是_year没有--,改一下:

看一下计算器算的:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孙鹏宇.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值