【C++】类和对象完结篇——日期类实现

再谈构造函数

构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;//只能是说是赋初始值,不能说初始化
		_month = month;
		_day = day;
		_year = 100;//再次赋值
	}
private:
	int _year;
	int _month;
	int _day;
};

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量
的初始化
,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。于是便有了接下来的初始化列表。

初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式。

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
		//,_day(100)err,已经初始化
	{}
private:
	int _year;
	int _month;
	int _day;
};

【注意】
1: 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

2: 类中包含以下成员,必须放在初始化列表位置进行初始化:

  • 引用成员变量(引用在定义时必须初始化),
  • const成员变量(const变量具有常性,无法修改,必须在定义时初始化),
class Date
{
public:

//	Date(int year, int month, int day)//err,无法初始化引用和const对象,必须使用初始化列表
//	{
//		_year = year;
//		_month = month;
//		_day = day;
//		_year = 100;
//		_rday = _day;
//		_x = _day;
//	}


	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
		,_rday(_day)
		,_x(_day)
		
	{}
private:
	int _year;
	int _month;
	int _day;
	int& _rday;
	const int _x;
};
  • 自定义类型成员(且该类没有默认构造函数时)
    • 默认构造函数是不需要传参的,如无参的或者全缺省的构造函数才叫做默认构造函数
typedef int DataType;
class Stack
{
public:
	//Stack(size_t capacity = 10)//默认构造函数不需要传参
	Stack(size_t capacity)//不提供默认构造函数
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		_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 My_Queue
{
public:
	My_Queue(int n)//必须显示去写构造函数
		:_pushst(n)
		,_pophst(n)
	{}

	//My_Queue(int n)//err,不允许这样,只能走初始化列表
	//{
	//	_pushst(n);
	//	_pophst(n);
	//}
private:
	Stack _pushst;
	Stack _pophst;
};

int main()
{
	My_Queue q(10);
	return 0;
}

如上面的代码,有两个类,一个Stack类和My_Queue类,Stack类作为My_Queue类的成员,而Stack类是没有默认构造函数的,只有一个需要传参的构造函数,所以Stack的对象需要传参才能进行构造,这也就导致无法构造My_Queue的对象。
解决方法:
必须手写一个My_Queue的构造函数,并且将Stack类需要的参数由My_Queue的对象传给Stack的构造函数。My_Queue类的构造函数必须写成初始化列表的方式

3:所以建议以后写构造函数时尽量写成初始化列表,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

4: 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
看看下面_a1,_a2是多少

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

先声明_a2,再声明_a1,所以初始的顺序为_a2,再到_a1。所以_a1是1,_a2为随机值。

explicit关键字

函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值
的构造函数,还具有类型转换的作用

以除第一个参数无默认值其余均有默认值的构造函数为例。

class Date
{
public:

	Date(int year, int month = 4, int day = 21)
	: _year(year)
	, _month(month)
	, _day(day)
	{}
	
	void Print() 
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}

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

void Test()
{
	Date d1 = 2024;
	d1.Print();

}

int main()
{
	Test();
	return 0;
}

在早期的编译器中,当编译器遇到Date d1 = 2024这句代码时,会先构造一个临时对象,再用临时对象拷贝构造d1;但是现在的编译器已经做了优化,当遇到Date d1 = 2024这句代码时,会按照Date d1(2024)这句代码处理,这就叫做隐式类型转换。

在语法上,代码中Date d1 = 2024等价于以下两句代码:

Date tmp(2024); //先构造
Date d1(tmp); //再拷贝构造

实际上,类型转化也是隐式类型转化

int a = 1;
double b = a; //隐式类型转换

在这个过程中,编译器会先构建一个double类型的临时变量接收a的值,然后再将该临时变量的值赋值给b。这就是为什么函数可以返回局部变量的值,因为当函数被销毁后,虽然作为返回值的变量也被销毁了,但是隐式类型转换过程中所产生的临时变量并没有被销毁,所以该值仍然存在。

但是,对于单参数的自定义类型来说,Date d1 = 2021这种代码的可读性不是很好,我们若是想禁止单参数构造函数的隐式转换,可以用关键字explicit来修饰构造函数

static成员

概念

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用
static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化

特性

1.静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。

class Test
{
private:
	static int _n;
};

int Test::_n = 0;

int main()
{
	Test t;
	cout << sizeof(t) << endl;
	return 0;
}

我们可以计算一下Test对象的大小来验证一下 静态成员存放在静态区。

2.静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。

注意:这里静态成员变量_n虽然是私有,但是我们在类外突破类域直接对其进行了访问。这是一个特例,不受访问限定符的限制,否则就没办法对静态成员变量进行定义和初始化了。

3.类静态成员即可用 类名::静态成员或者对象静态成员来访问。

class Test
{
public:
	static int GetN()
	{
		return _n;
	}

private:
	static int _n;
};

int Test:: _n = 1;

int main()
{
	Test t;
	cout << sizeof(t) << endl;
    t.GetN(); //通过对象调用成员函数进行访问
	Test().GetN();//通过匿名对象调用成员函数进行访问
	Test::GetN();//通过类名调用静态成员函数进行访问
	
	return 0;
}

当成员属性为私有时,提供一个获取_n的函数GetN来获取,若是访问属性为公共时,则无需提供获取函数,直接按上面的方法实现就可以。

4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员。

  • 但是非静态成员函数是可以访问静态成员函数的

5.静态成员也是类的成员,受public、protected、private 访问限定符的限制。

友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以
友元不宜多用。

友元分为:友元函数和友元类

友元函数

我们都知道C++的<<,>>可以自动识别类型,不再像C语言那样需要指明类型,看起来很智能很神奇。其实这里是重载了运算符<<,(>>同理)所以我们只需要去查看一下<<相关文档就可以揭开他的庐山真面目。流提取<<
<<文档
之所以能够自动识别类型,是因为库里面已经写好了运算符<<对内置类型的重载,所以遇到对应的内置类型时就会调用对应的opeator<<(),那么我们能不能直接用cout<<输出一个自定义类型的对象呢?当然可以

ostream& operator<<(ostream& out)
{
	out << _year << '/' << _month << '/' << _day << endl;
	return out;
}
  • 这里的out是cout的别名,在这个函数里随便取(理想情况下out为左操作数的别名:cout<<d1)。
  • 这里返回的out是为了能连续输出。
	Date d1;
	//cout << d1;err
	d1 << cout;

自定义输出
我们确实做到了自定义类型对象的直接输出,但好像形式与我们平时写的有点不一样

  • 由于我们在类内实现的该函数,该函数的第一个参数被this指针抢占了,ostream类型的对象只能作为第二个参数,所以运算符<<的左侧必须是this指针类型的(也就是日期类(Date*this)),ostream的对象就只能在<<的右侧。也就是d1<<cout的形式了。

那么有什么办法解决这个问题吗?

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

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

class Date
{
	// 友元函数的声明
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	Date(int year = 2024, int month = 4, int day = 22)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
// <<运算符重载
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day<< endl;
	return out;
}
// >>运算符重载
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

写成全局函数就没有this指针了, operator>>的第一个参数就可以是ostream的对象了。
总结:

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

友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

  • 友元关系是单向的,不具有交换性。
    比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递
    如果C是B的友元, B是A的友元,则不能说明C时A的友元。
  • 友元关系不能继承。
class A
{
	// 声明B是A的友元类
	friend class B;
public:
	A(int n = 0)
		:_n(n)
	{}
	void Print()
	{
		cout << "test" << endl;
	}
private:
	int _n;
};

class B
{
public:
	void Test(A& a)
	{
		// B类可以直接访问A类中的私有成员变量
		cout << a._n << endl;
		a.Print();
	}
};

int main()
{
	B b;
	A a;
	b.Test(a);
	return 0;
}

内部类

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

class A
{
private:
	static int k;
	int h;
public:
	class B // B天生就是A的友元
	{
	public:
		void test(const A& a)
		{
			cout << k << endl;//直接访问,无需指定类名
			cout << a.h << endl;//可以访问私有
		}
	};
};

int A::k = 1;

int main()
{
	A::B b;
	b.test(A());
	A a;
	cout << sizeof(a) << endl;//a对象的大小不包括b

	return 0;
}

注意内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访
问外部类中的所有成员。但是外部类不是内部类的友元。

特性

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。

匿名对象

匿名对象即创建没有具体名字的对象,他的生命周期只在匿名对象所在行数

class A
{
public:
	A(int a=0,int b=0)
		:_a(a)
		,_b(b)
	{
		cout << "A(int a=0,int b=0)" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}
	void Print() const
	{
		cout << _a << " " << _b << endl;
	}


private:
	int _a;
	int _b;
};

int main()
{
	A();
	int a = int();
	cout << a << endl;

	return 0;
}

如上面的A(), 我们可以这么定义匿名对象,匿名对象的特点不用取名字,但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数。

当然,也有延长匿名对象生命周期的办法,那就是引用。

	const A& ra = A();
	ra.Print();

ra是A()匿名对象的别名,将匿名对象的生命周期变为引用ra的生命周期 。可以看到直到程序运行结束才会去调用析构函数。

我们看到匿名对象对内置类型也是支持匿名对象的,而且会把a初始化为0,所以我们就可以这样写默认构造函数。

class A
{
public:
	A(int a= int(),int b= int())
		:_a(a)
		,_b(b)
	{
		//cout << "A(int a= int(),int b= int()" << endl;
	}

	~A()
	{
		//cout << "~A()" << endl;
	}
	void Print() const
	{
		cout << _a << " " << _b << endl;
	}

private:
	int _a;
	int _b;
};

int main()
{
	int a = int();
	cout << a << endl;
	A b;
	b.Print();

	return 0;
}

在默认构造函数里,a,b的初始值我们直接用匿名对象来进行初始化。现在看着有点鸡肋,但在容器中会频繁使用。

再次理解类和对象

现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现
实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创
建对象后计算机才可以认识
。比如想要让计算机认识洗衣机,就需要:

  1. 用户先要对现实中洗衣机实体进行抽象==—即在人为思想层面对洗衣机进行认识,洗衣机有什
    么属性,有那些功能,即对洗衣机进行抽象认知的一个过程。
  2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清
    楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、
    Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
  3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣
    机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才
    能洗衣机是什么东西。
  4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
    类和对象再探索

日期类的实现

进行了三轮的类和对象的学习,来敲个日期类来巩固一下吧。该日期类的实现我们采用声明定义分离的方式

获取月份天数

对于日期类而言,获取月份天数是很重要的,类中其他函数的实现也需要用到该函数,所以先实现一个获取月份天数的函数

int Date::GetMonthsOfDay(int year, int month)const
{
	//assert(year > 0 && month < 13);不能断言,跨年份时会报错
	int days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	if ((month == 2) && (year % 100 != 0 && year % 4 == 0) || year % 400 == 0)//闰年
	{
		return days[month] + 1;
	}
	return days[month];
}

判断月有多少天只需知道两点,什么月份(可知有多少天),哪一年(判断是否是闰年,是即二月为29天,否则为28天)。

  • 采用一个数组存放天数(最好第一个位置放0,数组下标从0开始),再通过月份获取天数。
  • 闰年判断:四年一闰并且百年不闰:四百年一闰。

构造函数

我们最好写一个默认构造函数(传不传参都可以)。对于日期类而言自己实现拷贝构造是没有意义的,因为浅拷贝就可以,但还是可以写来熟练一下语法。

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

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

默认构造函数需要借助GetMonthsOfDay来判断一下日期是否合法。

运算符重载——日期比较

对于日期类,日期比较是必不可少的。而且d1==d2这样表述也更为直观,但自定义类型是不能直接使用运算符的,所以会涉及大量运算符重载。

  • 该日期类是在类里面实现的,多以第一个参数为this指针(也是日期类),第二个参数采用引用接受要比较的日期类就好了,减少拷贝。
  • 以下复用指调用其他功能相似或相反的函数来实现该函数。
  • 以下的运算符重载只需要比较即可,所以加上const来修饰*this更为合适。
  • 返回类型为bool

==

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

年月日逐个比较判断是否相等即可

!=

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

!=是不是== 的反面啊,直接复用==不就好了,多香啊。

  • 如d1!=d2,*this就是d1,d就是d2.

>

bool Date:: operator>(const Date& d)const
{
	if (_year > d._year)
	{
		return true;
	}
	if (_year == d._year && _month > d._month)
	{
		return true;
	}
	if (_year == d._year && _month == d._month && _day > d._day)
	{
		return true;
	}
	return false;
}
  • 大于:我们只需要判断是否大于就好了,否则为假(false)

>=

bool Date:: operator>=(const Date& d)const
{
	return *this > d || *this == d;
}
  • 还是复用,前面已经实现>和==了。

<

bool Date:: operator<(const Date& d)const
{
	return !(*this >= d);
}
  • <则为>=取反。

<=

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

总结:
逻辑较为简单,条件可能性比较少,所以复用在这里是很合适的。

运算符重载——日期计算

  • 在类内实现,左操作数必然为this,所以参数只需要传要加减的天数即可。
  • 为支持连续使用重载的运算符,返回类型为该类。

+

Date Date:: operator+(int day)
{
	if (day < 0)
	{
		day = -day;
		*this -= day;//复用-=
	}
	else
	{
		Date tmp(*this);
		tmp._day += day;
		while (tmp._day > GetMonthsOfDay(tmp._year, tmp._month))
		{
			tmp._day -= GetMonthsOfDay(tmp._year, tmp._month);
			++tmp._month;
			if (tmp._month > 12)
			{
				tmp._year += 1;
				tmp._month = 1;
			}
		}
		return tmp;
	}

}
  • + 是不改变自身的,所以需要用this对象构造一个临时对象tmp,通过tmp去进行+操作,最后返回tmp(传值返回,tmp为临时对象,不能传引用)。
  • 主体逻辑为:先让tmp的——day+=day,然后通过循环检查天数是否符合本月天数进行调整
    • 天数超了加月份,月份过十二了归1加年数
  • if条件是由于+=是复用+的,当+=一个负数使等于-=负数的相反数。

+=

Date& Date:: operator+=(int day)
{
	*this=*this+ day;
	return *this;
}
  • +=是直接改变自身,不需要临时变量,所以可以传引用返回。
  • +=是直接复用+。

-

Date Date:: operator-(int day)
{
	if (day < 0)
	{
		day = -day;
		*this += day;
	}
	else
	{
		Date tmp(*this);
		tmp._day -= day;
		while (tmp._day <= 0)
		{
			//err,向前借,先调整月
			//tmp._day += GetMonthsOfDay(tmp._year,tmp. _month);
			//--tmp._month;
			//if (tmp._month == 0)
			//{
			//	--tmp._year;
			//	tmp._month = 12;
			//}

			--tmp._month;
			if (tmp._month == 0)
			{
				--tmp._year;
				tmp._month = 12;
			}
			tmp._day += GetMonthsOfDay(tmp._year, tmp._month);
		}
		return tmp;
	}

}
  • 整体逻辑和+差不多,不过要先调整月份再调天数
  • if条件的原因也是和==+==的差不多。

-=

Date& Date:: operator-=(int day)
{
	*this = *this - day;
	return *this;
}
  • 复用-。

运算符重载——前后++,–

前置++

Date& Date:: operator++()
{
	*this += 1;
	return *this;
}
  • 前置++,返回++后的值,也就是直接改变this指向的对象即可。
  • 前面已经重载了+(int)了,所以直接this+1即可。直接引用返回。

后置++

Date Date:: operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}
  • 返回++前的值,所以需要临时变量tmp,所以只能传值返回。

前置–

Date& Date:: operator--()
{
	*this -= 1;
	return *this;
}
  • 与前置++同理。

后置–

Date Date:: operator--(int)
{
	Date tmp(*this);
	*this -= 1;
	return tmp;
}
  • 与后置++同理

重载运算符——日期相减

int Date::operator-(Date& d)
{
    //假设
	Date max = *this;
	Date min = d;
	int flag = 1;
	//假设错误,调整
	if (min > max)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int count = 0;//计数
	while (max != min)
	{
		min++;
		count++;
	}
	return count * flag;
}
  • 思路:采用计数的办法,让小的日期++直到与大的日期相等。
  • flag用来最后检测两日期相减为正数还是负数

运算符重载——流输入输出

输出

//全局实现
ostream& operator<<(ostream& out, const Date& d)
{
	out <<d._year << '/' <<d._month << '/' <<d._day << endl;
	return out;
}
  • 为了和库里的类似,使用友元函数,在全局实现。

输入

istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}
  • 流输出原理在友元函数已进行介绍,可翻阅查看。
  • 记得在日期类里面友元声明
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);

篇幅有点长,错误难免,希望佬们指出。

  • 38
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值