C++类和对象(中)

前言

我们前面已经介绍过了C++类和对象的入门知识,例如面向过程和面向对象,类的概念以及this指针等~!本期我们再来对类和对象介绍~!

本内容介绍

类的6个默认成员函数

构造函数

析构函数

拷贝构造

赋值运算符重载

const成员函数

取地址及const取地址操作符重载

类的6个默认成员函数

我们如果实现一个空类即类中什么都没写,既无成员属性(成员变量)也无成员函数(成员方法)。那么该空类真的什么都没有吗?答案是:并不是!任何类什么都不写的时候编译器也会自动生成6个默认成员函数!

默认成员函数:实现者(程序员)没有显示的实现,而编译器自动生成的成员函数~!

注意:是实现者没有显示的实现的情况夏编译器才会生成,如果实现者显示的实现了则不会生成~!

那这里我们的学习目标就很明确了,如果我们不显示的生成,默认成员函数做了什么,是否符合我们的要求,如果不符合需求我们该如何实现~!

下面就是类的6大默认成员函数:

C++类和对象(中)_第1张图片

OK,下面我们就来一一的介绍一下每个默认成员函数~!

一、构造函数

构造函数的引出

OK,先来看一段代码:

class Date
{
public:
	void Init(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;
};

int main()
{

	Date d1;
	d1.Init(2023, 12, 29);
	d1.Print();

	Date d2;
	d2.Init(2023, 12, 30);
	d2.Print();

	return 0;
}

这是个日期类,我们可以通过创建日期类的对象后,调用Init方法去设置日期对象的相关属性,但每次先创建对象,在调掉对象的Init方法去初始化是不是有点麻烦呀!我们可不可以在初始化的时候就将对象的属性给设置好呢?这就是我们要介绍的构造函数,他就是专门干这个事情的!!

什么是构造函数?

构造函数:构造函数是一个特殊的成员函数,函数名与类名相同,创建类的对象时由编译器自动调用,目的是了让每个对象的成员变量都有一个合适的初始值,并且在对象整个的生命周期内只调用一次~!

构造函数的特性

1、函数名与类名相同

2、无返回值(void也没有

3、实例化对象时编译器自动调用

4、构造函数可以重载

5、如果实现者没有显示的定义则编译器会自动生成一个无参的默认构造,反之则不会生成

6、默认构造函数对于内置类型的成员变量不做处理,对于自定义的成员变量回去调用他们自己的默认构造!(C++11在这里又打了个补丁,在成员属性申明时给缺省值)

7、无参构造、全缺省的构造以及编译器默认生成的都成为默认构造(注意:默认构造只能有一个,否则会产生调用歧义

OK,我们一点一点的来解释一下!

1、函数名与类名相同

2、无返回值(void也没有)

C++类和对象(中)_第2张图片

3、实例化对象时编译器自动调用

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;
};

int main()
{

	Date d1(2023, 12, 29);
	d1.Print();

	Date d2(2023, 12, 30);
	d2.Print();

	return 0;
}

这就是上面的那个时间类,我们把Init换成了构造,而我们前面介绍过,构造是自动调用给对象的属性初始化的。我们可以打印看,也可以调试看。分别来看看:

C++类和对象(中)_第3张图片

C++类和对象(中)_第4张图片

这就很方便了,不用自己手动Init去初始化了~!

4、构造函数可以重载

其实前面在介绍6大默认成员函数时出了有构造函数还有一个拷贝构造,他两就是重载!拷贝构造下面会详细介绍。其实重载无非 参数的个数、参数的顺序、参数的类型不一样而已。但我们这里都是为属性初始化的,所以类型和顺序一定相同。那就是只有个数了!

全参数

Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

无参数

Date()
{
	_year = 1;
	_month = 1;
	_day = 1;
}

这是构造函数的重载~!他们都是给对象的属性赋值的。当然上面有参数的那个构造你想给几个参数赋值就可以给几个赋值,夹具体看你的需求~!

但注意的是:这里还可以支持用缺省值赋值!

 全缺省

Date(int year = 1, int month = 1, int day = 1)
{
	_year = year;
	_month = month;
	_day = day;
}

 部分值缺省

Date(int year, int month = 1, int day = 1)
{
	_year = year;
	_month = month;
	_day = day;
}

C++类和对象(中)_第5张图片

5、如果实现者没有显示的定义则编译器会自动生成一个无参的默认构造,反之则不会生成

C++类和对象(中)_第6张图片

此时是写了构造的,我们可以在创建时赋值。但如果我们没有提供,编译器会自动生成,他是如何处理的?我们一起来看看!

C++类和对象(中)_第7张图片

我们可以清楚的看到,默认自动生成的他是没有做任何处理的(对内置类型)~!

6、默认构造函数对于内置类型的成员变量不做处理,对于自定义的成员变量回去调用他们自己的默认构造!(C++11在这里又打了个补丁,在成员属性申明时给缺省值)

上面的也刚刚验证了这里的默认的构造函数是对内置的成员变量不做处理的!对于自定义类型又是去调用自己的构造的~!例如我们前面的用两个栈实现一个队列~!

先来个栈的类,看看他默认生成的会不会处理:

C++类和对象(中)_第8张图片

果然自定义类型没有被处理!这里需要注意的是:指针也是自定义类型~!

我们再来把他显示实现

C++类和对象(中)_第9张图片

再来看看两个栈实现队列的:

C++类和对象(中)_第10张图片

这里就很清楚的看到了,自定义类型的成员属性去调用Stack自己的默认构造,而自定义类型是没有处理的~!这里是VS2019的一个Bug,在VS2013上是没有处理的~!

C++类和对象(中)_第11张图片

C++类和对象(中)_第12张图片

所以根据语法的向前兼容性,我们是认为内置类型是不做处理的~!后来C++标准委员会意识到了这一点是不好的!你要处理就都处理,要不处理就都不处理,全部都是内置类型时不处理,自定类型+内置类型时又处理了,所以他在C++11给这里打了个补丁即可以再类的属性声明时给个缺省值~!

C++类和对象(中)_第13张图片

7、无参构造、全缺省的构造以及编译器默认生成的都成为默认构造(注意:默认构造只能有一个,否则会产生调用歧义)

最后一点很简单,但得注意!不是编译器默认生成的那个才叫默认构造。全缺省、无参构造和编译器默认生成的都叫默认构造~!上面一开始都演示了,这里就不在多啰嗦了~!

二、析构函数

什么是析构函数?

析构函数:也是一个特殊的成员函数,但与构造函数的功能相反。构造函数是给对象的属性赋值的,而析构函数是清理对象的资源的~!

注意:这里的清理资源不是销毁对象本身,对象本身的销毁是由编译器完成的,这里的资源是指对象的某一属性在堆区等需要手动释放的地方开的空间~!没有在堆区等地方开空间的等会随着函数栈帧一同在最后销毁~!

例如栈的析构:

C++类和对象(中)_第14张图片

析构函数的特性

1、构造函数的名字是在类名前加个 ~

2、无参数和返回值类型

3、一个类只能有一个析构函数。若未显示定义,编译器会自动生成。

4、对象的生命周期结束时自动调用

5、自定义类型会去调用自己的析构,内置类型不作处理最后系统一块回收。

6、如果类中没有资源申请是可以不用写析构(Date,MyQueue等类可以不写),有资源申请是一定要写析构,否则可能会内存泄漏(Stack等一定要写)!

注意:析构函数不能重载!!!

OK,还是来举栗子介绍一下:

1、构造函数的名字是在类名前加个 ~

2、无参数和返回值类型

C++类和对象(中)_第15张图片

3、一个类只能有一个析构函数。若未显示定义,编译器会自动生成。

4、对象的生命周期结束时自动调用

我们可以在析构函数里加一句,打印一下~!

C++类和对象(中)_第16张图片

C++类和对象(中)_第17张图片

的确是打印出了这句,那也就侧面证明了他是自动调用的~!

5、自定义类型会去调用自己的析构,内置类型不作处理最后系统一块回收。

例如MyQueue:

C++类和对象(中)_第18张图片

C++类和对象(中)_第19张图片

6、如果类中没有资源申请是可以不用写析构(Date,MyQueue等类可以不写),有资源申请是一定要写析构,否则可能会内存泄漏(析构类似于我们在数据结构玩的那个Destory,Stack等一定要写)~!

日期类全部都是内置类型不需要写析构,Stack有一个在堆区上开辟的空间,因此Stack需要进行写析构,MyQueue他的成员两个栈,栈的栈已经实现过了,还有一个内置类型无需处理~!

三、拷贝构造

什么是拷贝构造?

拷贝构造一个特殊的成员函数,他只有单个形参,该形参是本类类型对象的引用(一般会用const修饰),记在用一已经存在的该类的对象创建新的对象!拷贝构造也是自动调用!

例如d1就是拷贝d:

	Date d;
	Date d1(d);

拷贝构造的特性

1、拷贝构造函数是构造函数的一个重载形式

2、拷贝构造函数的参数只有一个且必须时类型对象的引用,使用传值编译器会直接报错,因为会引发无穷递归~!

3、若未显示定义,编译器会生成默认的拷贝构造。默认的拷贝构造按照对象的字节序进行拷贝。(即浅拷贝或值拷贝。这里深浅拷贝后面会详解!)

4、编译器默认生成的拷贝构造已经可以完成字节序的值拷贝了,那还有必要自己实现吗?答案是看是否涉及资源的申请!

5、拷贝构造的典型调用场景

        a、使用已经存在的对象创建新对象

        b、函数参数类型为类类型的对象

        c、函数返回值类型为类类型

OK,还是来用栗子来解释:

1、拷贝构造函数是构造函数的一个重载形式

2、拷贝构造函数的参数只有一个且必须时类型对象的引用,使用传值编译器会直接报错,因为会引发无穷递归~!

C++类和对象(中)_第20张图片

为什么传参时,形参必须是用应用接受呢?我们用一段代码解释一下:

C++类和对象(中)_第21张图片

所以这里不可以传值,为什么传引用可以呢?原因是:我们前面介绍过,引用是别名,在语法上没有开空间!所以如果这里用引用的话,就是要被拷贝的对象本身,传参时就无须拷贝了,也就不会陷入无穷递归了~!

3、若未显示定义,编译器会生成默认的拷贝构造。默认的拷贝构造按照对象的字节序进行拷贝

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 d(2023,12,29);
	Date d1(d);
	d.Print();
	d1.Print();

	return 0;
}

C++类和对象(中)_第22张图片

OK,我们不写是编译器会默认的生成一个,进行对对象的按字节序进行拷贝~!

4、编译器默认生成的拷贝构造已经可以完成字节序的值拷贝了,那还有必要自己实现吗?答案是看是否涉及资源的申请!

上面的日期类都是基本数据类型,编译器默认生成的拷贝构造就可以!我们来看看一个栈的,看会不会和上的一样:

class Stack
{
public:
	Stack(int capacity = 3)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc failed");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}

private:
	int* _a = nullptr;
	int _top = 0;
	int _capacity = 0;
};

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

	return 0;
}

看结果:

C++类和对象(中)_第23张图片

被黄牌警告了!!!这里是断言失败!原因是这里对同一块空间多次析构了!我们上面刚介绍了,编译器默认生成的拷贝构造会按照对象的字节序的值进行拷贝!这里的情况如下:

C++类和对象(中)_第24张图片

这里按照默认编译器生成的话就会让两个栈的指针指向同一块空间,当等到两个对象的生命周期结束时,就会自动调用析构,就会对同一块空间释放(资源清理)两次!就会出问题~!所以想栈这种的拷贝构造需要我们自己来实现!我们下面来实现一下:
 

~Stack()
{
	cout << "~Stack()" << endl;
	free(_a);
	_a = nullptr;
	_top = 0;
	_capacity = 0;
}

C++类和对象(中)_第25张图片

C++类和对象(中)_第26张图片

5、拷贝构造的典型调用场景

a、使用已经存在的对象创建新对象

b、函数参数类型为类类型的对象

c、函数返回值类型为类类型

C++类和对象(中)_第27张图片

C++类和对象(中)_第28张图片

C++类和对象(中)_第29张图片

注意:为了为了提高程序的效率一般无特殊需求建议传引用~!因为拷贝多少有空间和时间的消耗~!

四、赋值运算符重载

我们如果要比较两个日期类的对象是否相等该如何做?目前是不是得写一个函数来判断呀!如下:

	bool cmp(const Date& d2)
	{
		return _year == d2._year && _month == d2._month && _day == d2._day;
	}
int main()
{
	Date d1(2023, 12, 29);
	Date d2(1970, 1, 1);
	d1.cmp(d2);

	return 0;
}

C++类和对象(中)_第30张图片

但这样的问题就是,遇到一些命名相对规范的还好!如果遇到一些名字随便取的例如xiangdeng这样的,你让别人如何看?祖师爷当时就想到了这一点,于是提出了运算符重载!即自定义类型也可以和内置类型一样直接可以用一般的运算符比了!(但前提是你得重载了当前的运算符)!例如:d1 == d2(必须重载==才可以)!

运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。也具有其返回值类型,函数名字以及参数列表。函数名字和形参列表和普通函数类似!

函数名:operator后面加需要重载的运算符的符号

函数原型:返回值类型 operator操作符(形参列表)

注意:

(1)不能通过连接其他符号来创建新的操作符。例如:operator# 或operator@

(2)重载的操作符必须有一个类类型的参数

(3)用于内置类型的运算符,其含义不变。例如:整数+整数的+的含义不变

(4)作为类成员函数重载时,其形参操作数看起来比实际少1个,原因是成员函数的第一个参数是隐藏的this

(5).*    ::     sizeof    ?:   .   这5个运算符不能重载!!!!

也就是说,刚刚写的日期的判断是否相等可以写成这样:

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

所以上面日期比较可以写成这样:

int main()
{
	Date d1(2023, 12, 29);
	Date d2(1970, 1, 1);
	d1 == d2;//实际是d1.operator==(d2); <===> d1.operator==(&d1, d2);

	return 0;
}

C++类和对象(中)_第31张图片

OK,这就是判断相等的运算符重载!除了不能重载的那5个以外,其他的都能重载至于是否要重载具体看你的需求!(日期类很有必要重载我们后面实现日期类的时候再来一一实现)!

赋值运算符重载的格式

参数类型:const T&,传递引用可以提高传参的效率

返回值类型:T&, 返回引用可以提高返回值效率,有返回值的目的是为了支持连续赋值

检测是否是自己给自己赋值

返回*this:要符合连续赋值的含义

	Date& operator= (Date& d)
	{
		if(this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}

C++类和对象(中)_第32张图片

C++类和对象(中)_第33张图片

其实这里很符合我们的常识,我们在内置类型那里也是可以连续赋值的,换句话说内置类型的赋值也是有返回值的!

赋值运算符只能重载成类成员函数不能重载成全局函数

Date& operator= (Date& d, const Date& d2)
{
	if (&d != &d2)
	{
		d._year = d1._year;
		d._month = d2._month;
		d._day = d2._day;
	}

	return d;
}

如果是全局就没有this指针了,就需要把两个参数的引用都要传过去!但即使你传过去了这里还是会报错的!原因是,你在类里面没有显示的实现时编译器会生成一个默认的!这会与和类里面的冲突!所以赋值运算符重载只能重载类的成员变量~!<>第五版是这样说的:

实现者没有显示的实现时,编译器会默认的生成一个赋值运算符重载,以值的方式逐字节的拷贝。注意:内置类型是直接赋值的,自定义类型成员变量需要调用他们自己的类的赋值运算符重载!

C++类和对象(中)_第34张图片

我们把刚实现的给注释掉,运行发现是可以实现赋值的!这是内置类型的!自定义类型的话需要自己实现!因为涉及资源问题!

既然编译器生成默认的的赋值运算符重载函数,已经可以实现按照值逐一字节赋值了拷贝了!那还需要自己实现吗?

例如栈:默认生成的:

C++类和对象(中)_第35张图片

他会报错!下面这样写是对的:

Stack& operator=(const Stack& s)
{
	if (this != &s)
	{
		free(_a);//释放原来的资源
		_a = (int*)malloc(sizeof(int) * s._capacity);//开辟个要赋值的一样大的空间
		if (nullptr == _a)
		{
			perror("malloc failed");
			exit(-1);
		}

		memcpy(_a, s._a, sizeof(int) * s._capacity);
		_top = s._top;
		_capacity = s._capacity;
	}

	return *this;
}

总结:如果未涉及到资源管理,可以不用实现默认的就可以!否则一旦涉及到资源管理必须要显示的实现!

日期类的实现

我们这次还是和以前数据结构玩的那样正规一点,采用多文件编程!实现一个日期类,并把各种常见的运算符给重载一下!

首先由于日期内的成员属性都是内置类型,所以只需给一个全缺省的构造即可~!析构、拷贝构造以及赋值运算符重载都统统不需要!

Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
注意:这里有个细节是全缺省的构造函数不可以声明和定义时同时给缺省值!原因是怕两次的缺省值不一样,导致编译器不知道到底用哪个去初始化!所以我们一般采用声明时给缺省值,定义时不给!而且成员函数在声明和定义分离时需要在定义时指定类域!
Date(int year = 1, int month = 1, int day = 1);//全缺省的构造

还有就是我们得多个地方多次打印看这个日期类的属性,所以我们干脆再来封装一个打印函数!

void Date::Print()
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

1、判断两个日期是否相等

这个其实很简单,只要你逐一比较年月日即可!

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

2、判断两个日期是否不相等

这个更简单,你可以按照上面的逻辑在来一遍,逐一比较他们是不是都不相等。但最简单的方式还是复用上面相等的代码!只需要判断他们是否相等然后取反即可!

bool Date::operator!=(const Date d)
{
	return !(*this == d);
}

3、判断前面日期是否大于后面的日期

我们可以先比较第一个的年是否大于第二个的年,如果大于则就第一个大于第二个,如果年相等比较月是否第一个大于第二个,月大就大,月在相等比较天,天大就大。

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;
}

4、判断前面日期是否大于等于后面的日期

这个可以按照上面的大于等于的逻辑再写一遍,但最优的解法还是复用!我们当第一个大于或等于的时候就是大于等于了!

bool Date::operator>=(const Date d)
{
	return (*this > d) || (*this == d);
}

5、判断前面日期是否大小后面的日期

这个还是一样可以自己实现,但最优解还是复用!只要不大于等于就是小于了!

bool Date::operator<(const Date d)
{
	return !(*this >= d);
}

6、判断前面日期是否小于等于后面的日期

当不大于就是小于等于了!

bool Date::operator<=(const Date d) 
{
	return !(*this > d);
}

7、日期的+=天数

就是当前的日期+上天数,这里考虑的一个问题是:加上这个天数后可能会超出当月的天数,此时就得减去当月的天数,然后在份上+1,但如果加的天数比较大会超出12月,因此可以判断一下,当超出12月后可以++一下年!

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;
}

8、日期的-=天数

这里还是和上面差不多,但得反着来!先让天数-上一个要减的天数,然后判断减完后的天数师傅合法!即大于等于0,如果不合法了,就得向月借一个,如果月借完了,得向年借!

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

		if (_month == 0)
		{
			--_year;
			_month = 12;
		}
	}

	return *this;
}

OK,到这里基本上+=和-=就好了!但有个极端情况就是,如果你操作失误-=或+= 一个负数的话这段逻辑就会有问题!所以我们在对上面的这种情况处理一下!当+=一个负数是就等于-=一个正数!所以可以修正一下:

+=修正后:

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)
{
	if (day < 0)//当-=的是负数的情况
	{
		return *this += (-day);
	}

	_day -= day;
	while (_day <= 0)
	{
		--_month;
		_day = GetMonthDay(_year, _month);

		if (_month == 0)
		{
			--_year;
			_month = 12;
		}
	}

	return *this;
}

9、日期+一个天数

注意:+=和-=是自己要改变的,但+和-是自己不改变的!

有吱声不改变所以我们可以,准备一个和自己一样的日期即先给自己找个拷贝把拷贝的日期+=1,然后返回+=1完的日期的拷贝即可!这样自身既没有改变了!

Date Date::operator+ (int day)
{
	Date tmp(*this);
	tmp += day;
	return tmp;
}

10、日期-一个天数

-一个天数也和上面的一样,让自己的拷贝剪完并返回去!自己不变!

Date Date::operator- (int day)
{
	Date tmp(*this);
	tmp -= day;
	return tmp;
}

11、日期的前置++

前置++先自生+=1,然后返回回去。这里前置的++是自己先改变的,然后再返回去的!

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

12、日期的后置++

后置++首先要解决的一个问题是,如何和前置++做区别!祖师爷规定,后置++必须有个参数类型做区别,我们在C++入门的时候介绍过C++的函数吗重载的本质是符号不一样,C++在链接的时候,会去找修饰过的函数名,我们当时在Linux的g++下演示了,C++的函数修饰规则是-Z + 函数名的字符个数+参数的首字符,所以这里只要给个参数就可以区别!后置的++的话,加完自己先不变,先使用在+=1!所以还得一个拷贝,让自己+=1,返回没有+=1前的状态即拷贝!

Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}

13、日期的前置--

和上面的前置++同理,换成--即可!

Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

14、日期的后置--

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

15、两个日期相减

两个日期相减即间相差多少天,这个的实现思路一开始坑想到的就是直接相减,但如果直接相减了,闰年的月和平年的月和天都不一样!会出问题!所以不可以直接相减,另一种思路就是,分别和各自年的1月1号相减然后月和日相减,只剩一个年,只需考虑润不润即可!这种思路可行但还有一种可以复用的思路:我们找到最大和最小的日期,让最小的日期逐天加到大的那个日期,同时让个变量跟着小的日期,最后返回跟着的变量即可!但日期万一大减小会出现负数,所以得用标记记录!

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 (min != max)
	{
		++min;
		++n;
	}

	return n*flag;
}

OK,这就是全部的日期类了!OK,测试一波:

int main()
{
	Date d1;
	Date d2(2023, 12, 31);

	bool ret1 = d1 == d2;
	cout << ret1 << endl;

	bool ret2 = d1 != d2;
	cout << ret2 << endl;

	bool ret3 = d1 > d2;
	cout << ret3 << endl;

	bool ret4 = d1 >= d2;
	cout << ret4 << endl;
	
	bool ret5 = d1 < d2;
	cout << ret5 << endl;

	bool ret6 = d1 <= d2;
	cout << ret6 << endl;

	Date& ret7 = d1 += 10;
	ret7.Print();

	Date ret8 = d1 +10;
	ret7.Print();

	Date& ret9 = d1 -= 10;
	ret7.Print();

	Date ret10 = d1 - 10;
	ret7.Print();

	Date& reta = ++d1;
	reta.Print();

	Date retb = d1++;
	retb.Print();

	Date& retc = --d1;
	retc.Print();

	Date retd = d1--;
	retd.Print();

	Date d3(2023, 12, 31);
	Date d4(2023, 1, 1);
	cout << d3 - d4 << endl;

	return 0;
}

C++类和对象(中)_第36张图片

OK,没问题!这里我在添加一个函数,给定一个日期返回时当前年的第多少天:

返回给定日期当前年的第多少天

实现逻辑很简单,拿当前的日期和当前年的1月1相减,然后加上相差月的月份和相差的天数在+1(1月1号的)即可!

int Date::GetdayOfYear()
{
	int GetDay[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	_month -= 1;
	_day -= 1;

	int ret = 0;
	if ((_year % 4 == 0 && _year % 100 != 0) || (_year % 400 == 0))
		GetDay[2] = 29;

	for (int i = 0; i <= _month; i++)
	{
		ret += GetDay[i];
	}

	return ret += _day + 1;
}

C++类和对象(中)_第37张图片

全部源码

#include 
using namespace std;

class Date
{
public:
	void Print();
	int GetMonthDay(int year, int month);
	int GetdayOfYear();

	Date(int year = 1, int month = 1, int day = 1);//全缺省的构造

	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);

	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);
	int operator-(const Date d);

private:
	int _year;
	int _month;
	int _day;
};
#include "Date.h"

void Date::Print()
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

int Date::GetdayOfYear()
{
	int GetDay[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	_month -= 1;
	_day -= 1;

	int ret = 0;
	if ((_year % 4 == 0 && _year % 100 != 0) || (_year % 400 == 0))
		GetDay[2] = 29;

	for (int i = 0; i <= _month; i++)
	{
		ret += GetDay[i];
	}

	return ret += _day + 1;
}

int Date::GetMonthDay(int year, int month)
{
	int GetDay[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 GetDay[month] + 1;
	
	return GetDay[month];
}

Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

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

bool Date::operator!=(const Date d)
{
	return !(*this == d);
}

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)
{
	return (*this > d) || (*this == d);
}

bool Date::operator<(const Date d)
{
	return !(*this >= d);
}

bool Date::operator<=(const Date d) 
{
	return !(*this > d);
}

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)
{
	Date tmp(*this);
	tmp += day;
	return tmp;
}

Date& Date::operator-= (int day)
{
	if (day < 0)//当-=的是负数的情况
	{
		return *this += (-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);
	tmp -= day;
	return tmp;
}

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

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;
}

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 (min != max)
	{
		++min;
		++n;
	}

	return n*flag;
}
#include "Date.h"

int main()
{
	Date d1;
	Date d2(2023, 12, 31);

	bool ret1 = d1 == d2;
	cout << ret1 << endl;

	bool ret2 = d1 != d2;
	cout << ret2 << endl;

	bool ret3 = d1 > d2;
	cout << ret3 << endl;

	bool ret4 = d1 >= d2;
	cout << ret4 << endl;
	
	bool ret5 = d1 < d2;
	cout << ret5 << endl;

	bool ret6 = d1 <= d2;
	cout << ret6 << endl;

	Date& ret7 = d1 += 10;
	ret7.Print();

	Date ret8 = d1 +10;
	ret7.Print();

	Date& ret9 = d1 -= 10;
	ret7.Print();

	Date ret10 = d1 - 10;
	ret7.Print();

	Date& reta = ++d1;
	reta.Print();

	Date retb = d1++;
	retb.Print();

	Date& retc = --d1;
	retc.Print();

	Date retd = d1--;
	retd.Print();

	Date d3(2023, 12, 31);
	Date d4(2023, 1, 1);
	cout << d3 - d4 << endl;

	int day = d3.GetdayOfYear();
	cout << "这是当前年的第" << day << "天" << endl;

	return 0;
}

五、const成员

我们先来看一段,代码:

	bool Test(const Date d)
    {    
        //...
    }

这里的const修饰的是Date类的对象d,目的是为了不让他被修改!但我们知道每个成员函数都有一个隐藏的参数---this,他是个同类的指针参数!我们一般在C元那块玩的时候,如果对指针不进行修改我们一般会对这个指针进行const修饰const void* ptr!这里如果不想被修改稿如何做了!我么的策略还是用const修饰!格式如下:

	bool Test(const Date d) const
    {    
        //...
    }

这个就是被const成员函数了!她实际上修饰的this!表示不能对成员的任何属性进行修改!

总结:将const修饰的成员函数称之为const成员函数,const 实际修饰成员函数的隐藏的this指针,表示不能对成员的任何属性进行修改!

这里我又想到了几个经常被问的问题!

const的对象不可以调非const的成员函数,其实这里我在C++入门的那一期已经介绍过了!本质是权限的问题! 权限可以被缩小和平移但不放大!
1. const对象可以调用非const成员函数吗?
权限平移可以!
2. 非const对象可以调用const成员函数吗?
权限缩小可以!
3. const成员函数内可以调用其它的非const成员函数吗?
权限放大不可以!
4. 非const成员函数内可以调用其它的const成员函数吗?
权限缩小可以!

六、取地址以及const取地址操作符重载

这两个函数我们一般都不显示的实现,我们一般都用编译器默认生成的即可!特殊情况下,我们如果需要让别人获取到指定的内容才会重载!

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

OK,本期分享到这里就结束了!2023年的分享也到这里结束了!希望来年可以变的更强!好兄弟,我们明年再见!

你可能感兴趣的:(C++初阶之路,c++,开发语言)