C++入门:类和对象(中)

目录

前言:

一:类的6个默认成员函数

二:构造函数(第一个成员)

(1)概念

(2)特性

(3)特性的解析

⭐特性4

⭐特性5

⭐特性6

⭐特性7

三:初始化列表

(1)引入

(2)概念

(3)注意

四:析构函数(第二个成员)       

(1)概念       

(2)特性

(3)例子

五:拷贝构造函数(第三个成员)

(1)概念

(2)特性

(3)特性的解析

⭐特性2

⭐特性3

⭐特性5

 六:运算符重载

(1)概念

(2)实例

(3)注意

七:赋值运算符重载(第四个成员)

(1)赋值运算符重载格式

(2)特性

⭐特性1

⭐特性2

八:const修饰的成员

九:取地址及const取地址操作符重载(第五、六个成员)

十:友元

(1)友元函数

(2)友元类

十一:实现一个比较完整的日期类

(1)先讲几个比较重要的点

⭐获取月天数

⭐实现比较运算符重载的原则

⭐输入输出重载

⭐前置++(--)和后置++(--)的区分

(2)日期类实现(分文件)

⭐Date.h(函数声明)

⭐Date.cpp(函数实现)

⭐test.cpp(测试)


前言:

C++专栏内容连贯,没展开讲的重要内容都在专栏往期。

为方便语法学习,直接展开std命名空间。

个人主页链接:派小星233的博客_CSDN博客-初阶数据结构,C语言,C++初阶领域博主


一:类的6个默认成员函数

如果一个类中什么成员都没有,我们简称为空类。

但是空类并不是真的什么都没有,即使我们什么都不写,编译器也会默认生成六个成员函数。

class Date{};

 这一部分大家只需要有个基本了解,后面我会一一展开讲解的。(这些成员函数都比较特殊,不要以看待普通函数的眼光去看待它们)


二:构造函数(第一个成员)

(1)概念

我们看下面这个Date类:

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(2022, 7, 5);
	d1.Print();
	Date d2;
	d2.Init(2022, 7, 6);
	d2.Print();
	return 0;
}

在用C语言写代码的时候,我们往往会写初始化函数,每次创建结构体变量都需要手动去调用初始化函数,那能不能每次创建变量后自动去调用初始化函数呢?

答案是有的,构造函数就能解决这个问题

构造函数是一个特殊的成员函数,没有返回值(就是真正意义上的没有,连空都不是),函数名和类名相同,创建对象时编译器会自动调用这个函数,并且对单个对象来说只会调用一次

(2)特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任
务并不是开空间创建对象,而是初始化对象
。(构造函数负责初始化!初始化!!初始化!!!)

其特性如下:

  1. 函数名和类名相同
  2. 无返回值(就是真正的没有,连空都不是)
  3. 对象实例化时由编译器自行去调用构造函数
  4. 构造函数可以重载
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
    用户显式定义编译器将不再生成。
  6. 类的每个成员变量都有对应的构造函数,一个类默认生成的构造函数其实是去调用成员变量对应类型的构造函数。(这个不好理解,后面我会展开讲)
  7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个
    注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
    是默认构造函数。

(3)特性的解析

特性4

构造函数可以重载。

class Date
{
public:
	// 1.无参构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

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

int main()
{
	// 调用无参构造函数
	Date d1;
	// 调用带参的构造函数
	Date d2(2015, 1, 1); 
	// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
}


虽然可以构成重载,但我们一般不会采用上面的写法,可以利用我们前面所学的全缺省参数来使代码更加简洁

下面这个写法实际中应用更多,也非常好用!!!

class Date
{
public:
	//利用全缺省参数
	//实际中这个写法非常好用
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	// 都是调用一个函数,但不传参数,缺省值生效
	Date d1;
	// 都是调用一个函数,传了参数,缺省值无效
	Date d2(2015, 1, 1); 
}

⭐特性5

  如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。  

class Date
{
public:
	/*
	// 如果用户显式定义了构造函数,编译器将不再生成
    //这也是为什么建议写成全缺省
    //写成全缺省这里放开也不会报错
	Date(int year, int month, int day)
	{
	    _year = year;
	    _month = month;
	    _day = day;
	}
	*/
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
	// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
	// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
	Date d1;
	return 0;
}

我们把注释部分放出

⭐特性6

类的每个成员变量都有对应的构造函数,一个类默认生成的构造函数其实是去调用各个成员变量对应类型的构造函数。

大家只需要牢记这两点:

  • 默认生成的构造函数是不会处理内置类型的
  • 默认生成的构造函数会调用成员变量的构造函数,如果成员变量没有显示定义构造函数,去调用默认生成的构造函数,否则调用显示定义的构造函数

我们看下面的代码:

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

解释一下这个现象

  1. 其实实例化对象d的时候调用了类Date默认生成的构造函数
  2. 这个默认生成的构造函数又去调用了三次int类型默认生成的构造函数(_year、_month、_day这些都是内置类型,不会进行处理)和Time类型(类算是自定义类型)的构造函数
  3. 这之中Time类型的构造函数时显示定义的,故去调用该函数完成初始化

另外:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值

​
​
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
    //这个操作可以理解为给了int类型默认生成的构造函数一个缺省值
    //原本这三个变量默认生成的构造函数时一致的
    //给了默认值后编译器会为这三个变量生成不同的构造函数
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

⭐特性7

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。(它们的共同点就是可以不传参数调用)

 我们看下面这段代码:

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

int main()
{
	//调用的时候存在歧义,调用哪一个好像都可以,所以编译器会报错
	Date d1;
}

分析:

  • 上面代码调用的时候存在歧义,调用哪一个好像都可以,所以编译器会报错
  • 虽然说默认构造函数只能有一个,但是你一定要让它们同时存在也是可以的,只要你每个变量都手动初始化即可(这样另一个函数就没意义了其实,缺省也失去了作用)

三:初始化列表

(1)引入

我们看下面这个代码:

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
    //我自己写了一个构造函数,不会自动生成构造函数了
    //_t还会被初始化吗?
	Date(int year = 1,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

我们知道默认生成的构造函数会去调用成员变量的构造函数,那现在的构造函数是显示定义的,还会去调用成员变量的构造函数吗?

答案是会,这一切都是初始化列表干的。

(2)概念

初始化列表是 C++ 中用于在创建对象时初始化成员变量的一种语法形式。它可以通过在对象构造函数的函数体之前的冒号后面列出成员变量的初始化值来实现。

初始化列表的作用是在对象的构造函数中直接对成员变量进行赋值,从而避免了先默认构造再赋值的操作,减少了对象的构造和初始化时间

注意:只有显示定义的构造函数才存在初始化列表。

简单的说,初始化列表干的活就是在进入构造函数的函数体前调用成员变量的构造函数,并且这个过程是可以人为去控制的。

看代码,这个不难理解:

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date(int year = 1,int month = 1,int day = 1)
		//初始化列表在这个位置发挥作用
		//大家也可以把这个地方理解为对象的成员开辟了空间
		//展示一下如何人为控制
		:_year(year) 
		,_month(10)  //括号后面也可以自己给值
		,_day(day)  //前面所讲的声明时给默认值其实就是这里给值
	{}
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

学习了初始化列表后,建议统一使用初始化列表进行初始化,因为它可以在对象构造时对其成员变量进行初始化。这样做可以提高程序的执行效率,避免一些初始化问题,也更加规范和清晰。

但对于一些复杂的情况还是要在函数体中进行的。(比如数组的初始化和malloc申请空间的检查)

(3)注意

成员变量初始化的顺序是由声明顺序决定的,和初始化列表顺序无关。

class Date
{
public:
	Date(int year = 1,int month = 1,int day = 1)
		:_month(12) 
		,_year(_month)
		,_day(day) 
	{}
	void print()
	{
		cout << "年:>" << _year << endl; 
		cout << "月:>" << _month << endl;
		cout << "日:>" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	d.print();
	return 0;
}

成因:

  • 声明顺序决定了初始化顺序,_year最先初始化
  • 但初始化列表却用_month来初始化_year,这个时候_month还没初始化,是未知数,就导致了上面的结果

四:析构函数(第二个成员)       

(1)概念       

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作    

图解:

(2)特性

析构函数是特殊的成员函数,其特征如下:

  1.  析构函数名是在类名前加上字符 ~
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。(注意:因为析构函数规定不能有参数,故无法存在重载)    
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。     
  5. 如果类中没有显式定义析构函数,则C++编译器会自动生成一个默认的析构函数,一旦
    用户显式定义编译器将不再生成。
  6. 类的每个成员变量都有对应的析构函数,一个类默认生成的析构函数其实是去调用成员变量对应类型的析构函数。(内置类型对应的析构函数不会进行处理,这一部分的特性和前面的构造函数一致)
  7. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。

(3)例子

class SeqList
{
public:
	//析构函数
	~SeqList()
	{
		//清理向堆申请的空间
		free(_a);
		_a = nullptr;
	}
	//构造函数
	SeqList(int capacity)
		:_capacity(capacity)
		,_a(nullptr)
		,_size(0)
	{
		_a = (int*)malloc(sizeof(int) * _capacity);
		//检查是否申请成功
		if (_a == nullptr)
		{
			cout << "malloc error" << endl;
			assert(false);
		}
	}
	//一系列成员函数
private:
	int* _a;
	int _capacity;
	int _size;
};

int main()
{
	SeqList s(10);
	return 0;
}

五:拷贝构造函数(第三个成员)

(1)概念

只有单个形参,该形参是对本类类型对象的引用(常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

例子:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}
	//和构造函数构成函数重载
	A(const A& a)
	{
		_a = a._a;
	}
private:
	int _a;
};

int main()
{
	A a1(10);
	//调用拷贝构造
	A a2(a1);
}

(2)特性

拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式(写了拷贝构造就不会生成默认构造)
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
    因为会引发无穷递归调用。(一会会解释为什么造成无穷递归)
  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
    字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
    (就是一个个字节的把成员变量拷贝过去)。
  4. 类的每个成员变量都有对应的拷贝构造函数,一个类默认生成的拷贝构造函数其实是去调用成员变量对应类型的拷贝构造函数。(内置类型对应的拷贝构造函数会进行处理,这一部分的特性和前面的构造函数有一些不同)
  5. 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 
    时,则拷贝构造函数是一定要写的,否则就是浅拷贝。 (一会举个例子)
  6. 拷贝构造的调用用两种写法,第一种是A a1(a2)第二种是A a3 = a2这两种写法是完全等价的,一定要牢记这一点。

(3)特性的解析

⭐特性2

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

原因很简单,传函数参数和函数返回值的时候都会创建一个临时变量,如果传递的参数是一个类对象,这个临时变量的创建其实就是去调用拷贝构造函数

然后就形成了下面这种情况:

展开讲这个特性主要就是知道对象在传参和做返回值的时候需要创建一个临时变量,这个过程调用了拷贝构造

⭐特性3

若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝(就是一个个字节的把成员变量拷贝过去)。

代码:

class Time
{
public:
	Time()
		:_hour(1)
	{}
private:
	int _hour;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	return 0;
}

⭐特性5

类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

代码(这段代码运行直接运行会崩溃):

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
		:_size(0)
		,_capacity(capacity)
	{
		_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;
};
int main()
{
	Stack s1;
	//不仅仅是不符合要求,运行的时候会导致崩溃
	//原因是对堆上申请的空间多次释放
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}


 六:运算符重载

剩下的三个成员函数都涉及到运算符重载,而且运算符重载的意义很大,所以这里单独讲一下。

(1)概念

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
(类也可以使用运算符,通过把运算符表达式转换成相关函数,使用起来简洁,可读性强)

函数名字为:关键字operator后面接需要重载的运算符符号。

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

(2)实例

代码:

//以 == 重载为例
class A
{
public:
	A(int a)
		:_a(a)
	{}
	bool operator==(const A& x)
	{
		return (x._a == _a);
	}
private:
	int _a;
};

int main()
{
	A a1(5);
	A a2(10);
	A a3(10);
	cout << (a1 == a2) << endl;
	cout << (a1 == a2) << endl;
	//(a1 == a2)实际上就是a1.operator==(&a1,a2)
	//这里加()是因为cout本质也是函数重载,不加括号就会先和a1结合,cout和a1结合去调用函数
	//这个调用的返回值是另一个类的对象,这个对象再和>a2结合,我们没有实现这个函数重载,会报错
	//后面专门讲一下输入输出实现
}

(3)注意

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
    藏的this
  • *(解引用)、::(作用域限定符)、 sizeof 、?:(三目操作符)、 .(类成员访问操作符)、 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
  • 运算符重载的结合顺序与运算符优先级、结合性相关。
  • 运算符重载大多数情况下作为类的成员函数,实在没办法的情况可以做成正常函数,本质都是替换成函数调用。(比如要实现类的输入输出,后面单独讲)

七:赋值运算符重载(第四个成员)

(1)赋值运算符重载格式

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要复合连续赋值的含义
代码:
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

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

	Date& operator=(const Date& d)
	{
		//this指针和传递过来的对象地址一样说明是自己赋值自己,不进行处理
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    Date d1;
    Date d2;
    Date d3(2022,11,11);
    d1 = d2 = d3;
    //连续赋值,相当于先d2 = d3(d2.operator=(&d2,d3)),返回值为d2的引用
	//然后d1 = d2(相当于d1.operator=(&d1,d2))
    return 0;
}

(2)特性

  1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载(这一点很特殊),以值的方式逐字节拷贝(这种属于前面所谓的浅拷贝,对需要清理资源的情况不适用),显示定义则不会生成。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
  2. 基于第一点,引出另一个特性,赋值运算符只能重载成类的成员函数不能重载成全局函数

⭐特性1

对于什么时候应该自己实现赋值重载,以我们前面所说的栈为例子。

总结:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必
须要实现。
 

⭐特性2

代码:

//这一段代码无法通过编译
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
	if (&left != &right)
	{
		left._year = right._year;
		left._month = right._month;
		left._day = right._day;
	}
	return left;
}

 原因:赋值运算符如果不显式定义,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数


八:const修饰的成员

我们先看下面这段代码:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}
	void print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

int main()
{
	const A a1;
	a1.print();
	return 0;
}

编译结果:

我们知道C++中用const修饰的变量已经属于常量了,但即使是常量,我们也希望它可以打印、比较、作为拷贝母本,但上面的代码却连编译都通过不了。

原因:这涉及到权限放大和缩小的问题,因为被隐藏的this指针传递时默认的类型是->类名* const this,*前面没有用const修饰,表示这个指针指向的对象是可修改的,但是上面的对象用了const修饰,传过来的指针类型是->const 类名* const this,这属于权限的放大,是不被允许的。

解决方法:想解决这个问题,需要把被隐藏的this的类型前面加上const,但this被隐藏了,我们应该怎么告诉编译器我们的需求呢?

小结:对于一些不需要修改对象的成员函数,我们尽量在函数后加上const,可以让代码的应用更加广泛。(前面的大部分例子其实都应该加const修饰) 


九:取地址及const取地址操作符重载(第五、六个成员)

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public:
	Date* operator&()
	{
		return this;
	}
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

十:友元

在默认的情况下,一个对象的成员变量只能在类的内部访问,但如果我们想在其它地方访问类的成员变量,该怎么办呢?这个就需要友元了

注意:友元可以更加灵活,但是破坏了封装,应该尽量少用。

(1)友元函数

class A
{
public:
	//关键字friend,在类中进行函数声明即可。
	friend void print(A& a);
	A(int a = 0)
		:_a(a)
	{}
private:
	int _a;
};

//想让非成员函数访问类成员变量
void print(A& a)
{
	a._a++;
	cout << a._a << endl;
}
int main()
{
	A a1(20);
	print(a1);
}

(2)友元类

class A
{
public:
	//利用关键字friend,A是B的友元,B可以访问A,A不可以访问B
	//A把B当朋友,B不一定把A当朋友
	friend class B;
	A(int a = 0)
		:_a(a)
	{}
private:
	int _a;
};

class B
{
public:
	
	B(int b = 0)
		:_b(b)
	{}
	void print()
	{
		//在B类中访问A类的对象
		cout << "A:>" << a._a << "B:>"<< _b << endl;
	}
private:
	int _b;
	A a;
};

十一:实现一个比较完整的日期类

(1)先讲几个比较重要的点

获取月天数

// 获取某年某月的天数
//这个很容易理解,后面经常会用到这个函数
int	Date::GetMonthDay(int year, int month) const
{
	//会多次调用这个函数,所以直接设计成静态的数组
	static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
	int day = days[month];
	if (month == 2
		&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
	{
		day += 1;
	}
	return day;
}

实现比较运算符重载的原则

现在写好了一个>比较的重载和==比较的重载,要实现<的重载,有人可能会选择直接拷贝>比较的代码然后更改比较逻辑,这样的做法会让代码变得非常冗余。


像这样的比较关系,我们只需要写好>(或者<)比较和==比较,其它的比较直接复用这两个重载,然后逻辑取反即可。

例如:<的判断可以写成 !((A>B) || (A == B)),括号里面的条件判断>=,>=的逻辑取反不就是<吗?其它的情况以此类推即可。

⭐输入输出重载

cout是ostream这个类的实例化全局对象,cout << a(int)实际是去调用这个对象的成员函数,这个成员函数有一个参数是int类型,cout << d(double),这个也是一个成员函数,但是有一个参数是double类型,可以和前面int对应的函数构成函数重载,不过ostream只重载了内置类型。

至于连续打印的实现,cout << a(int) << d(double),实际上先和a结合调用int对应的成员函数,然后返回cout的引用,再和d结合去调用double对应的成员函数。


cin是istream这个类的实例化全局对象,它能自动识别类型、连续输入的原理和cout类似,不同的是它需要更改变量的值,所以传递的时候是传引用而不是传值。


那如果我们直接把输入输出重载为成员函数会怎么样呢?

代码:

class Date
{
public:
	const Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	ostream& operator<<(ostream& out)const
	{
		//<<实现内置类型重载的时候类型是没有用const修饰的
		//这里不能加const修饰,不然就找不到这个成员函数了
		out << _year << "." << _month << "." << _day << endl;
		return out;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	const Date d1;
	const Date d2(2022,11,11);

	cout << d1 << d2 << endl;
	//这个先看ostream这个类里面有没有实现这个重载
	//没有看全局有没有第一个参数为ostraem&,第二个参数为Date&的全局函数
	//都没有找到说明没有这个函数,编译报错
}


为解决上面的问题,我们可以把这个重载设计成全局函数,这个全局函数需要访问对象的成员变量,我们需要声明这个全局函数是类的友元函数。

class Date
{
public:
	const Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	//声明,表明这个函数是友元函数
	friend ostream& operator<<(ostream& out,const Date& d);
private:
	int _year;
	int _month;
	int _day;
};

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

int main()
{
	const Date d1;
	const Date d2(2022,11,11);

	cout << d1 << d2 << endl;
}

输入和打印是同理的。

前置++(--)和后置++(--)的区分

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 前置++:返回+1之后的结果
	// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
	Date& operator++()
	{
		_day += 1;
		return *this;
	}
	// 后置++:
	// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
	// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
	// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
	//而temp是临时对象,因此只能以值的方式返回,不能返回引用(之前讲过)
	Date operator++(int)
	{
		Date temp(*this);
		_day += 1;
		return temp;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d;
	Date d1(2022, 1, 13);
	d = d1++; // d: 2022,1,13   d1:2022,1,14
	d = ++d1; // d: 2022,1,15   d1:2022,1,15
	return 0;
}

(2)日期类实现(分文件)

大家可以先对照声明尝试自己写一下,大部分的逻辑都比较简单。

Date.h(函数声明)

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
using namespace std;

class Date
{
public:
	//两个友元函数
	friend ostream& operator<<(ostream& out,const Date& d);
	friend istream& operator>>(istream& in,Date& d);
	// 获取某年某月的天数
	int GetMonthDay(int year, int month)const;

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

	// 拷贝构造函数
	// d2(d1)
	Date(const Date& d)
		:_year(d._year)
		,_month(d._month)
		,_day(d._day)
	{
	}

	// 赋值运算符重载
	// d2 = d3 -> d2.operator=(&d2, d3)
	//不传引用返回会多构造一个
	Date& operator=(const Date& d);
	// 析构函数(日期类可以不写,这里是为了方便观察)
	~Date()
	{
		//cout << "析构" << endl;
	}
	// 日期+=天数
	Date& operator+=(int day);

	// 日期+天数
	Date operator+(int day) const;

	// 日期-天数
	Date operator-(int day) const;

	// 日期-=天数
	Date& operator-=(int day);

	// 前置++
	Date& operator++();

	// 后置++,这个int参数只是为了区分,传什么值又编译器自行处理
	Date operator++(int);

	// 后置--
	Date operator--(int);

	// 前置--
	Date& operator--();

	// >运算符重载
	bool operator>(const Date& d)const;

	// ==运算符重载
	bool operator==(const Date& d) const;

	// >=运算符重载
	bool operator >= (const Date& d) const;

	// <运算符重载
	bool operator < (const Date& d) const;

	// <=运算符重载
	bool operator <= (const Date& d) const;

	// !=运算符重载
	bool operator != (const Date& d) const;

	// 日期-日期 返回天数
	int operator-(const Date& d) const;

	//进行日期检查,合法返回1,否则返回0
	int CheckDate() const;

	//打印
	void print()
	{
		cout << _year << "|" << _month << "|" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

//某一天是星期几
int GetWeek(Date& d);

⭐Date.cpp(函数实现)

​
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"

//打印重载
ostream& operator<<(ostream& out,const Date& d)
{
	cout << d._year << "." << d._month << "." << d._day << endl;
	return out;
}

//输入重载
istream& operator>>(istream& in,Date& d)
{
	cin >> d._year >> d._month >> d._day;
	//输入完检查一下是否合法
	assert(d.CheckDate());
	return in;
}

// 全缺省的构造函数
Date::Date(int year, int month, int day)
	:_year(year)
	, _month(month)
	, _day(day)
{
	//检查一下是否合法
	assert(CheckDate());
}

// 获取某年某月的天数
int	Date::GetMonthDay(int year, int month) const
{
	//会多次调用这个函数,所以直接设计成静态的数组
	static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
	int day = days[month];
	if (month == 2
		&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
	{
		day += 1;
	}
	return day;
}

// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
//不传引用返回会多构造一个
Date& Date::operator=(const Date& d)
{
	if (this != &d)
	{
		_day = d._day;
		_month = d._month;
		_year = d._year;
	}
	return *this;
}

// 日期+=天数
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) const
{
	Date tmp(*this);
	tmp += day;
	return tmp;
}

// 日期-=天数
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}

	_day -= day;
	//减到0要变成下个月的最后一天
	while (_day <= 0)
	{
		_month--;
		//如果减到0,要变成下一年的12月
		if (_month <= 0)
		{
			_month = 12;
			_year--;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

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

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

// 后置++,这个int参数只是为了构成函数重载,传什么值编译器自行处理
Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}

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

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

// >运算符重载
bool Date::operator>(const Date& d) const
{
	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) const
{
	return (_year == d._year)
		&& (_month == d._month)
		&& (_day == d._day);
}

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

// <运算符重载
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);
}

// 日期-日期 返回天数
int Date::operator-(const Date& d) const
{
	Date max = *this;
	Date min = d;
	int n = 0;
	//一开始默认前面大于后面
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		//如果后面大于前面,最后结果为负数
		flag = -1;
	}

	while (max > min)
	{
		min++;
		n++;
	}
	return n * flag;
}

int GetWeek(Date& d)
{
	Date tmp(1, 1, 1);
	int week = 0; //0 - 周一
	int n = d - tmp;
	week += n;
	return week % 7;
}

//合法返回1,否则返回0
int Date::CheckDate() const
{
	return _month > 0 && _month < 13 && (_day > 0 && _day <= GetMonthDay(_year, _month));
}

​

⭐test.cpp(测试)

#include "Date.h"

void text1()
{
	//  =
	/*Date d1;
	Date d2(3000, 11, 11);
	Date d3;
	d3 = d1 = d2;
	d1.print();
	d2.print();
	d3.print();*/


	//  +=
	/*Date d1(2022,12,1);
	d1 += 50;
	d1.print();
	d1 += 500;
	d1.print();
	d1 += 5000;
	d1.print();*/

	// + 
	//Date d1(2022, 11, 1);
	//Date d2 = d1 + 50;
	//d1.print();
	//d2.print();


	// -
	/*Date d1(2022, 12, 20);
	Date d2 = d1 - 20;
	d1.print();
	d2.print();*/

	// -=
	//Date d1(2022, 12, 20);
	//d1 -= d1 -= 20;

	// 前置++
	/*Date d1(2022, 11, 30);
	Date d2 = ++d1;*/

	// 后置++
	/*Date d1(2022, 11, 1);
	Date d2 = d1++;*/

	//前置--
	/*Date d1(2022, 11, 1);
	Date d2 = --d1;*/

	// 后置--
	/*Date d1(2022, 11, 1);
	Date d2 = d1--;*/


}
void text2()
{
	Date d1(2022, 12, 11);
	Date d2(2023, 12, 12);
	//cin >> d1 >> d2;
	cout << d1 << d2 << endl;
	// >
	/*Date d1(2022, 12, 20);
	Date d2(2023, 12, 20);
	cout << (d2 > d1) << endl;*/
	
	// ==
	/*Date d1(2023, 11, 20);
	Date d2(2023, 12, 20);
	cout << (d2 == d1) << endl; */
	
	// >=
	/*Date d1(2022, 12, 20);
	Date d2(2023, 12, 20);
	cout << (d2 >= d1) << endl;*/ 


	// <
	/*Date d1(2022, 12, 20);
	Date d2(2022, 12, 20);
	cout << (d2 < d1) << endl; */


	// <=
	/*Date d1(2022, 12, 20);
	Date d2(2023, 12, 20);
	cout << (d2 <= d1) << endl; */


	// !=
	/*Date d1(2022, 12, 20);
	Date d2(2022, 12, 21);
	cout << (d2 != d1) << endl; */


	// -(日期-日期 == 天数)
	/*Date d1(2022, 12, 20);
	Date d2(2023, 12, 30);
	cout << (d2 - d1) << endl; */

}

void menu()
{
	cout << "*****************************" << endl;
	cout << "1.日期加/减天数   2.日期-日期" << endl;
	cout << "3.算星期几        -1.退出    " << endl;
	cout << "*****************************" << endl;

}

//用来计算某一天是星期几,日期间的加减
void text3()
{
	int input = 0;
	int day = 0;
	Date d1;
	Date d2 = d1;
	//用下标来一一对应,0下标代表星期一
	const char* WeekArr[] = { "周一","周二","周三","周四","周五","周六","周日" };
	do
	{
		menu();
		cout << "请输入:>";
		cin >> input;
		if (input == 1)
		{
			cout << "请输入一个日期(空格隔开):>";
			cin >> d1;
			cout << "请输入天数:>";
			cin >> day;
			cout << d1 + day << endl;
		}
		else if (input == 2)
		{
			cout << "请输入两个日期(空格隔开):>";
			cin >> d1;
			cin >> d2;
			cout << d1 - d2 << endl;
		}
		else if (input == 3)
		{
			cout << "请输入一个日期(空格隔开):>";
			cin >> d1;
			cout << WeekArr[GetWeek(d1)] << endl;
		}
		else if (input != -1)
		{
			cout << "非法输入" << endl;
		}
	} while (input != -1);
}


int main()
{
	//text1();
	//text2();
	text3();
	return 0;
}

猜你喜欢

转载自blog.csdn.net/2301_76269963/article/details/131189359
今日推荐