c++类与对象(中)


前言

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。


一、构造函数

1、构造函数介绍

我们回想在前面实现的队列,栈,单链表等,在使用这些数据结构时,都要先调用对应的Init()函数,即初始化函数,如果我们忘记调用,则这些数据结构里面存的就会是随机值。但是我们也不能保证每次使用这些数据结构时都能先调用初始化函数。所以在c++中就增加了构造函数这一个特殊的成员函数。
我们第一个要介绍的就是构造函数,构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。即构造函数在类实例化为对象时会自动调用。

2、构造函数特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下
1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。
在这里插入图片描述
在这里插入图片描述

class Date
{
public:
	//无参构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
		cout << "无参构造函数;" << endl;
	}
	//有参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "有参构造函数;" << endl;

	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

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


int main()
{
	//此时创建对象d1时会自动调用无参构造函数
	Date d1;

	//此时创建对象d2时会自动调用有参构造函数
	Date d2(2023, 8, 3);


	//当创建对象d3时向自动调用无参构造函数时,不可以加括号
	//因为此时会报出未调用原型函数的警告。
	//Date d3();

	return 0;
}

5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
例如下面当将写的有参构造函数和无参构造函数都注释时,此时创建一个对象d1,则编译器会自动生成一个无参的默认构造函数,然后创建对象d1时会自动调用这个自动生成的默认构造函数。
在这里插入图片描述
在这里插入图片描述
但是如果将有参的构造函数的注释去掉,即代表用户显示定义了构造函数,则编译器就不会再自动生成无参构造函数,此时再次创建d1对象并调用无参构造函数时,就会出现错误。这是因为用户只定义了有参构造函数,而Date d1;创建对象时自动调用的是无参构造函数,但是用户没有定义无参构造函数,编译器此时也不会自动生成无参构造函数,所以就会报出没有合适的默认构造函数可用的错误。
在这里插入图片描述

在这里插入图片描述
所以用户自己定义构造函数时,一定要考虑到使用的场景,并且为每个场景都提供合适的构造函数。其实如果我们学习了缺省参数的话,上面分开定义无参构造函数和有参构造函数的方式其实可以使用缺省参数,将这两个函数和为一个。使用缺省参数定义构造函数的话,使用的场景更多了,在创建对象时如果不传参数则缺省参数的值就会生效,如果传一个参数则该参数的缺省值就失效,此时创建对象时,可以只传入第一个参数,也可以只传入前两个参数,还可以传入三个参数。

class Date
{
public:

	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "构造函数;" << endl;

	}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

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


int main()
{
	//此时创建对象d1时会自动调用无参构造函数
	Date d1;

	Date d2(2023, 3, 23);

	Date d3(2023);

	Date d4(2023, 4);

	return 0;
}

6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
所以如果使用全缺省参数来实现默认构造函数的话,无参的默认构造函数就不可以再出现了,因为此时就相当于有了两个默认无参构造参数,编译器不明确使用哪一个,就会产生歧义。所以就会报出对重载函数的调用不明确的错误。

7. C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,编译器生成的默认构造函数对自定义类型成员调用它的默认构造函数,而对于内置类型成员是不做处理的。
我们知道构造函数的作用就类似于初始化函数,那么当我们使用编译器自动生成的默认无参构造函数,编译器将我们的成员变量都初始化为了什么值呢?是0还是其它的值呢?我们可以使用下面的代码来查看一下,但是看到结果时我们发现此时成员变量的值不是0,而是随机值,这是为什么呢?

:这是因为c++中将类型分为内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,比如:int、char…;而自定义类型就是我们使用class、struct、union等自己定义的类型。编译器生成的默认构造函数会对自定义类型成员调用它的默认构造函数,而编译器生成的默认构造函数对于内置类型成员是不做处理的,这就造成了下面的代码中查看_year、_month、_day时发现是随机值。而对于自定义类型成员_birthday,则会调用Birthday的默认构造函数。

class Birthday
{
public:
	Birthday()
	{
		cout << "Birthday构造函数" << endl;
	}

};

class Date
{
public:
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	//内置类型
	int _year;
	int _month;
	int _day;
	//自定义类型
	Birthday _birthday;
};


int main()
{
	//此时创建对象d1时会自动调用无参构造函数
	Date d1;

	d1.Print();

	return 0;
}

在这里插入图片描述
虽然这样的设计会让使用者在使用时容易出错,但是c++已经发布了很久了,不可能更改这一特性,但是C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。这样在类中声明时就可以先给内置类型成员设置一个默认值,这样内置类型成员就不会为一个随机值了。即可以在声明位置给缺省值,此处并不是初始化,因为还没有实例化对象,并没有开辟空间。此处的意思就是如果没有写构造函数,并且默认生成的构造函数对这些内置类型也不做处理,则内置类型的值就为给的缺省值;如果写了构造函数来初始化这些内置类型成员的话,就不会将这些内置类型的值置为缺省值。


class Birthday
{
public:
	Birthday()
	{
		cout << "Birthday构造函数" << endl;
	}

};

class Date
{
public:
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	//内置类型
	int _year = 2000;
	int _month = 1;
	int _day = 1;
	//自定义类型
	Birthday _birthday;
};


int main()
{
	//此时创建对象d1时会自动调用无参构造函数
	Date d1;

	d1.Print();

	return 0;
}

在这里插入图片描述

二、析构函数

1、析构函数介绍

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

2、析构函数特性

析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

class Birthday
{
public:
	Birthday()
	{
		cout << "Birthday构造函数" << endl;
	}
	
	//Birthday类的默认析构函数
	~Birthday()
	{
		cout << "Birthday析构函数" << endl;
	}

};

class Date
{
public:
	/*Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "构造函数;" << endl;

	}*/

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

	//没有显式定义析构函数的话,编译器会自动生成默认的析构函数。

private:
	//内置类型
	int _year = 2000;
	int _month = 1;
	int _day = 1;
	//自定义类型
	Birthday _birthday;
};


int main()
{
	//此时创建对象d1时会自动调用无参构造函数
	Date d1;

	d1.Print();

	return 0;
	//对象生命周期结束时,C++编译系统系统自动调用析构函数,即在return之后会调用d1对象的默认析构函数。
}

在这里插入图片描述
我们经过分析知道了编译器自动生成的默认构造函数对内置类型成员和自定义类型成员所做的处理,那么编译器自动生成的析构函数对内置类型成员和自定义类型成员做什么样的处理呢?
:编译器自动生成的析构函数对内置类型成员和自定义类型成员所做的处理也是不同的,该默认析构函数不会对内置类型成员做资源清理,因为内置类型成员系统会直接将其所占用的内存回收,默认析构函数会调用自定义类型成员的默认析构函数,这样才能保证其内部每个自定义对象都可以销毁。例如上面的代码中对象d1的生命周期为main函数的生命周期,所以当return 0;之后系统就会自动调用Date类的默认析构函数,而Date类的默认析构函数是编译器自动生成的,在该默认析构函数中又会调用类Birthday的默认析构函数,这样就将_birthday对象所占用的内存清理了,这样处理就保证了对象d1内部每个自定义对象都可以正确销毁。

注意:创建哪个类的对象则调用该类的构造函数,销毁哪个类的对象则调用该类的析构函数。

三、拷贝构造函数

1、拷贝构造函数介绍

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时,可否创建一个与已存在对象一模一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

2、拷贝构造函数特征

拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。

class Birthday
{
public:
	Birthday()
	{
		cout << "Birthday构造函数" << endl;
	}
	
	//Birthday类的默认析构函数
	~Birthday()
	{
		cout << "Birthday析构函数" << endl;
	}

};

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "构造函数;" << endl;

	}
	//拷贝构造函数
	//可以看到拷贝构造函数是构造函数的一个重载形式
	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;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

	//没有显式定义析构函数的话,编译器会自动生成默认的析构函数。

private:
	//内置类型
	int _year = 2000;
	int _month = 1;
	int _day = 1;
	//自定义类型
	Birthday _birthday;
};


int main()
{
	Date d1(2023,2,3);
	//即将d1对象拷贝生成d2对象
	Date d2(d1);
	//拷贝构造还可以这样写
	Date d3 = d1;
	d1.Print();
	d2.Print();
	d3.Print();
	return 0;
	//对象生命周期结束时,C++编译系统系统自动调用析构函数,即在return之后会调用d1对象的默认析构函数。
}

在这里插入图片描述

2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
我们知道当函数使用传值传参时,本质是在栈区中将传入的实参拷贝一份临时变量来当作形参,然后在函数中改变的都是这个临时变量,而不会影响真正的实参。当我们对下面的代码进行调试时会发现当执行到Func1(d1);时会直接跳转到Date类中的拷贝构造函数内。这是因为对于内置类型编译器可以直接拷贝值过去;而对于自定义类型的话,编译器不知道怎么拷贝,所以编译器会调用该自定义类型中的拷贝构造函数。所以在调用Func1(d1)时会跳到Date类的拷贝构造函数中去,因为Func1()函数为传值传参,而传值传参会将实参拷贝一份临时变量,此时发生了Date类类型的拷贝,所以编译器会去Date类中调用该类的拷贝构造函数。

在这里插入图片描述
在这里插入图片描述

class Birthday
{
public:
	Birthday()
	{
		cout << "Birthday构造函数" << endl;
	}
	
	//Birthday类的默认析构函数
	~Birthday()
	{
		cout << "Birthday析构函数" << endl;
	}

};

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "构造函数;" << endl;

	}
	//拷贝构造函数
	//可以看到拷贝构造函数是构造函数的一个重载形式
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

	//没有显式定义析构函数的话,编译器会自动生成默认的析构函数。

private:
	//内置类型
	int _year = 2000;
	int _month = 1;
	int _day = 1;
	//自定义类型
	Birthday _birthday;
};

void Func1(Date d)
{

}

void Func2(Date& d)
{

}
int main()
{
	Date d1(2023,2,3);
	Func1(d1);
	Func2(d1);

	return 0;
}

当我们从上面的分析得出了传值传参传入的为自定义类型时,会先调用该自定义类型的拷贝构造函数的结论时。我们就可以分析出为什么使用传值方式编译器直接报错,因为会引发无穷递归调用的原因了。因为传值传参传入的为自定义类型时,会先调用该自定义类型的拷贝构造函数,而该自定义类型的拷贝构造函数就是传值传参的函数,并且该函数需要传入该自定义类型,而传值传参函数传入的为自定义类型时,就会先调用该自定义类型的拷贝构造函数。这就引发了无穷递归调用,所以拷贝构造需要传引用调用,因为传引用调用不会发生自定义类型的拷贝。

在这里插入图片描述
在这里插入图片描述
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
即默认拷贝构造函数就是将该类类型的内置类型的值直接拷贝过去,然后调用自定义类型的拷贝构造函数完成拷贝。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "构造函数;" << endl;

	}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

	//没有显式定义析构函数的话,编译器会自动生成默认的析构函数。

private:
	//内置类型
	int _year = 2000;
	int _month = 1;
	int _day = 1;
};


int main()
{
	Date d1(2023, 2, 3);
	Date d2(d1);

	d1.Print();
	d2.Print();
	return 0;
}

在这里插入图片描述
在这里插入图片描述
4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然还需要,因为编译器生成的默认拷贝构造函数只能完成浅拷贝,而很多时候对象的拷贝并不像上面那么简单。
例如我们之前实现的Stack类,该类里面定义了一个int*data指针类型的成员变量,如果要对Stack类创建时使用编译器自动生成的拷贝构造函数,则会出现很大的问题。因为如果我们创建了一个s1对象,然后将s2对象拷贝s1对象时,此时调用编译器自动生成的拷贝构造函数,我们可以发现该拷贝构造函数直接将s1对象的data数组的地址拷贝给了s2,那么就造成了s1和s2两个栈共用一个数组来存储数据,这是肯定不行的,所以这种情况编译器自动生成的拷贝构造函数就不能用了,需要用户自己写一个深拷贝的拷贝构造函数。

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。在这里插入图片描述
在这里插入图片描述
向上面的情况使用编译器自动生成的拷贝构造函数就会出现错误,因为上面的情况就需要深拷贝来完成拷贝。下面我们可以试着写一个简单的深拷贝。下面的代码中Stack类的默认拷贝构造函数就为一个深拷贝,因为该函数中又申请了一片新的数组空间来给s2对象的_data,并且还将s1对象的数据拷贝给了s2。此时可以看到s1的_data和s2的_data指向的不是同一片内存空间。

typedef int STDataType;

class Stack
{
public:
	//成员函数
	
	Stack(int n = 4)
	{
		_data = (int*)malloc(sizeof(int) * n);
		if (nullptr == _data)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = _capacity = 0;
	}

	Stack(const Stack& s)
	{
		STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * s._capacity);
		if (nullptr == tmp)
		{
			perror("malloc fail");
			exit(-1);
		}
		_data = tmp;
		memcpy(_data, s._data, sizeof(STDataType) * s._top);
		_capacity = s._capacity;
		_top = s._top;
	}
	void CheckCapacity()
	{
		int newCapacity = _capacity == 0 ? 4 : _capacity * 2;
		if (_top == _capacity)
		{
			STDataType* tmp = (STDataType*)realloc(_data, newCapacity * sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				exit(-1);
			}
			_data = tmp;
			_capacity = newCapacity;
		}
	}

	void Push(STDataType x)
	{
		CheckCapacity();
		_data[_top] = x;
		_top++;
	}

	bool IsEmpty()
	{
		if (_top == 0)
		{
			return true;
		}
		return false;
	}

	void Pop()
	{
		assert(!IsEmpty());
		_top--;
	}

	

	STDataType Top()
	{
		assert(!IsEmpty());
		return _data[_top - 1];
	}

private:
	//成员变量/类属性
	STDataType* _data;
	int _top;
	int _capacity;
};


int main()
{
	Stack s1(10);
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);

	Stack s2(s1);

	cout << s1.Top() << endl;
	cout << s2.Top() << endl;

	s2.Push(5);
	cout << s1.Top() << endl;
	cout << s2.Top() << endl;

	return 0;
}

在这里插入图片描述

5. 拷贝构造函数典型调用场景:
(1). 使用已存在对象创建新对象
(2). 函数参数类型为类类型对象
(3). 函数返回值类型为类类型对象

(1)和(2)我们在上面已经分析过,而函数返回值类型为类类型对象时也会调用类的拷贝构造函数,我们知道传值返回函数在返回值时,编译器都会生成一个函数返回对象的拷贝,用来作为函数调用返回值,这样调用函数的栈帧销毁了,返回值也还有拷贝的一份,不会让函数的返回值丢失。所以这就用到了一次自定义类型的拷贝,所以会调用拷贝构造函数。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "构造函数;" << endl;

	}

	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "构造函数" << endl;
	}

	//函数参数类型为类类型对象
	//函数返回值类型为类类型对象
	Date Test(Date d)
	{
		//使用已存在对象创建新对象
		Date tmp(d);
		return tmp;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

	//没有显式定义析构函数的话,编译器会自动生成默认的析构函数。

private:
	//内置类型
	int _year = 2000;
	int _month = 1;
	int _day = 1;
};


int main()
{
	Date d1(2023, 2, 3);
	//使用已存在对象创建新对象
	Date d2(d1);
	Date d3 = d1;
	d1.Test(d1);
	return 0;
}

3、拷贝构造函数的应用 – 求n天后的日期

计算n天以后的日期为多少。
第一种方法

//求n天后的日期
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int GetMonthDay(int year, int month)
	{
		assert(month > 0 && month < 13);
		int monthArray[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 29;
		}
		else
		{
			return monthArray[month];
		}
	}
	Date GetAfterDay(int x)
	{
		_day += x;
		//判断如果天数大于该月数的天数就进位
		while (_day > GetMonthDay(_year, _month))
		{
			_day -= GetMonthDay(_year, _month);
			++_month;
			//如果月数为13就进位
			if (_month == 13)
			{
				_year++;
				_month = 1;
			}
		}
		//返回该Date对象
		return *this;
	}
	void Print()
	{
		cout << _year << "年" << _month<< "月" << _day << "日" << endl;
	}

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

int main()
{
	Date d1(2023, 9, 11);
	Date d2 = d1.GetAfterDay(100);
	//d1对象也会被改变
	d1.Print();
	d2.Print();

	return 0;
}

第二种方法
第一种方法会将d1对象的值也改变,所以我们可以在GetAfterDay()方法中创建一个tmp临时变量,然后将该变量返回,这样d1对象的值就不会改变了。

//求n天后的日期
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int GetMonthDay(int year, int month)
	{
		assert(month > 0 && month < 13);
		int monthArray[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 29;
		}
		else
		{
			return monthArray[month];
		}
	}
	Date GetAfterDay(int x)
	{
		//会调用拷贝构造函数
		Date tmp = *this;
		tmp._day += x;
		//判断如果天数大于该月数的天数就进位
		while (tmp._day > GetMonthDay(tmp._year, tmp._month))
		{
			tmp._day -= GetMonthDay(tmp._year, tmp._month);
			++(tmp._month);
			//如果月数为13就进位
			if (tmp._month == 13)
			{
				tmp._year++;
				tmp._month = 1;
			}
		}
		//返回该Date对象
		//会调用拷贝构造函数
		return tmp;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

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

int main()
{
	Date d1(2023, 9, 11);
	Date d2 = d1.GetAfterDay(100);
	//d1对象不会被改变
	d1.Print();
	d2.Print();

	return 0;
}

上面的两种方法可以总结为一个为改变对象,一个不改变对象。合在一起可以写两个函数,一个AddEqual()函数为改变对象,其中将该函数的返回值设置为了传引用返回,因为*this为main中的d1对象,不会随着AddEqual函数的销毁而销毁,所以可以使用传引用返回,这样就减少了一次拷贝构造。因为传值返回编译器都会生成一个函数返回对象的拷贝,这样就进行了一次拷贝构造函数的调用,而使用传引用返回就减少了一次拷贝。而另一个不改变对象的Add()函数需要有两次拷贝构造函数的调用。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int GetMonthDay(int year, int month)
	{
		assert(month > 0 && month < 13);
		int monthArray[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 29;
		}
		else
		{
			return monthArray[month];
		}
	}

	//对象改变
	Date& AddEqual(int x)
	{
		_day += x;
		//判断如果天数大于该月数的天数就进位
		while (_day > GetMonthDay(_year, _month))
		{
			_day -= GetMonthDay(_year, _month);
			++_month;
			//如果月数为13就进位
			if (_month == 13)
			{
				_year++;
				_month = 1;
			}
		}
		//返回该Date对象
		//会调用拷贝构造函数
		return *this;
	}

	//对象不改变
	Date Add(int x)
	{
		//调用一次拷贝构造函数
		Date tmp = *this;
		tmp._day += x;
		//判断如果天数大于该月数的天数就进位
		while (tmp._day > GetMonthDay(tmp._year, tmp._month))
		{
			tmp._day -= GetMonthDay(tmp._year, tmp._month);
			++(tmp._month);
			//如果月数为13就进位
			if (tmp._month == 13)
			{
				tmp._year++;
				tmp._month = 1;
			}
		}
		//返回该Date对象
		//再调用一次拷贝构造函数
		return tmp;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

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

int main()
{
	Date d1(2023, 9, 11);
	Date d2 = d1.Add(100);
	d1.Print();
	d2.Print();

	Date d3 = d1.AddEqual(200);
	d1.Print();
	d3.Print();

	return 0;
}

四、赋值运算符重载

1、运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表) 例如:bool operator>(const Date& d)。
注意:
(1). 不能通过连接其他符号来创建新的操作符:比如operator@
(2). 重载操作符必须有一个类类型参数
(3). 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
(4). 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
(5). .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

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

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

//运算符重载
//运算符的操作数有几个,就有几个参数
//并且因为==只是判断d1对象和d2对象是否相等,所以我们使用传引用传参,可以减少调用拷贝构造函数,以便减少开销
bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

int main()
{
	Date d1(2023, 9, 12);
	Date d2(2023, 3, 12);
	//上面的运算符重载就类似于定义了一个名字为operator==的函数,所以可以使用函数名加()函数调用符来进行调用
	cout << operator==(d1, d2) << endl;
	//但是c++中加入了运算符重载,编译器在判断d1 == d2时会检查有没有实现==的运算符重载
	//如果实现了==的运算符重载,则会直接call到operator==函数的地址
	cout << (d1 == d2) << endl;  //要注意运算符优先级,<<的优先级大于==
	//上面的d1 == d2也会转换成去调用operator==()这个函数

	return 0;
}

可以看到d1==d2在底层也是调用的operator == 这个函数,即会直接call到operator = =函数的地址去。
在这里插入图片描述
在这里插入图片描述
上面的写法中因为我们将运算符重载函数写到了Date类的外面,所以此时如果访问Date类的成员变量,就需要将Date类的成员变量变为public,即可以公共访问,这样上面的operator ==函数才不会出错。而当我们将该函数定义在Date类中,变为Date类的成员函数,此时就不需要将Date类的成员变量设置为public,而且在Date类中定义 operator = =时,就不需要再写两个参数了,因为类的成员函数有一个隐式的this指针。

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

	//因为有一个隐式的this指针来表示d1,所以只需定义形参d2即可
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

//此时成员变量为私有
private:
	int _year;
	int _month;
	int _day;
};



int main()
{
	Date d1(2023, 9, 12);
	Date d2(2023, 3, 12);
	//此时就需要这样访问operator==函数了
	cout << d1.operator==(d2)<<endl;
	cout << (d1 == d2) << endl;  //要注意运算符优先级,<<的优先级大于==
	//上面的d1 == d2也会转换成去d1.operator==(d2)这个函数

	return 0;
}

2、一些运算符重载的实现

我们在上面实现了==运算符的重载后,还可以继续尝试实现其它的运算符重载。下面我们可以实现一下<号的运算符重载。

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

	//因为有一个隐式的this指针来表示d1,所以只需定义形参d2即可
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

	// d1 < d2
	bool 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;
		}*/

		//上面的代码可以简化为这样
		return (_year < d._year) 
			|| (_year == d._year && _month < d._month) 
			|| (_year == d._year && _month == d._month && _day < d._day);
	}

//此时成员变量为私有
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1(2023, 9, 12);
	Date d2(2023, 3, 12);
	Date d3(2022, 3, 1);
	cout << d1.operator<(d2) << endl;
	cout << (d3 < d2) << endl;

	return 0;
}

当我们实现了==和<的运算符重载函数后,其他的类似的运算符重载的函数我们就可以复用上面实现的函数来实现。这样我们就实现了这些运算符的重载函数。

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

	//因为有一个隐式的this指针来表示d1,所以只需定义形参d2即可
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

	// d1 < d2
	bool 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;
		}*/

		//上面的代码可以简化为这样
		return (_year < d._year) 
			|| (_year == d._year && _month < d._month) 
			|| (_year == d._year && _month == d._month && _day < d._day);
	}

	// d1 <= d2
	bool operator<=(const Date& d)
	{
		//*this就表示d1对象,d表示d2对象
		//先调用<的运算符重载函数判断d1是否小于d2
		//如果d1<d2,再调用==的运算符重载函数判断d1是否等于d2
		return *this < d || *this == d;
	}

	// d1 > d2
	bool operator>(const Date& d)
	{
		//如果d1不小于等于d2,那么就是d1大于d2,
		//所以直接对<=运算符重载函数逻辑取反即可得到>的运算符重载函数
		return !(*this <= d);
	}

	// d1 >= d2
	bool operator>=(const Date& d)
	{
		//如果d1不小于d2,那么就是d1大于等于d2
		//所以可以直接对<的运算符重载函数逻辑取反得到>=的运算符重载函数
		return !(*this < d);
	}

	// d1 != d2
	bool operator!=(const Date& d)
	{
		//如果d1==d2不成立,那么d1!=d2
		//所以直接对==的运算符重载函数逻辑取反得到!=的运算符重载函数
		return !(*this == d);
	}

//此时成员变量为私有
private:
	int _year;
	int _month;
	int _day;
};



int main()
{
	Date d1(2023, 9, 12);
	Date d2(2023, 3, 12);
	Date d3(2023, 3, 12);
	cout << d1.operator<(d2) << endl;
	cout << (d3 < d2) << endl;

	cout << (d1 > d2) << endl;

	cout << (d2 >= d3) << endl;

	cout << (d2 <= d3) << endl;
	cout << (d1 != d2) << endl;
	return 0;
}

3、赋值运算符重载

  1. 赋值运算符重载格式
    参数类型:const T&,传递引用可以提高传参效率
    返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
    检测是否自己给自己赋值
    返回*this :要符合连续赋值的含义
    当我们将=的运算符重载函数写成这样,那么在遇到像 d1=d2=d3 这样的连续赋值时,就会报出错误。
// d1 = d2
	void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

所以我们需要给该函数返回一个Date对象,这样才能保持运算符的特性,而因为d1、d2、d3不会随着operator=函数的销毁而销毁,所以使用传引用返回比较好,这样就减少了拷贝构造函数的调用。

	// d1 = d2
	//返回值为了支持连续赋值,保持运算符的特性
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		//将d1对象返回
		return *this;
	}

我们在使用=赋值运算符时还会遇到这样的情况,将自己赋值给自己,例如d1=d1,所以我们还需要进行判断,是不是进行了将自己赋值给自己,如果是的话就不需要做任何操作。

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

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

	
	// d1 = d2
	//返回值为了支持连续赋值,保持运算符的特性
	Date& operator=(const Date& d)
	{
		//比较两个对象的地址,如果地址不一样则不为d1=d1这样的自己给自己赋值
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		
		//将d1对象返回
		return *this;
	}

	//此时成员变量为私有
private:
	int _year;
	int _month;
	int _day;
};



int main()
{
	Date d1(2023, 9, 12);
	Date d2(2023, 3, 12);
	Date d3(2023, 3, 12);
	d1.Print();
	d2.Print();
	d1 = d2;
	d1.Print();
	d2.Print();

	d1 = d2 = d3;
	d1.Print();
	d2.Print();
	d3.Print();

	d1 = d1;
	d1.Print();
	return 0;
}

赋值运算符只能重载成类的成员函数不能重载成全局函数
我们在前面写了==的全局的运算符重载函数,但是为什么=的运算符重载函数就不能写为全局的了呢?
这是因为赋值运算符如果不在类中显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
在这里插入图片描述
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现赋值运算符重载函数吗?答案是当然需要了,这其实又和拷贝构造函数遇到同样的问题了,对于我们写的Date时间类,值拷贝就可以了,但是如果遇到像前面的Stack类时,该类里面的成员变量中有一个指针变量data指向一个数组,此时如果使用默认的赋值运算符重载函数就会在s1=s2后,将s1和s2指向同一片内存空间,即s1和s2使用一个数组存储数据,这显然是不可以的。所以需要 注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

4、日期类的实现

我们已经初步学习了c++中的类与对象的一些知识,可以将前面的知识综合起来,实现一个日期类,并且为该日期类实现运算符的重载函数。而且可以计算该日期加上一个天数后日期为多少。

#include<iostream>
using namespace std;


class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1);
	void Print();
	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=(const Date& d);
	Date& operator+=(int day);
	int GetMonthDay(int year, int month);
	Date operator+(int day);

//此时成员变量为私有
private:
	int _year;
	int _month;
	int _day;
};

#include"Date.h"


Date::Date(int year, int month, int day)
{
	if (month > 0 && month < 13 && (day > 0 && day <= GetMonthDay(year, month)))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "日期非法" << endl;
	}
}

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

//因为有一个隐式的this指针来表示d1,所以只需定义形参d2即可
bool Date::operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

// d1 < d2
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;
	}*/

	//上面的代码可以简化为这样
	return (_year > d._year)
		|| (_year == d._year && _month < d._month)
		|| (_year == d._year && _month == d._month && _day < d._day);
}

// d1 <= d2
bool Date::operator<=(const Date& d)
{
	//*this就表示d1对象,d表示d2对象
	//先调用<的运算符重载函数判断d1是否小于d2
	//如果d1<d2,再调用==的运算符重载函数判断d1是否等于d2
	return *this < d || *this == d;
}

// d1 > d2
bool Date::operator>(const Date& d)
{
	//如果d1不小于等于d2,那么就是d1大于d2,
	//所以直接对<=运算符重载函数逻辑取反即可得到>的运算符重载函数
	return !(*this <= d);
}

// d1 >= d2
bool Date::operator>=(const Date& d)
{
	//如果d1不小于d2,那么就是d1大于等于d2
	//所以可以直接对<的运算符重载函数逻辑取反得到>=的运算符重载函数
	return !(*this < d);
}

// d1 != d2
bool Date::operator!=(const Date& d)
{
	//如果d1==d2不成立,那么d1!=d2
	//所以直接对==的运算符重载函数逻辑取反得到!=的运算符重载函数
	return !(*this == d);
}

// d1 = d2
//返回值为了支持连续赋值,保持运算符的特性
Date& Date::operator=(const Date& d)
{
	//比较两个对象的地址,如果地址不一样则不为d1=d1这样的自己给自己赋值
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	//将d1对象返回
	return *this;
}

int Date::GetMonthDay(int year, int month)
{
	int arr[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 29;
	}
	else
	{
		return arr[month];
	}
}


//因为+=也可以连续赋值,所以需要返回值,而该赋值运算符为+=,所以将改变后的d1返回
//使用传引用返回可以减少一次拷贝构造函数的调用
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)
{
	//可以复用前面写的+=来实现+的赋值运算符重载函数

	//先调用拷贝构造函数将d1拷贝给tmp对象
	Date tmp(*this);
	//然后改变tmp对象
	tmp += day;
	//将tmp对象返回,此时会再调用一次拷贝构造函数
	return tmp;
}
//日期类
#include"Date.h"
int main()
{
	Date d1(2023, 5, 6);
	Date d2 = d1+100;
	d1.Print();
	d2.Print();
	d1 += 100;
	d1.Print();


	return 0;
}

上面的代码就是Date类的常规实现,当我们在实现+的运算符重载函数时,我们复用了+=的运算符重载函数,而其实我们也可以先实现+的运算符重载函数,然后在实现+=的运算符函数时复用+的运算符重载函数。对比了两种实现方法后,我们可以发现第一种方法比较好一点,因为+的运算符重载函数需要两次拷贝构造函数的调用,第二种方法中复用+的运算符重载函数来实现+=时使+=运算符重载函数也调用了两次拷贝构造函数,而第一种方法中+=的运算符重载函数不需要调用拷贝构造函数,减少了程序开销。
先实现+=的运算符重载函数,然后复用+=的运算符重载函数实现+的运算符重载函数。

//因为+=也可以连续赋值,所以需要返回值,而该赋值运算符为+=,所以将改变后的d1返回
//使用传引用返回可以减少一次拷贝构造函数的调用
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)
{
	//可以复用前面写的+=来实现+的赋值运算符重载函数

	//先调用拷贝构造函数将d1拷贝给tmp对象
	Date tmp(*this);
	//然后改变tmp对象
	tmp += day;
	//将tmp对象返回,此时会再调用一次拷贝构造函数
	return tmp;
}

先实现+的运算符重载函数,然后复用+的运算符重载函数实现+=的运算符重载函数。

Date Date::operator+(int 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;
}


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

+=我们按照进位的逻辑来实现,那么-=我们就可以按照借位的逻辑来实现,我们可以按照这个结论来先实现 -= 。

Date& Date::operator-=(int day)
{
	//先让d1的天数为负数
	_day -= day;
	//如果天为0时也要转换,因为此时为上一个月的最后一天,例如2月0日为1月31日
	while (_day <= 0)
	{
		//向月去借位
		--_month;
		//如果月为0就去向年借位
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}
		//将借的位加上
		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

上面写的+=和-=只适合+和-正数,当这样使用时就会出现错误,d1 += -100 或 d1 -= -100;这时得到的日期就是错误的。此时就需要再对+=和-+运算符的重载函数进行修改。即判断如果天数为负数时就做另外的运算。

Date& Date::operator+=(int day)
{
	//当+=一个负数,就相当于-=这个负数的绝对值
	if (day < 0)
	{
		//复用-=运算符重载函数
		*this -= -day;
		return *this;
	}
	_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)
	{
		*this += -day;
		return *this;
	}
	//先让d1的天数为负数
	_day -= day;
	//如果天为0时也要转换,因为此时为上一个月的最后一天,例如2月0日为1月31日
	while (_day <= 0)
	{
		//向月去借位
		--_month;
		//如果月为0就去向年借位
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}
		//将借的位加上
		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

5、前置++和后置++ && 前置- -和后置- -

在实现了Date类的一些运算符重载函数后,我们还可以复用这些运算符重载函数来实现前置++和后置++,我们知道前置++是先将目标+1,然后返回+1的目标;后置++是先返回目标,然后再将目标+1。经过下面的代码我们可以看到自定义类型中前置++的效率高,因为不需要拷贝,而后置++需要创建临时变量tmp,需要拷贝。而且前置++和后置++形成了函数重载,那么在使用时编译器是怎样知道要调用前置++还是后置++呢?在这里其实编译器做了一个特殊处理,即调用后置++时向函数中传入了一个值,这样在链接时就可以区分去调用前置++还是后置++函数了。

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

//可以看到运算符重载函数也可以实现重载
//int参数的设计,仅仅是为了占位,这样才能和前置++的运算符重载函数形成重载
Date Date::operator++(int)
{
	//先将没有++的*this存起来
	Date tmp(*this);
	//然后将*this+1
	*this += 1;
	//返回没有+1的tmp
	return tmp;
}

//日期类
#include"Date.h"
int main()
{
	Date d1(2023, 5, 6);

	//编译器在调用后置++时做了一个特殊处理,即调用函数时传入了一个值。
	d1++;  //d1.operator++(0)
	++d1;  //d1.operator++()

	return 0;
}

当知道了前置++和后置++的写法后,前置- -和后置- -也是一样的处理。

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

Date& Date::operator--(int)
{
	Date tmp(*this);
	*this -= 1;
	return tmp;
}
#include"Date.h"
int main()
{
	Date d1(2023, 5, 6);

	//编译器在调用后置--时做了一个特殊处理,即调用函数时传入了一个值
	//这样才能区分去调用前置--还是后置--的运算符重载函数
	d1--;  //d1.operator--(0)
	--d1;  //d1.operator--()

	return 0;
}

6、求两个日期相减的天数(d1 - d2)

因为一个日期减去一个天数没有多大意义,所以我们将-运算符的重载函数写为求两个日期相减的天数。

int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	//如果发现d1<d2,就将d2设置为max,将d1设置为min,此时d1-d2就为一个负数,所以将flag设为-1
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int n = 0;
	while (min != max)
	{
		//自定义类型中前置++效率高一点
		++min;
		++n;
	}

	return (flag*n);
}

7、总结

上面就是日期类的全部实现,将完整的代码放入到下面。

#pragma once
#include<iostream>
using namespace std;


class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1);
	void Print();
	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=(const Date& d);
	Date& operator+=(int day);
	int GetMonthDay(int year, int month);
	Date operator+(int day);
	Date& operator-=(int day);
	int operator-(const Date& d);
	Date& operator++();
	//int参数的设计,仅仅是为了占位,这样才能和前置++的运算符重载函数形成重载
	Date operator++(int);
	Date& operator--();
	//int参数的设计,仅仅是为了占位,这样才能和前置--的运算符重载函数形成重载
	Date& operator--(int);
//此时成员变量为私有
private:
	int _year;
	int _month;
	int _day;
};
#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"


Date::Date(int year, int month, int day)
{
	if (month > 0 && month < 13 && (day > 0 && day <= GetMonthDay(year, month)))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "日期非法" << endl;
	}
}

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

//因为有一个隐式的this指针来表示d1,所以只需定义形参d2即可
bool Date::operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

// d1 < d2
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;
	}*/

	//上面的代码可以简化为这样
	return (_year < d._year)
		|| (_year == d._year && _month < d._month)
		|| (_year == d._year && _month == d._month && _day < d._day);
}

// d1 <= d2
bool Date::operator<=(const Date& d)
{
	//*this就表示d1对象,d表示d2对象
	//先调用<的运算符重载函数判断d1是否小于d2
	//如果d1<d2,再调用==的运算符重载函数判断d1是否等于d2
	return *this < d || *this == d;
}

// d1 > d2
bool Date::operator>(const Date& d)
{
	//如果d1不小于等于d2,那么就是d1大于d2,
	//所以直接对<=运算符重载函数逻辑取反即可得到>的运算符重载函数
	return !(*this <= d);
}

// d1 >= d2
bool Date::operator>=(const Date& d)
{
	//如果d1不小于d2,那么就是d1大于等于d2
	//所以可以直接对<的运算符重载函数逻辑取反得到>=的运算符重载函数
	return !(*this < d);
}

// d1 != d2
bool Date::operator!=(const Date& d)
{
	//如果d1==d2不成立,那么d1!=d2
	//所以直接对==的运算符重载函数逻辑取反得到!=的运算符重载函数
	return !(*this == d);
}

// d1 = d2
//返回值为了支持连续赋值,保持运算符的特性
Date& Date::operator=(const Date& d)
{
	//比较两个对象的地址,如果地址不一样则不为d1=d1这样的自己给自己赋值
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	//将d1对象返回
	return *this;
}

int Date::GetMonthDay(int year, int month)
{
	int arr[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 29;
	}
	else
	{
		return arr[month];
	}
}


//因为+=也可以连续赋值,所以需要返回值,而该赋值运算符为+=,所以将改变后的d1返回
//使用传引用返回可以减少一次拷贝构造函数的调用
Date& Date::operator+=(int day)
{
	//当+=一个负数,就相当于-=这个负数的绝对值
	if (day < 0)
	{
		//复用-=运算符重载函数
		*this -= -day;
		return *this;
	}
	_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)
{
	//可以复用前面写的+=来实现+的赋值运算符重载函数

	//先调用拷贝构造函数将d1拷贝给tmp对象
	Date tmp(*this);
	//然后改变tmp对象
	tmp += day;
	//将tmp对象返回,此时会再调用一次拷贝构造函数
	return tmp;
}


//Date Date::operator+(int 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;
//}
//
//
//Date& Date::operator+=(int day)
//{
//	*this = *this + day;
//	return *this;
//}


Date& Date::operator-=(int day)
{
	//如果-=一个负数,就相当于+=这个负数的绝对值
	if (day < 0)
	{
		*this += -day;
		return *this;
	}
	//先让d1的天数为负数
	_day -= day;
	//如果天为0时也要转换,因为此时为上一个月的最后一天,例如2月0日为1月31日
	while (_day <= 0)
	{
		//向月去借位
		--_month;
		//如果月为0就去向年借位
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}
		//将借的位加上
		_day += GetMonthDay(_year, _month);
	}

	return *this;
}


int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	//如果发现d1<d2,就将d2设置为max,将d1设置为min,此时d1-d2就为一个负数,所以将flag设为-1
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int n = 0;
	while (min != max)
	{
		//自定义类型中前置++效率高一点
		++min;
		++n;
	}

	return n;
}



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

//可以看到运算符重载函数也可以实现重载
//int参数的设计,仅仅是为了占位,这样才能和前置++的运算符重载函数形成重载
Date Date::operator++(int)
{
	//先将没有++的*this存起来
	Date tmp(*this);
	//然后将*this+1
	*this += 1;
	//返回没有+1的tmp
	return tmp;
}


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

Date& Date::operator--(int)
{
	Date tmp(*this);
	*this -= 1;
	return tmp;
}
//日期类
#include"Date.h"
void Test1()
{
	Date d1(2023, 9, 13);
	Date d2(2000, 5, 22);
	cout << d1 - d2 << endl;
}
void Test2()
{
	Date d1(2023, 9, 13);
	Date d2 = d1 + 1000;
	d2.Print();

}
void Test3()
{
	Date d1(2023, 9, 13);
	d1 -= 8514;
	d1.Print();
}
int main()
{
	Test1();
	Test2();
	Test3();
	return 0;
}

8、<<流插入和>>流提取

8.1 <<流插入符重载函数

我们可以看到c++库中对于<<符号就使用到了运算符重载函数,即c++中对<<运算符进行了运算符重载,并且<<运算符的重载函数也进行了重载,即<<符号可以接收不同的参数,这就是为什么c++中不需要进行类型判断,因为<<对每个类型都进行了重载,即参数为任何内置类型的<<函数都有。而且cout就是一个ostream对象。
在这里插入图片描述
我们写出<<运算符的重载函数如下。

//因为cout为ostream对象,所以我们将形参设为cout对象的引用,即out就表示cout
void Date::operator<<(ostream& out)
{
	out << _year << "年" << _month << "月" << _day << "日" << endl;
}

但是当我们使用时会发现cout<<d1;会报错,而d1<<cout;和d1.operator(cout)不会报错,这是因为我们写的<<运算符重载函数中的this为一个Date类类型的对象,而cout<<d1这样的形式调用的运算符重载函数中this为一个ostream类类型的对象,所以如果想要cout<<d1这样调用<<运算符的重载函数,我们就需要将第一个参数置为ostream,但是类成员函数默认的将隐式参数this作为第一个参数。

在这里插入图片描述
在这里插入图片描述
所以我们就可以将<<运算符的重载函数变为全局的函数,然后设置两个形参,将cout对象设置为第一个形参。但是此时函数内就不能访问Date类的私有成员变量了,我们可以将该函数设置为Date类的友元函数,这样该函数就可以访问Date类中的私有成员变量了。 此时将第一个函数设置为ostream类类型的对象,这样就可以使用cout<<d1来调用该函数了。
在这里插入图片描述

//将第一个参数设置为ostream对象,第二个参数为Date对象,这样就可以cout<<d1这样调用函数了。
//并且将该函数设置为Date类的友元函数,这样该函数就可以访问Date类中的私有成员变量了。
void operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

在这里插入图片描述
但是此时又出现了一个问题,当我们连续插入时又会出现错误。这时因为cout<<d1执行完后,<<d2的右边没有ostream对象了,相当于operator<<(operator<<(cout,d1),d2);而在执行完operator<<(cout,d1)后执行operato<<r( ,d2)时发现少了第一个参数,所以该函数还需要一个返回值,返回cout这个ostream类类型的对象。然后再连续插入时就不会出错了。
在这里插入图片描述

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

在这里插入图片描述
此时我们再对比c++库中对<<的运算符重载函数,可以发现库中的写法也是这样写的。
在这里插入图片描述
但是为什么c++中需要将<<运算符进行运算符重载呢?
这是因为c语言中的printf无法打印自定义类型的成员变量,因为成员变量为私有,受访问限制。所以c++写了流插入,用户也可以进行<<的运算符重载函数,这样就可以进行自定义类型的成员变量的打印。但是cout的效率比printf低,因为printf打印不同类型的值只需要一次调用,而cout打印不同的值需要多次调用函数,因为在<<运算符重载函数中每使用一次<<都会调用一次函数。
在这里插入图片描述

8.2 >>流提取符重载函数

上面我们实现了<<流插入符重载函数,下面我们就可以实现>>流提取符重载函数了。>>的运算符重载函数中因为需要改变d1对象中成员变量的值,所以不能将该函数的第二个参数使用const修饰。并且我们写了这两个运算符重载函数后发现它们的实际代码量很少,而且还需要经常调用,所以我们其实可以将这两个函数写为内联函数。

//该函数就不能将Date& d用const修饰,因为我们需要修改d1对象中成员变量的值
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

在这里插入图片描述

五、const成员

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
当我们用const修饰我们创建的对象时,该对象调用它的成员方法就会报错。
在这里插入图片描述
上面出错的原因是因为发生了权限的放大。const A aa实例化了一个aa对象,当aa调用Print函数时,因为c++会Print()函数设置一个隐式形参,即void Print(A* this),该隐式形参为一个A* 类型的指针。而aa.Print调用函数时,因为aa被const修饰,所以aa.Print()调用函数时传入的是一个const A* 类型的指针,即将一个权限为只读的指针变量传参给一个权限为读写的指针变量,所以发生了权限放大,才会报错。此时需要将该隐式形参也使用const修饰才不会报错,但是c++中不可以显示定义和修改this。
在这里插入图片描述
但是c++也为我们提供了上面这种情况的解决办法,即将函数写为void Print() const这个形式,这个const就代表修饰的为隐式形参*this。此时我们就可以发现错误没有了。
在这里插入图片描述
那么我们又会有疑问了,上面将类实例化的对象使用const修饰我们几乎都不会使用到,还废那么大劲解决这个问题干啥。虽然上面的情况我们很少会遇到,但是下面的情况我们却会经常遇到。即我们在Func函数中将形参定义的引用使用const修饰,此时在该函数中调用这个对象的方法就会出现错误。而如果在函数Print()后面加上const就不会出错了。
在这里插入图片描述
在这里插入图片描述

所以当类的成员函数里面改变*this对象的值时,该函数不能用const修饰。
因为* this已经被const修饰了,则就不能通过解引用this指针来修改this指针指向的对象。
在这里插入图片描述
当类的成员函数内部不改变*this对象的值时,该函数最好加上const修饰
所以前面我们实现Date类时写的很多函数其实都需要加上const修饰,并且声明和定义分离的函数,声明和定义都需要加上const。
在这里插入图片描述

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

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
如果自己实现的话也很简单,只需将this指针的值返回即可。

class A
{
public:
	void Print() const  //等价于void Print(const A* this)
	{
		cout << _a << endl;
	}
	//取地址符&的操作符重载函数
	A* operator&()
	{
		return this;
	}
	//const取地址操作符重载函数
	const A* operator&() const
	{
		return this;
	}
private:
	int _a = 20;
};

void Func(const A& x)
{
	cout << &x << endl;
}

int main()
{
	A aa;
	const A bb;
	cout << &aa << endl;
	cout << &bb << endl;
	Func(aa);
	return 0;
}

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

//取地址符&的操作符重载函数
	A* operator&()
	{
		//此时返回nullptr即为000000000地址
		return nullptr;
	}
	//const取地址操作符重载函数
	const A* operator&() const
	{
		//此时返回nullptr即为000000000地址
		return nullptr;
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值