C++类和对象(下)

目录

运算符重载

日期类+=运算符重载

​编辑

日期类+运算符重载

赋值运算符“=”重载

赋值运算符重载和拷贝构造异同

 日期类-=天数和日期类-天数

​编辑 前置++和后置++运算符重载

>日期类大于号运算符重载

 “==”日期类等于号运算符重载

日期类!=号运算符重载 

 >日期类小于号运算符重载

日期类>=号运算符重载

日期类<=号运算符重载

日期-日期运算符重载

const成员

取地址&操作符重载

初始化列表

static成员 

友元

友元函数

用友元实现日期类<<和>>输入输出运算符重载

友元类

内部类 

​构造的优化


运算符重载

内置类型可以是可以直接用运算符进行计算的,比如int a=10;int b=10;可以直接cout<<a+b;但是自定义类型不能直接用加号减号赋值以及比较运算符大于小于等于号进行运算

比如说日期类Date,日期加天数,如果不经过处理是不能直接Date+day的,只能写一个函数输出返回值

函数打印日期+=天数

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	int getmonth(int year, int month, int day)//当月最大天数
	{
		int months[] = { 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 months[month] + 1;//判断闰年,闰年2月份是29天,所以加1
		}
		else
		{
			return months[month];
		}
	}

	Date Dateadd(int day)//日期加等于天数
	{
		_day += day;
		while(_day > getmonth(_year, _month, _day))
		{
			if (_month == 12)
			{
				_day -= getmonth(_year, _month, _day);
				_year++;
				_month=1;
			}
			else
			{
				_day -= getmonth(_year, _month, _day);
				_month++;
			}
		}
		return *this;
	}

	void Printf()//打印函数,因为不能直接访问私有的变量,只能通过成员函数来访问
	{
		cout << "年:" << _year <<" " << "月:" << _month <<" " << "日:" << _day << endl;
	}

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

int main()
{
	Date d1(2024, 4, 22);
	Date d2=d1.Dateadd(10000);//拷贝构造d2
	d2.Printf();//调打印函数
}

 

 Date Dateadd(int day)//日期加等于天数
    {
        _day += day;
        while(_day > getmonth(_year, _month, _day))
        {
            if (_month == 12)
            {
                _day -= getmonth(_year, _month, _day);
                _year++;
                _month=1;
            }
            else
            {
                _day -= getmonth(_year, _month, _day);
                _month++;
            }
        }
        return *this;
    }

这个函数具体实现了日期加天数,我采用的是先用天数把要加的天数加起来也就是  _day += day;但是如果day这个要加的天数很大的话,肯定会超过一个月最多的天数,比如1月最多有31天,可day是100的话,_day+=day最少也是100。所以我写了循环

while(_day > getmonth(_year, _month, _day))把天数限制成合理的天数,getmonnth获取的是当前月份最多的天数。只要_day还大于getmonth那么就都会进入循环调整,因为它小于等于getmonth才是合格的日数

 else
            {
                _day -= getmonth(_year, _month, _day);
                _month++;
            }

这个else是除12月份的调整天数的做法,_month++和天数绝对不能反过来(此时减去的是上个月最多的天数),如果反过来的话减去的是当月的天数。如果我答案是5月13号的话,相当于已经提前把5月的31天已经全都减去了,但是我13号后面的天数还没到是不能减的,5月份才过13天而已,但是却全减去了,就会出错。

if (_month == 12)
            {
                _day -= getmonth(_year, _month, _day);
                _year++;
                _month=1;
            }

_month==12是要单独处理的(这个_month判断是用当月的月份去判断),因为一年的12月份就已经到末尾了,如果此时还是_day > getmonth(_year, _month, _day),那说明这个天数一加已经跨年了,所以_year要+1代表去算下一年的情况,年份都已经改变了,所以月份等于1,从1月开始重新重新算

但是如果用函数来写日期+=day,那么每次都得调函数,比如Date d2=d1.Dateadd(10000);

为了使自定义类型也可以自由地用内置类型以及代码的可读性的运算符进行计算,所以运算符重载就诞生了。但是运算符重载也是函数,实现基本和函数一致,也具有返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通函数类似,只是给了一个直接使用内置类型运算符的机会

运算符重载函数名字为关键字operator操作符(参数列表)

注意:
不能通过连接其他符号来创建新的操作符:比如 operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐
藏的 this
.*  ::  sizeof  ?:(三目运算符)   . 注意以上 5 个运算符不能重载。这个经常在笔试选择题中出
现。

日期类+=运算符重载

	Date& operator+=(int day)
	{
		_day += day;
		while (_day > getmonth(_year, _month))
		{
			if (_month == 12)
			{
				_day -= getmonth(_year, _month);
				_year++;
				_month = 1;
			}
			else
			{

				_day -= getmonth(_year, _month);
				_month++;
			}
		}
		return *this;
	}// 日期+=天数
也可以把声明写在类里面,定义写在类外面
Date& operator+=(int day);//写在类里面的声明
     /*...................
     ...................
         此处省略
     ...................*/
Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > getmonth(_year, _month,_day))
	{
		if (_month == 12)
		{
			_day -= getmonth(_year, _month,_day);
			_year++;
			_month = 1;
		}
		else
		{

			_day -= getmonth(_year, _month,_day);
			_month++;
		}
	}
	return *this;
}// 日期+=天数

其实可以看出来基本和函数的实现差不多,只是函数形式不一样,不过这样就可以直接用+=号

日期类+运算符重载

+号与+=号的区别在与+加完了是不会去改变加数日期,而+=相当于d1=d1+天数,所以是会改变d1的大小的

+=运算符重载打印一下d1看看,其实会发现+=之后d1的值改变了

+是d1是不会去改变的,但是我得到的却是在d1的基础上加上天数的结果,所以使用一个tamp临时日期来进行加天数,最后把tamp返回就行了。而tamp是由d1拷贝构造而来,tamp的值和d1的值相等。这样就做到了得到d1加天数之后的值但是d1本身不改变

Date Date::operator+(int day)
{
	Date tamp = *this;//this指针解引用得到的是d1
	tamp += day;//重载+=号
	return tamp;//返回加天数之后的值
}// 日期+天数

 上图可以看出只是d2改变了,因为d2=d1+天数,但是d1没改变。tamp+=day,+=这个运算符重载因为我们之前已经提前做了,所以可以直接拿来用。

也可以先实现+号运算符重载,再实现+=号运算符重载,以下是代码示例

Date Date::operator+(int day)
{
	Date tamp = *this;//this指针解引用得到的是d1
	tamp._day += day;
	while (tamp._day > getmonth(tamp._year,tamp._month,tamp. _day))
	{
		if (tamp._month == 12)
		{
			tamp._day -= getmonth(tamp._year, tamp._month, tamp._day);
			tamp._year++;
			tamp._month = 1;
		}
		else
		{

			tamp._day -= getmonth(tamp._year, tamp._month, tamp._day);
			tamp._month++;
		}
	}
	return tamp;//返回加天数之后的值
}// 日期+天数

Date& Date::operator+=(int day)
{
	*this = *this + day;//+运算符重载已经提前做了,所以可以直接用
	return *this;
}// 日期+=天数

完整代码

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	int getmonth(int year, int month, int day)//当月最大天数
	{
		int months[] = { 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 months[month] + 1;//判断闰年
		}
		else
		{
			return months[month];
		}
	}

	
	Date operator+(int day);
	Date& operator+=(int day);
	void Printf()//打印函数,因为不能直接访问私有的变量,只能通过成员函数来访问
	{
		cout << "年:" << _year <<" " << "月:" << _month <<" " << "日:" << _day << endl;
	}

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




Date Date::operator+(int day)
{
	Date tamp = *this;//this指针解引用得到的是d1
	tamp._day += day;
	while (tamp._day > getmonth(tamp._year,tamp._month,tamp. _day))
	{
		if (tamp._month == 12)
		{
			tamp._day -= getmonth(tamp._year, tamp._month, tamp._day);
			tamp._year++;
			tamp._month = 1;
		}
		else
		{

			tamp._day -= getmonth(tamp._year, tamp._month, tamp._day);
			tamp._month++;
		}
	}
	return tamp;//返回加天数之后的值
}// 日期+天数

Date& Date::operator+=(int day)
{
	*this = *this + day;//+运算符重载已经提前做了,所以可以直接用
	return *this;
}// 日期+=天数

int main()
{
	Date d1(2024, 4, 22);
	cout << "d1+天数之前" << endl;
	d1.Printf();
	Date d2=d1+50;//拷贝构造d2
	cout << "d1+天数之后" << endl;
	d1.Printf();//调打印函数
	cout << "d2=d1+天数的值" << endl;
	cout << " " << endl;

	Date d3(2024, 4, 22);
	cout << "d3+=天数之前" << endl;
	d1.Printf();
	Date d4 = d3 += 50;//拷贝构造d2
	cout << "d3+=天数之后" << endl;
	d1.Printf();//调打印函数
	cout << "d4=d3+=天数的值" << endl;

	d2.Printf();
}

 但是选择先写+=运算符重载再写+要好一点,这样会少一重拷贝构造

把拷贝构造显式地写出来,把拷贝构造和构造函数加上打印信息来看看调用了几次

首先是用+运算符重载来实现+=运算符重载

 打印结果

完整的测试代码

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		cout << "构造函数" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	
	Date(const Date& d)
	{
		cout << "拷贝构造" << endl;
		_year = d._year;
		_day = d._day;
		_month = d._month;
	}

	int getmonth(int year, int month, int day)//当月最大天数
	{
		int months[] = { 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 months[month] + 1;//判断闰年
		}
		else
		{
			return months[month];
		}
	}

	
	Date operator+(int day);
	Date& operator+=(int day);
	void Printf()//打印函数,因为不能直接访问私有的变量,只能通过成员函数来访问
	{
		cout << "年:" << _year <<" " << "月:" << _month <<" " << "日:" << _day << endl;
	}

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



Date Date::operator+(int day)
{
	Date tamp = *this;//this指针解引用得到的是d1
	tamp._day += day;
	while (tamp._day > getmonth(tamp._year, tamp._month, tamp._day))
	{
		if (tamp._month == 12)
		{
			tamp._day -= getmonth(tamp._year, tamp._month, tamp._day);
			tamp._year++;
			tamp._month = 1;
		}
		else
		{

			tamp._day -= getmonth(tamp._year, tamp._month, tamp._day);
			tamp._month++;
		}
	}
	return tamp;//返回加天数之后的值
}// 日期+天数

Date& Date::operator+=(int day)
{
	*this = *this + day;//+运算符重载已经提前做了,所以可以直接用
	return *this;
}// 日期+=天数



int main()
{
	Date d3(2024, 4, 22);
	cout << "调用+=天数之前" << endl;
	Date d4 = d3 += 50;//拷贝构造d2
	cout << "调用+=天数之后" << endl;
}

 然后是先实现+=运算符重载,再实现+号运算符重载的情况

打印结果,只有一层拷贝构造

完整测试代码

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		cout << "构造函数" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	
	Date(const Date& d)
	{
		cout << "拷贝构造" << endl;
		_year = d._year;
		_day = d._day;
		_month = d._month;
	}

	int getmonth(int year, int month, int day)//当月最大天数
	{
		int months[] = { 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 months[month] + 1;//判断闰年
		}
		else
		{
			return months[month];
		}
	}

	
	
	Date& operator+=(int day);
	Date operator+(int day);
	void Printf()//打印函数,因为不能直接访问私有的变量,只能通过成员函数来访问
	{
		cout << "年:" << _year <<" " << "月:" << _month <<" " << "日:" << _day << endl;
	}

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



Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > getmonth(_year, _month, _day))
	{
		if (_month == 12)
		{
			_day -= getmonth(_year, _month, _day);
			_year++;
			_month = 1;
		}
		else
		{

			_day -= getmonth(_year, _month, _day);
			_month++;
		}
	}
	return *this;
}// 日期+=天数

Date Date::operator+(int day)
{
	Date tamp = *this;//this指针解引用得到的是d1
	tamp += day;//重载+=号
	return tamp;//返回加天数之后的值
}// 日期+天数


int main()
{
	Date d3(2024, 4, 22);
	cout << "调用+天数之前" << endl;
	Date d4 = d3 += 50;//拷贝构造d2
	cout << "调用+天数之后" << endl;
}

赋值运算符“=”重载

赋值运算符重载格式
参数类型 const T& ,传递引用可以提高传参效率
返回值类型 T& ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回 *this :要复合连续赋值的含义
同样以日期类举例
Date& Date::operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	return *this;
}



int main()
{
	Date d3(2024, 4, 22);
	Date d2(2024,5,31);
	d2 = d3;//赋值运算符重载
	d2.Printf();
	d3.Printf();
}

传参一定要用引用,不然会一直拷贝构造死循环了

+=运算符重载实现可以写一个全局的函数,但是赋值运算符重载不能写全局函数,因为如果你不在把它写出类的成员函数而是全局,那么编译器会自动生成一个默认的赋值运算符重载函数,这两个赋值运算符重载就会产生冲突

 把赋值运算符重载函数删除的情况,依旧会赋值成功,代码和运行结果如下

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		cout << "构造函数" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	
	Date(const Date& d)
	{
		cout << "拷贝构造" << endl;
		_year = d._year;
		_day = d._day;
		_month = d._month;
	}

	int getmonth(int year, int month, int day)//当月最大天数
	{
		int months[] = { 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 months[month] + 1;//判断闰年
		}
		else
		{
			return months[month];
		}
	}

	void Printf()//打印函数,因为不能直接访问私有的变量,只能通过成员函数来访问
	{
		cout << "年:" << _year <<" " << "月:" << _month <<" " << "日:" << _day << endl;
	}

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

int main()
{
	Date d3(2024, 4, 22);
	Date d2(2024,5,31);
	d2 = d3;//赋值运算符重载
	d2.Printf();
	d3.Printf();
}

赋值运算符重载和拷贝构造异同

赋值运算符重载和拷贝构造最大的区别在于构造,虽然两者都是复制,但是赋值的前提是两个对象都已经实例化出来了才能赋值,拷贝构造是只有一个已经实例化出来了,用这个现成的对象的值去构造另一个还没有构造出来的对象,如下图所示d3和d2都已经提前实例化出来了,所以等号是赋值。而对于Date d4=d3来说d4还没有实例化构造出来但是d3已经构造出来了,所以这是拷贝构造

为什么拷贝构造也能使用等号呢,

可以将Date d4 = d2;这种语法理解为以下步骤:

  1. 首先,编译器会创建一个临时对象,这个临时对象是通过调用拷贝构造函数并将d2作为参数来创建的。
  2. 然后,将这个临时对象的值拷贝给对象d4,这实际上是通过调用拷贝构造函数来完成的。
  3. 最后,这个临时对象会被销毁,而d4将持有与临时对象相同的值。

也就是首先会拷贝构造出一个临时的Date对象,然后再通过拷贝构造函数将这个临时对象的值拷贝给d4。然而,编译器可能会进行优化,将这个过程合并为一次拷贝构造,而不会创建临时对象。

对于赋值运算符重载和拷贝构造来说,就算你不写编译器也会生成默认的拷贝构造(你自己写了就不会生成了),这两个默认的都是浅拷贝或者浅赋值,也就是原封不动地把值复制过去。如果有指针开空间的话,会形成两个指针指向同一块空间的情况。对于拷贝构造来说会形成两块空间析构了两次的情况。对于赋值来说,虽然两个对象是不同时刻构造的,如果有指针的话会指向不同的地址空间,但是因为赋值会使两个指针指向同一块空间,这样造成的一个问题是变化的那个指针原先指向空间会丢失,造成内存泄漏,另一方面这两个指针指向同一块地方,那么会造成和拷贝构造一样的情况,同一块空间释放了两次。这种浅复制的解决办法都是自己手动写拷贝构造或者赋值运算符重载,把它变成深拷贝。

 日期类-=天数和日期类-天数

这两个和+=与+的关系差不多,直接放一块讲了

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

			_month = 12;
			_year -= 1;
			_day += getmonth(_year,_month);
		}
		else
		{

			_month--;
			_day += getmonth(_year, _month);
		}
	}
	return*this;
}
Date Date::operator-(int day)
{
	Date tamp = *this;
	tamp -= day;
	return tamp;
}//日期-天数

首先减去指定的天数_day -= day;。在减去指定的天数后调整日期到合适的日期,代码中的 while 循环用于处理可能出现的情况:如果减去天数后的天数小于 0,表示日期需要向前倒退到上一个月。在这种情况下,代码会根据当前月份是否为 1 来判断是否需要倒退到上一年的 12 月。

如果当前月份不是 1,表示可以直接向前倒退一个月,即月份减 1,同时根据上一个月的天数来调整当前天数。如果当前月份是 1,表示需要向前倒退到上一年的 12 月,这时会将月份调整为 12,年份减 1,并根据上一个月的天数来调整当前天数。

对于日期减天数来说,首先要保证减天数的那个日期自身不能改变,同时要得到改变后的日期,因此拷贝构造了临时变量tamp,由它作为改变后的日期返回,这样就做到了得到改变日期的同时不改变原先的日期

 前置++和后置++运算符重载

前置++和后置++本质的区别在于前置++是先++然后再使用,使用的是++后的值,而后置++是先使用然后++,使用的是++前的值,使用完后再++;

对于前置++来说,其实就是日期+=1而已;而日期+=天数的重载之前已经做出来了 ,所以可以直接用日期+=1;最后把日期+=1后的日期直接返回就行了。

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

对于后置++你会发现后置++和前置++好像运算符都是++啊,难不成要这样写++operator吗。++operator是错误的表示,后置++给这个运算符重载函数做了标记operator(int)以做区分,这个int没有任何实际含义,仅仅只是标记作用。值得注意的是除了int你不能使用任何其他类型作为标记,比如char double等都不行。

然后后置++具体实现要注意的是返回的++之前的值,返回之后使原值再++,所以这个也需要拷贝构造一下临时变量tamp,然后再使原日期+=1就可以了,这个时候也可以使用+=的运算符重载(之前日期+=天数已经实现了)

// 后置++
Date Date::operator++(int)
{
	Date tamp = *this;
	*this += 1;
	return tamp;
}

前置--与后置-- 运算符重载

这两个运算符重载和++的类似,就不多加赘述了,直接放代码

// 后置--
Date Date::operator--(int)
{
	Date tamp = *this;
	_day -= 1;
	return tamp;
}

// 前置--
Date& Date::operator--()
{
	_day -= 1;
	return *this;
}

>日期类大于号运算符重载

日期>号运算符重载函数实现是通过多重判断来实现的,年大于就大于,年相等月大于就大于,月相等日数大于就大于。

如果一个日期的年份不等于另一个日期的年份,那么就可以直接return 第一个日期年份>第二个日期年份,这个return其实也可以算一重判断,因为我返回的是bool值,这两个年份一定有一个大的情况

如果两个年份相等的情况就去判断月份,月份的情况和年份类似,如果不相等就直接return 第一个日期的月份>第二个日期的月份。日期的比较一般都是使用bool做为返回值。

如果月相等就去判断日的情况,直接return 第一个日期日数>第二个日期天数

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

 “==”日期类等于号运算符重载

日期类等于号的底层实现逻辑是年不相等就不会相等,年相等月不相等就不相等。年月相等日不相等也就不会相等,除此之外就是相等的情况

// ==运算符重载
bool Date::operator==(const Date& d)
{
	if (_year != d._year)
		return false;
	else
	{
		if (_month != d._month)
			return false;
		else
		{
			if (_day != d._day)
				return false;
			else
				return true;
		}
	}
}

日期类!=号运算符重载 

既然已经实现了==号的运算符重载,那么直接结果取非就可以了,或者取反

// !=运算符重载
bool Date::operator != (const Date& d)
{
	return !(*this == d);
}

 >日期类小于号运算符重载

通常的做法和大于号类似,年小于就小于,年相等月小于就小于,月相等日数小于就小于。这种做法就是把大于的情况改成小于号就行

另一种做法是对于小于来说,反面要么是大于要么是等于,所以如果有大于或者等于的情况出现的话,那么就直接return false就可以了。否则那就是return true了。因为已经做好了大于和等于的重载,所以直接用就可以了。

// <运算符重载
bool Date::operator < (const Date& d)
{
	if (*this > d || *this == d)
		return false;
	else
		return true;
}

日期类>=号运算符重载

 大于等于就是小于的反面,因为要么就是大于,要么等于,要么小于,所以求大于等于只用求小于的反面就可以了

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

日期类<=号运算符重载

小于等于和大于等于情况类似

// <=运算符重载
bool Date::operator <= (const Date& d)
{
	return !(*this > d);
}

日期-日期运算符重载

日期减日期得到的是具体天数,没有日期加日期(日期+日期是没有意义的)。日期-日期运算符重载可以将两个日期全部转换成从公元1年开始到那个日期的具体天数,然后两个日期直接相减就得到日期减日期的天数了,但是这种做法有点麻烦。

好的做法是先确定两个日期哪个日期是小的那个日期,然后一直循环让小的日期一直+=1,直到小的日期和大的日期相等,同时在这个循环中间加一个临时变量用来计数,这个临时变量的值就是两个日期之间的差值。

因为之前已经做过了小于的运算符重载,所以直接使用就可以了,拷贝构造两个临时变量,一个代表大于的日期,一个代表小于的日期。同时因为this指针默认是左操作数,所以有可能减出来是负数的情况(左操作数为小日期的时候),所以用flag作为标记,如果左操作数是小日期的话,最后结果要乘负1

int Date::operator-(const Date& d)
{
	int flag = 1;//标记
	int num = 0;//最后结果
	Date max = *this;
	Date min = d;
	if (*this < d)//找出更小的日期
	{
		min = *this;
		max = d;
		flag = -1;//左操作数为小日期
	}
	while (min != max)
	{
		++min;
		++num;
	}
	return flag * num;
}

完整日期类运算符重载代码

int Date::GetMonthDay(int year, int month)
{
	static int monthday[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	if (month == 2)
	{
		if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
			return 29;
		else
		{
			return monthday[month];
		}
	}
	else
	{
		return monthday[month];
	}
}//获取某年某月的天数

Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}//构造函数

Date::Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}//拷贝构造


// d2 = d3 -> d2.operator=(&d2, d3)
Date& Date::operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	return *this;
}//赋值运算符重载



Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		if (_month == 12)
		{
			_day -= GetMonthDay(_year, _month);
			_year++;
			_month = 1;
		 }
		else
		{
			
			_day -= GetMonthDay(_year, _month);
			_month++;
		}
	}
	return *this;
}// 日期+=天数

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

// 日期-天数
Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day <0)
	{
		if (_month == 1)
		{
			
			_month = 12;
			_year -= 1;
			_day += GetMonthDay(_year, _month);
		}
		else
		{
			
			_month--;
			_day += GetMonthDay(_year, _month);
		}
	}   
	return* this;
}

Date Date::operator-(int day)
{
	Date tamp = *this;
	tamp -= day;
	return tamp;
}//日期-天数

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

// 后置++
Date Date::operator++(int)
{
	Date tamp = *this;
	*this += 1;
	return tamp;
}

// 后置--
Date Date::operator--(int)
{
	Date tamp = *this;
	_day -= 1;
	return tamp;
}

// 前置--
Date& Date::operator--()
{
	_day -= 1;
	return *this;
}

// >运算符重载
bool Date::operator>(const Date& d)
{
	if (_year != d._year)
		return _year > d._year;
	else if (_month != d._month)
		return _month > d._month;
	else
		return _day > d._day;
}

// ==运算符重载
bool Date::operator==(const Date& d)
{
	if (_year != d._year)
		return false;
	else
	{
		if (_month != d._month)
			return false;
		else
		{
			if (_day != d._day)
				return false;
			else
				return true;
		}
	}
}



// <运算符重载
bool Date::operator < (const Date& d)
{
	if (*this > d || *this == d)
		return false;
	else
		return true;
}

// >=运算符重载
bool Date::operator >= (const Date& d)
{
	return !(*this < d);
}

// <=运算符重载
bool Date::operator <= (const Date& d)
{
	return !(*this > d);
}

// !=运算符重载
bool Date::operator != (const Date& d)
{
	return !(*this == d);
}

// 日期-日期 返回天数
int Date::operator-(const Date& d)
{
	int flag = 1;//标记
	int num = 0;//最后结果
	Date max = *this;
	Date min = d;
	if (*this < d)//找出更小的日期
	{
		min = *this;
		max = d;
		flag = -1;//左操作数为小日期
	}
	while (min != max)
	{
		++min;
		++num;
	}
	return flag * num;
}

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



}//析构函数

const成员

用const修饰的成员函数称为const成员函数,const修饰类成员函数,实际修饰该成员的this指针指向的内容,表明在该成员函数中不能对类的任何成员进行修改。因为正常来讲要使一个变量arr不能修改,那么直接在变量前面加const就可以了,但是this指针是隐藏的,所以C++做了改变,如果要用const修饰this指针,那么直接在函数括号后面加const就可以了

比如说日期类的构造函数,const成员是这么写的

但是这样会直接报错,因为const Date*this意味着 this指向的内容不能改变,本来构造函数指望着传过来的year,day等参数值来完成初始化构造的。但是一加上const就变成了,const this->_year=year;这样就会构造失败,所以构造函数和析构函数是绝对不能用const的

但是如果你不改变this指针指向的值(其实也就是这个对象的成员变量),就可以用const修饰成员函数,比如Printf()函数,它只是打印而没有改变成员变量的值

值得注意的是,如果你对象前面const话那么就不能再使用非const成员函数,就算他不会改变这个对象的任何东西,比如Printf()函数,它不改变任何东西,但是依旧不能调用

当你声明一个对象为const时,意味着这个对象的内容在程序运行期间是不允许被修改的。

d1.Printf()其实相当于d1.Printf(&d1),因为对面是隐藏this指针来接收这个地址以便于识别出不同的对象。所以问题就来了d1前面写了const,意味着它不能修改,所以this指针接收这个地址应该写成这样const Date *this=&d1,这个const修饰的是this的指向内容,也就是const Date d1。如果不写写成const成员函数的话,那么就是权限的扩大,在调用之前d1的权限只能读,而调用后(*this也就是d1,此时不加const在函数里他的权限是可以修改的)却变成了又能读又能修改,所以就会起冲突报错。

权限不能扩大,但是可以缩小,比如说我调函数的之前的既可以读又可以写,但是我在函数里面被const限制了只能读。

 对于const成员函数来说也不能调用非const成员函数,在const成员函数里*this限制不能修改,而调非const成员函数意味着它可以修改,这就成了权限的扩大。

反之,非const成员函数可以调用cosnt成员函数,这是权限的缩小

取地址&操作符重载

取地址&操作符重载有两种写法,一种是非const对象取地址,另一种是const对象取地址

这两个运算符重载和别的不同,它们有默认成员函数 ,编译器默认会生成。
class Date
{
public:
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print()
    {
        cout << "Print()" << endl;
        cout << "year:" << _year << endl;
        cout << "month:" << _month << endl;
        cout << "day:" << _day << endl << endl;
    }
    void Print() const
    {
        cout << "Print()const" << endl;
        cout << "year:" << _year << endl;
        cout << "month:" << _month << endl;
        cout << "day:" << _day << endl << endl;
    }
private:
    int _year; // 年
    int _month; // 月
    int _day; // 日
};
void Test()
{
    Date d1(2022, 1, 13);
    d1.Print();
    const Date d2(2022, 1, 13);
    d2.Print();
}
class Date
{
public:
    Date* operator&()
    {
        return this;
    }
    const Date* operator&()const
    {
        return this;
    }
private:
    int _year; // 年
    int _month; // 月
    int _day; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如 想让别人获取到指定的内容

初始化列表

初始化列表是另一种有别于有参构造赋值初始化给值的初始化方式

带成员初始化列表的构造函数一般形式如下

类名:构造函数名(参数):成员(值),成员(值)

{

    //构造函数体

}

1. 每个成员变量在初始化列表中只能出现一次 ( 初始化只能初始化一次 )
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const 成员变量
自定义类型成员 ( 且该类没有默认构造函数时 )
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
日期类初始化列表
Date(int year, int month, int day)
	: _year(year)
	, _month(month)
	, _day(day)
{}

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

初始化列表和有参构造函数赋值初始化最大的区别在于赋值可以在函数体里赋值很多次,但是初始化列表只能一次(除非在main函数里重写传参)。

在构造函数被调用之前,对象本身还不存在,因此编译器也不会为其成员变量分配内存空间。当构造函数被调用时,编译器会为对象本身分配足够的内存空间来存储其所有成员变量(包括const成员变量)。然后,根据构造函数的实现(包括初始化列表和构造函数体),成员变量被初始化。对于const成员变量,它们必须在构造函数的初始化列表中进行初始化,因为它们的值在对象的生命周期内不能被修改。如果尝试在构造函数体内部为const成员变量赋值,编译器会报错。

初始化可以看做本质上可以看做是每个对象定义+初始化的地方,给空间和给值是同步进行的。

也许有人会疑惑函数体里也可以只给赋值一次啊,为什么就不能写const成员,当对象的成员变量没有在构造函数的初始化列表中显式初始化时,它们会经历所谓的“默认初始化”或“隐式初始化”。这意味着编译器会自动为这些成员变量赋予一个初始值,这个初始值取决于成员变量的类型。也就是说如果你不显示写成员初始化列表也会进行初始化,内置类型给随机值,自定义类型调无参默认构造(没有的话会报错)。因为已经给过一次了,所以再进入到函数体里进行赋值就会出错(这时候相当于给了两次值)。

const放到函数体的情况,直接报错

前期queue类,如果用默认构造的话,必须Stack也有默认构造才能初始化成功,而Stack没有默认构造就会报错。而初始化列表就解决了这种情况

Stack有参构造的情况,直接报错

#include<iostream>
using namespace std;

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity)//Stack有参构造
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}

		/*注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
		时,则拷贝构造函数是一定要写的,否则就是浅拷贝。*/
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};

class queue
{
private:
	Stack s1;
	Stack s2;
};

int main()
{
	queue arr;
}

默认构造函数的情况

初始化列表的情况

也可以初始化成功

 

初始化列表也可以和构造函数体串着用,除了const和引用以及无默认构造的自定义类型外,剩下的都可以写在函数体里面,可以一半成员用初始化列表初始化,也可以另一半用赋值来初始化

 

另一个值得注意的是成员变量在类里面的声明顺序就是在初始化列表里初始化的顺序,与他们在初始化列表的顺序无关

#include<iostream>
using namespace std;
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}

	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
	A aa(1);
	aa.Print();
}

上面那个代码虽然是a1在初始化列表里先写的,但是它定义比a2晚,所以a2先初始化。而a2是用a1拷贝构造的,此时a1还没初始化所以是随机值,因此a2是随机值

C++11的标准里新加了可以给成员变量直接缺省值,为什么这个可以给缺省值,因为在类里面成员变量只是声明,它的缺省值是直接给初始化列表初始化用的,即使你不写初始化列表也会有默认的初始化列表接收,所以不会报错

如下图所示,没有报错

其实就等价于

static成员 

声明为static的类成员,用static修饰的成员变量,称之为静态成员变量

举个例子

为什么上面a2走初始化列表会出错呢,这是因为静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。因此静态成员必须在类外定义,定义时不加static关键字,类中只是声明。类的静态成员可用类名::静态成员或者对象.静态成员来访问

所以正确的做法是这样的

#include<iostream>
using namespace std;
class A
{
public:
	A(int a)
		:_a1(a)
	{}

	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a1;
	static int _a2;
};
int A::_a2 = 10;
int main() {
	A aa(1);
	aa.Print();
}

 因为静态区不在类里面,被所有对象所共有,所以计算sizeof时是不会计算进去的

对于静态成员函数来说,因为没有this指针,所以不能访问任何非静态成员函数,但是非静态成员函数可以访问静态成员函数。同时非静态成员可以去调用静态成员函数 这是因为静态成员函数属于类本身,而不是类的某个特定对象。因此,非静态成员函数(它们有this指针并属于类的某个特定对象)可以调用静态成员函数,因为这些函数是类级别的,不需要特定的对象实例。

简单来说,非静态成员函数可以访问静态成员函数是因为静态成员函数不需要对象实例来调用,它们只与类本身相关。而非静态成员函数是在特定对象上调用的,这个对象有this指针,可以访问类的所有成员(包括静态成员)。

设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )

C c;

int main()

{

A a;

B b;

static D d;

  return 0;

}

A.D B A C

B.B A D C

C.C D B A

D.A B D C

分析:1、类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象

   2、全局对象先于局部对象进行构造

   3、局部对象按照出现的顺序进行构造,无论是否为static

   4、所以构造的顺序为 c a b d

   5、析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构

   6、因此析构顺序为B A D C

友元

友元分为友元函数和友元类,友元提供了一种突破封装的方式,有时提供了便利,但是友元会增加耦合度,破坏了封装,所以一般很少用

友元函数

 由于类的成员变量大多是私有的,所以类外的函数是访问不到类的成员变量的,要访问成员变量只能通过公有的类的成员函数才能访问到

举个例子

Print是类外函数,无法访问到类里面私有的成员变量。解决办法可以把成员变量直接置为public,但是这种方法就完全破坏了类的封装性。因此把函数置为友元就可以访问,实际上就是给这个函数加了一个访问权限而已。友元函数的一般形式就是把函数声明放在 类里面,然后前面加个friend

如下面的例子,Print设为友元

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

用友元实现日期类<<和>>输入输出运算符重载

因为 cout 的输出流对 象和隐含的 this 指针在抢占第一个参数的位置 this 指针默认是第一个参数也就是左操作数了。但是实际使用 中cout 需要是第一个形参对象,才能正常使用。所以要将 operator<< 重载成全局函数。但又会导致类外没办 法访问成员,此时就需要友元来解决。operator>> 同理
class Date
{
public:
 Date(int year, int month, int day)
 : _year(year)
 , _month(month)
 , _day(day)
 {}
 // d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
 // 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
 ostream& operator<<(ostream& _cout)
 {
 _cout << _year << "-" << _month << "-" << _day << endl;
 return _cout;
 }
private:
 int _year;
 int _month;
 int _day;
};
友元函数 可以 直接访问 类的 私有 成员,它是 定义在类外部 普通函数 ,不属于任何类,但需要在类的内部声 明,声明时需要加friend 关键字。
class Date
{
 friend ostream& operator<<(ostream& _cout, const Date& d);
 friend istream& operator>>(istream& _cin, Date& d);
public:
 Date(int year = 1900, int month = 1, int day = 1)
 : _year(year)
 , _month(month)
 , _day(day)
 {}
private:
  int _year;
 int _month;
 int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
 _cout << d._year << "-" << d._month << "-" << d._day;
 return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
 _cin >> d._year;
 _cin >> d._month;
_cin >> d._day;
 return _cin;
}
int main()
{
 Date d;
 cin >> d;
 cout << d << endl;
 return 0;
}

友元类

 友元类就是把一个类A设定为另一个类B的友元,A可以访问B里面的所有成员变量,但是B不是A的友元,B不能访问A的东西(B把A当朋友,A不把B当朋友)

例子如下

#include<iostream>
using namespace std;
class B
{
	friend class A;//A是B的友元类
public:
	B() :bbr(5), bbt(13)
	{};
private:
	int bbr;
	int bbt;
};
class A
{
public:
	A(int a)
		:_a1(a)
	{}

	void Printf()
	{
		cout << bb.bbr << " " << bb.bbt << endl;//A可以直接访问B的私有成员
	}
private:
	int _a1;
	B bb;
};

int main() {
	A aa(1);
	aa.Printf();
}

内部类 

概念: 如果一个类定义在另一个类的内部,这个内部类就叫做内部类 。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意: 内部类就是外部类的友元类 ,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中
的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的 public protected private 都是可以的。
2. 注意内部类可以直接访问外部类中的 static 成员,不需要外部类的对象 / 类名。
3. sizeof( 外部类 )= 外部类,和内部类没有任何关系
using namespace std;
class A
{
private:
 static int k;
 int h;
public:
 class B // B天生就是A的友元
 {
 public:
 void foo(const A& a)
 {
 cout << k << endl;//OK
 cout << a.h << endl;//OK
 }
 };
};
int A::k = 1;
int main()
{
	A::B b;
	b.foo(A());
	return 0;
}

sizeof 内部类不会算进去,它只是加了一层访问权限而已

A里面还有一个静态int成员,这个也不会算进去,所以大小是4

构造的优化

先看一个例子

#include<iostream>
using namespace std;
class A
{
public:
	A(int a)
		:_a1(a)
	{
		cout << "构造函数" << endl;
	}
	A(const A& B)
	{
		_a1 = B._a1;
		cout << "拷贝构造" << endl;
	}
private:
	int _a1;
};

int main() {
	A aa(1);
	A bb(aa);//拷贝构造

	cout << "第二趟" << endl;
	A cc = 3;
}

为什么A cc = 3;只是走了一次构造函数呢,内置类型3是不能直接给自定义类型A赋值的,所以会先隐式类型转换为一个A类型的临时变量(可以在构造函数前面加上explicit关键字来禁止隐式类型转换),此时相当于A  tamp(3)也就是单参数构造。此时等式变为了A cc=tamp ,tamp是已经实例化出来的,这一步相当于拷贝构造。也就是说A cc=3其实是先构造了一个值为3的临时变量tamp,然后用tamp拷贝构造给cc,但是为什么就打印一次构造呢。这是因为编译器遇到连续的构造+拷贝构造会直接优化为直接构造,但是必须要写在一行,如果先构造出来了,然后取等于3,这样是不会优化的

为什么下图引用会出错呢,因为临时变量具有常性

所以需要加const 

 

  • 35
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值