类和对象(中)

前言

在上一篇博文,我们了解类中有成员变量和成员函数。在本篇博文,将会详细介绍类的默认成员函数

我们来看下面这个类

class A
{
	
}

空类中真的什么都没有吗?并不是,类在创建时,编译器会自动生成以下6个默认成员函数。
在这里插入图片描述

类的默认成员函数是用户在创建类时隐式生成的。


一、构造函数

🌽 概念

构造函数是类的一种特殊成员函数,用于初始化对象的状态。构造函数在对象创建时被自动调用,其主要目的是给对象的成员变量赋初值和进行必要的资源分配。并且在对象整个生命周期内只调用一次

#include<iostream>
using namespace std;

class Date
{
public:
	int year;
	int month;
	int day;
	//构造函数
	//(1)无参构造函数

	Date()
	{
		this->year = 2;
		this->month = 2;
		this->day = 2;
	}
	//(2)有参构造函数

	Date(int year , int month , int day )
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}

public:
	void Init(int year, int month, int day)
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}

	void Init()
	{
		this->year = 2;
		this->month = 2;
		this->day = 2;
	}

	void Print()
	{
		cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
	}
};


int main()
{
	//没有传递参数,自动调用无参构造函数
	Date d1;
	//自动调用有参构造函数
	Date d2(2,3,4);
	
	d1.Print();
	//out: 2年2月2日
	d2.Print();
	//out: 2年3月4日
	return 0;
}

🥕 特性

(1)函数名和类相同
(2)构造函数无返回值(无需写void)
(3)实例化对象自动调用对应类的构造函数
(4)构造函数支持重载

#include<iostream>
using namespace std;

class Date
{
public:
	int year;
	int month;
	int day;

	Date()
	{
		this->year = 5;
		this->month = 5;
		this->day = 5;
	}

	Date(int year, int month, int day)
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}
	
	void Print()
	{
		cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
	}
	
};


int main()
{
	//调用无参构造函数
	Date d1;
	//有参构造函数
	Date d2(4, 6, 9);
	d1.Print();
	d2.Print();
	return 0;
}

d1对象调用Date类无参构造函数,d2对象调用Date有参构造函数。
在这里插入图片描述

注意使用类实例化对象时不需要加(),否则会和返回类型为该类的函数混淆。

Date d1(); //错误,容易和函数声明混淆
Date d1;//正确

(5)只有当用户没有显式定义构造函数编译器才会自动生成一个无参构造函数

  • 当显式定义一个有参构造函数
    在这里插入图片描述
    由于用户定义了有参构造函数,编译器没有自动生成无参构造函数,因此在创建对象d1时不能以无参的形式初始化。

  • 当无显式定义构造函数

#include<iostream>
using namespace std;

class Date
{
public:
	int year;
	int month;
	int day;
	//构造函数
	//(1)无参构造函数

	/*Date()
	{
		this->year = 2;
		this->month = 2;
		this->day = 2;
	}*/
	//(2)有参构造函数

	/*Date(int year , int month , int day )
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}*/

public:
	void Init(int year, int month, int day)
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}

	void Init()
	{
		this->year = 2;
		this->month = 2;
		this->day = 2;
	}

	void Print()
	{
		cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
	}
};


int main()
{
	//没有传递参数,自动调用无参构造函数
	Date d1;

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

在这里插入图片描述
很明显,我们没有传入值到类中。因此当使用编译器的无参构造函数,类的内置类型不会被初始化

(6)编译器自动生成的默认构造函数,对内置类型的对象不做处理,只调用自定义类型对象构造函数。

#include<iostream>
using namespace std;


class A
{
private:
	int x;
	int y;
	
public:
	A()
	{
		cout << "hhh" << endl;
	}
};

class Date
{
public:
//内置类型
	int year;
	int month;
	int day;
//自定义类型
	A a;
public:
	void Init(int year, int month, int day)
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}

	void Init()
	{
		this->year = 2;
		this->month = 2;
		this->day = 2;
	}

	void Print()
	{
		cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
	}
};


int main()
{
	Date d1;

	d1.Print();
	return 0;
}

创建了一个A类
在这里插入图片描述
当创建基于Date类的对象d1时,使用了编译器生成默认无参构造函数,去调用了自定义类型 A类 的构造函数
默认无参构造函数不处理内置类型,所以在打印Date类中的内置类型year,month,day才会出现随机值。

  • 内置类型

C++语言自带的数据类型,如基本数据类型,指针类型,引用类型,数组类型等等

  • 自定义类型

自定义类型是程序员根据特定需求定义的类型,主要包括类,结构体,联合,枚举等等。

注意:C++11针对使用用构造函数内置类型无法初始化这个问题,打了补丁,即内置类型的成员变量在声明时可以给默认值

#include<iostream>
using namespace std;

class Date
{
public:
	int year;
	int month;
	int day;
	//无参构造函数
	//Date()
	//{
	//	this->year = 5;
	//	this->month = 5;
	//	this->day = 5;
	//}
	//全缺省构造函数
	Date(int year=5, int month=5, int day=5)
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}
	
	void Print()
	{
		cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
	}
};


int main()
{
	Date d1;
	Date d2(4, 6, 9);
	d1.Print();
	d2.Print();
	return 0;
}

全缺省构造函数中实现了无参构造函数的功能——内置类型初始化

(7)无参的构造函数全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个
被认为是默认构造函数的有三种:

  • 显式的无参构造函数
Date()
{
	this->year = 1;
	this->month = 1;
	this->day = 1;
}
  • 显式的全缺省构造函数
Date(int year=1, int month=1, int day=1)
{
	this->year = year;
	this->month = month;
	this->day = day;
}
	
  • 编译器默认生成的构造函数

注意默认构造函数是指不传参就可以调用的构造函数

比如上面提到的无参构造函数全缺省函数

总结一般情况下构造函数都需要我们显式的实现,只有少数场景可以让编译器自动生成

  • 由于构造函数对内置类型的对象不做处理,只调用自定义类型对象构造函数,而自定义类型本质上也是内置类型
  • 当类内部成员都是自定义类型时,可以让尝试编译器自动生成

二、析构函数

🍸 概念

析构函数负责在对象生命周期结束时自动执行清理工作,功能恰好和构造函数相反。

那么清理工作具体是什么呢?

  1. 释放对象在构造和使用过程中分配的资源(如动态内存、文件句柄、网络连接等)
  2. 调用对象中其他基类生成对象的析构函数(在派生类中,析构函数会隐式或显式地调用基类的析构函数,以确保基类部分也得到适当清理)

🍹 特性

(1)析构函数名是在类名前加上字符 ~
(2)析构函数无参数无返回类型(不需要写void
(3)一个类只能有一个析构函数,如果用户没有显式定义,编译器会自动生成默认的析构函数。
(4)对象生命周期结束时,C++编译系统系统自动调用析构函数

  • 对象离开作用域
#include<iostream>
using namespace std;

class Date
{
public:
	int year=8;
	int month=9;
	int day=10;

	Date(int year=1, int month=1, int day=1)
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}
	
	~Date()
	{
		cout << "~Date" << endl;
		if (year||month||day)
		{
			year = 0;
			month = 0;
			day = 0;
		}
	}

	void Print()
	{
		cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
	}
};


void func()
{
	Date d;
}

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

func函数内部创建了Date类的对象d,func函数调用结束,对象d离开了作用域,被析构函数释放资源
在这里插入图片描述

  • 关闭文件句柄
#include <iostream>
#include <fstream>

class FileHandler {
private:
    std::ofstream file;

public:
    FileHandler(const std::string& filename) {
        file.open(filename);
        std::cout << "File opened.\n";
    }

    ~FileHandler() {
        if (file.is_open()) {
            file.close();
            std::cout << "File closed.\n";
        }
    }
};

int main() {
    FileHandler file("example.txt");
    // ...
    return 0;
}

析构函数确保打开的文件在对象生命周期结束时被关闭。
在这里插入图片描述

  • 调用基类析构函数
#include <iostream>

class Base {
public:
    ~Base() {
        std::cout << "Base class cleanup.\n";
    }
};

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived class cleanup.\n";
    }
};

int main() {
    Derived obj;
    return 0;
}

在这里插入图片描述
(5)析构函数只需要对有资源申请的类进行处理。(自定义类型成员

Q:如何理解有资源申请?
这些资源不是简单的数据类型(如int、float、double等),它们的生命周期需要被手动管理。如果这些资源没有被适当地释放,就可能导致资源泄漏。

#include<iostream>
using namespace std;

class Time
{
public:
	~Time()
	{
		cout << "~Time" << endl;
	}
};

class Date
{
public:
	int year=8;
	int month=9;
	int day=10;
	Time t;

	Date(int year=1, int month=1, int day=1)
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}
	

	void Print()
	{
		cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
	}
};

int main()
{
	Date d;
	return 0;
}

Date类中有自定义成员Time类的对象t,所以在销毁对象d时需要先销毁成员变量Time类的对象t。

编译器会在Date类中生成一个默认析构函数,这个函数再去调用Time类的析构函数。

由于year month day 成员变量是内置类型,析构函数不需要对其进行处理(编译器会自动清理)
在这里插入图片描述
所以当类内部没有资源申请(如只有内置类型),则不需要显式调用析构函数,编译器会自动清理。

🥂 析构顺序辨析

我们来看下面这个代码

#include<iostream>
using namespace std;

class A
{
public:
	A(int x=1)
		:_x(x)
	{
		cout << "A(int x)" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _x;
};

A func()
{
	static A a;//函数局部的静态只有第一次调用才构造,生命周期是全局的
	return a;
}

A a3;//全局对象在进入main函数之前构造

int main()
{
	//后定义先析构
	A a1;
	A a2;
	func();
	func();

	return 0;
}

问题来了,谁先构造谁先析构呢?

  • 通过测试,整个程序进行了四次构造,对象a3第一个构造,a1a2和函数的静态对象aa逐一构造。整个程序进行了六次析构,顺序为:两次返回临时对象析构,a2a1析构,静态对象aa析构,最后是全局对象a3析构。

请添加图片描述

//构造顺序:
1)a3
2)a1
3)a2
4)aa

//析构顺序:
1)两次返回临时对象
2)a2
3)a1
4)aa
5)a3

☑️总结:

  1. 全局对象在进入main函数前被构造
  2. 局部静态对象只有第一次调用才会发生构造
  3. 对象后定义先析构

三、拷贝构造函数

🥝 概念

在前面的学习,我们知道了变量可以用其他变量来初始化

	int a = 1;
	int b = a;

那么对于对象,我们也能通过这种方式初始化吗?拷贝构造函数就是为了解决这个问题的。

拷贝构造函数会创建一个新对象,并用另一个同类型的对象初始化,生成一个新的对象副本

拷贝构造只有一个参数,规定使用引用传参(一般用const修饰),使用已存在的类类型对象创建时编译器自动调用

🍅 特性

C++规定自定义类型的传值传参必须调用拷贝构造

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数只有一个参数,并且必须是当前类对象的引用
class Time
{
private:
	int year;
	int month;
	int day;

public:
	//拷贝构造函数(只能使用引用传参,传值传参会出现无限递归问题)
	//Time(const Time t) //错误写法
	Time(const Time& t)//正确写法
	{
		this->year = t.year;
		this->month = t.month;
		this->day = t.day;
	}
};

当使用传值调用会造成无限递归调用,编译器会报错。
在这里插入图片描述

无限递归调用原因

如果拷贝构造函数的参数是通过值传递的,那么每次调用拷贝构造函数时,都会创建一个新的对象副本,这个副本又需要调用拷贝构造函数来创建它的副本,如此反复,形成无限递归。

使用引用传参就不会造成无限递归调用原因

使用引用传递方式,拷贝构造函数接收的是一个已经存在的A对象的引用,而不是一个新的副本。这样,拷贝构造函数就不需要再次创建一个新的对象副本,从而避免了无限递归。

每次创建新的对象,拷贝构造函数都会自动创建一个该对象的副本

  1. 拷贝构造函数的参数需要使用const修饰符

(1)保证参数对象不被修改

通过使用const修饰,将无法从拷贝构造函数中修改参数对象。(拷贝构造函数的目的是创建一个对象的副本,而不是改变原有对象的状态。通过将参数声明为const类型,可以确保函数内不会对传入的对象进行任何修改)

(2)允许常量对象作为参数
如果拷贝构造函数的参数不是const引用,那么它只能接受非常量对象作为参数。如果不使用const修饰符,传入常量对象会造成权限放大

#include<iostream>
using namespace std;

class Time
{
private:
	int year;
	int month;
	int day;

public:
	Time(int year=1,int month=2,int day=3)
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}

	Time(Time& t)
	{
		this->year = t.year;
		this->month = t.month;
		this->day = t.day;
	}

	void Print()
	{
		cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
	}
};

int main()
{
	int a = 1;
	int b = a;
	const Time t1(8,9,10);
	//Time t2 = t1;
	//这里会报错 常量对象t1无法赋值给非常量对象t2,会造成权限放大
	return 0;
}

在这里插入图片描述
4. 若拷贝构造函数未显式定义,编译器会生成默认的拷贝构造函数

默认的拷贝构造函数对象按内存存储字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

我们把上面显示定义的拷贝构造函数删除,观察编译器是否生成了默认拷贝构造函数

#include<iostream>
using namespace std;

class Time
{
private:
	int year;
	int month;
	int day;

public:
	Time(int year=1,int month=2,int day=3)
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}


	void Print()
	{
		cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
	}
};

int main()
{
	Time t1(8,9,10);
	Time t2 = t1;

	t1.Print();
	t2.Print();

	return 0;
}

可以看到调用了编译器生成的默认拷贝构造函数完成了Time类内置类型的拷贝
在这里插入图片描述

因为日期类中没有涉及资源申请,只有内置类型,因此使用编译器生成的默认构造函数就可以完成拷贝。(这种拷贝被称为值拷贝浅拷贝

  1. 涉及资源申请的类需要使用深拷贝

不同于上述的日期类,当类中涉及到资源申请,如栈这种数据结构,需要我们显式的去完成深拷贝

  • 不使用深拷贝会怎么样

以栈举例

存在栈st1,当使用浅拷贝引用创建st2,栈st1和st2指向同一块内存空间。当程序退出时,st1和st2分别调用它们的destory函数,这会导致释放同一块内存空间多次,导致程序崩溃

  • 深拷贝有什么不同?

深拷贝不仅复制对象的非静态成员变量的值,还为指针或动态分配的内存分配新的内存,并复制指针指向的数据到新分配的内存中。这样,新旧对象的指针指向不同的内存区域,互不影响

  • 如何实现深拷贝?

(1)自定义拷贝构造函数:在拷贝构造函数中,为每个指针成员分配新的内存,并复制数据到新的内存中。

以栈举例

typedef int DataType;
class Stack
{
public:
	//显式手动的分配内存
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}

	Stack(const Stack& s)
	{
		_array = (DataType*)malloc(sizeof(DataType) * s._capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!");
			return;
		}
		memmove(s._array, _array, sizeof(DataType) * s._size);//复制对象副本
		_capacity = s._capacity;
		_size = s._size;
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	//析构函数
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

(2)使用智能指针:如std::unique_ptr或std::shared_ptr,这些智能指针类型自动管理内存,可以简化资源管理并减少内存泄漏的风险。(后续深入研究,了解即可)

四、赋值运算符重载

🏹 运算符重载

为了使代码更易读和更易于维护,C++引入了运算符重载。运算符重载是拥有特殊函数名的函数,函数名为关键字operator,该函数和普通函数一样有返回类型。

例如我要对=进行重载,赋值后返回被赋值的变量。

返回值类型 operator=(参数列表)

作用

运算符重载是允许开发者重新定义或“重载”现有运算符(如 +, -, *, /等)以用于自定义数据类型的过程。重载后,这些运算符可以直接用于自定义类型的实例,就像它们用于内置数据类型一样。

注意
(1)使用operator时只能对已经存在的运算符进行重载

例如 operator$ 就不行,因为不存在$运算符

(2)重载操作符必须有一个类类型参数(因为重载运算符的目的是用于自定义类型的使用
(3)作为类成员函数重载时,形参比操作数数目少1,因为成员函数的第一个参数为隐藏的this
使用举例

//重载==运算符,对两个自定义类型进行比较,只需要传入一个类对象的参数即可
bool operator==(const Date & d)//尽量使用引用传入
{
	return  this->_year == d._year
		&& this->_month == d._month
		&& this->_day == d._day;
}

重载成全局,无法访问私有成员

1)提供这些成员的get 和 set
2)使用友元
3)重载为成员函数(一般使用这种方法)

在类外部使用友元来访问私有的成员变量

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

(4)重载后的运算符,其特性应该和重载前一致
(5)以下五个运算符不能重载

  • ?: (三元运算符)
  • . (引用对象成员)
  • :: (域的解析符)
  • sizeof (获取数据在内存中所占用的存储空间)
  • .* (引用指向类成员的指针)

🎣 赋值运算符重载

(1)重载格式

  • 参数类型const 类名&,使用引用减少拷贝,提高传参效率
  • 返回值类型类名 &返回对象出作用域不销毁(没有调用析构函数),使用引用&,否则只能用传值 类名

能用传引用就用传引用,传引用无需拷贝,传值需要拷贝(复制)

  • 检测*this是否和参数地址完全相同
  • *this支持连续赋值,保持运算符的特性
//重载!=运算符
bool operator!=(const Date& d)
{
	return  !(this->_year == d._year
		&& this->_month == d._month
		&& this->_day == d._day);
}

//重载=运算符
Date &operator=(const Date& d)
{
//检测*this地址和d是否完全相同,避免自己给自己赋值
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	//this是指向当前类对象的指针
	return *this;
}

(2)特性

  • 赋值运算符只能在类内部重载为类的成员函数,不能在类外实现

在全局使用operator没有this指针,所以需要传入多一个参数
在这里插入图片描述operator必须是非静态成员

  • 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝

对于内置类型编译器会自动拷贝,对于自定义类型调用对应类的赋值运算符重载

  • 和拷贝构造函数类似,如果类中未涉及到资源申请,赋值运算符是否实现都可以。一旦涉及到资源申请则必须要显式实现

🏑 前置++和后置++

(1)前置++重载

//前置++ 使用传引用提高效率
Date& operator++()
{
	_day++;
	return *this;
}

(2)后置++重载
后置++先赋值再自增,用*tmp拷贝原来的对象,对象自增后返回 *tmp

//后置++
Date& operator++()
{
	Date* tmp = this;
	_day++;
	return *tmp;
}

🥊 流运算符重载

(1)概念
流运算符<<(左移)和>>(右移)主要用于I/O操作。左移运算符用于输出,而右移运算符用于输入。在C++标准库中,它们已经被重载,用于处理各种内置类型

但是对于自定义的数据类型(一般为类或结构体),则需要开发者显式的重载

operator>>和operator<<可以重载为成员函数
d1<<cout;
但是用起来不符合正常逻辑,不建议这样处理,建议重载为全局函数(下面的举例均重载为全局函数)

(2)重载输入运算符>>

istream 是C++标准库中的一个类,代表输入流。函数返回istream的引用istream&),istream &in是输入流对象,Date& d是我们自定义的类对象。返回in支持链式操作

istream& operator >> (istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

(3)重载输出运算符<<
ostream 是C++标准库中的一个类,代表输出流返回out支持链式操作

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

注意:iostream和ostream不支持拷贝构造
(4)问题和反思

  1. 为什么使用引用&返回

不加引用是值传递,会重新拷贝一份,此时的out或in不再是使用者传递的对象,导致编译器无法正确识别和重载该运算符。使用引用能避免对象的复制并且能够保证参数的唯一性

  1. 为什么istream不能加const,ostream可以加const?
  • istream不能加const,因为输入操作会改变流的状态,需要修改流的内部状态
    (istream的主要职责是从流中读取数据,这个过程会改变流的状态(如流的缓冲区、读取位置等)
  • ostream可以加const,因为输出操作不需要修改流的内部状态。

五、日期类的实现

.h文件

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
#define endl "\n"
class Date
{
private:
	int _year;
	int _month;
	int _day;

public:
	//流运算符重载(使用友元使能在类外部访问类的私有变量)
	friend ostream& operator << (ostream& out, const Date & d);
	friend istream& operator >> (istream& in, Date& d);
	//为什么istrean不能加const :输入运算符通常需要修改对象的状态,因为它要从输入流中读取数据并将其存储在对象中
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;

		if (!checkdate())
		{
			cout << "日期不合法!" << endl;
		}
	}

	bool isleapyear(int year)
	{
		return year % 400 == 0 || year % 100 != 0 && year % 4 == 0;
	}

	int getday(int year, int month)
	{
		assert(year >= 1);
		assert(month >= 1 &&month<=12);
		//静态数组,相当于放在全局
		static int mon[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (isleapyear(year))
		{
			if (month == 2) return 29;
		}
		return mon[month];
	}

	int getday(const Date& d)
	{
		assert(d._year >= 1);
		assert(d._month >= 1 && d._month <= 12);
		//静态数组,相当于放在全局
		static int mon[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (isleapyear(d._year))
		{
			if (d._month == 2) return 29;
		}
		return mon[d._month];
	}
	void Print();
	bool checkdate();
	//能写const就写const,这样const对象和非const对象都能调用
	//但并不是所有成员函数都能加->修改类的成员数据的不能加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;
	bool operator<=(const Date& d) const;
	//日期减日期
	int operator-(const Date& d) const;

	//无需赋值运算符重载,编译器自动生成
	//显式实现
	Date& operator+(int day) const;
	Date& operator-(int day) const;
	Date& operator+=(int day);
	Date& operator-=(int day);
	//day前置++
	Date& operator++();
	//day前置--
	Date& operator--();
};

.cpp文件

#include "Date.h"

//判断日期是否合法
bool Date::checkdate()
{
	if (_month > 12 || _month < 1 || _year < 1 || _day<1 || _day>getday(_year, _month))
	{
		return false;
	}
	return true;
}
//初始化(全缺省构造函数)
//Date::Date(int year=1,int month=1,int day=1)
//{
//	_year = year;
//	_month = month;
//	_day = day;
//
//	if (!check())
//	{
//		cout << "日期不合法!" << endl;
//	}
//}

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

//最后一个const允许类的自定义运算符重载在类外部实现
//重载==运算符
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);
}
//重载>运算符
bool Date::operator>(const Date& d) const
{
	if (_year > d._year)
		return true;
	else if (_year == d._year)
	{
		if (_month > d._month)
			return true;
		else if (_month == d._month)
		{
			if (_day > d._day)
				return true;
			else false;
		}
		else
			return false;
	}
	else
		return false;
}

bool Date::operator>=(const Date& d) const
{
	if (*this == d || *this > d) 
		return true;
	else 
		return false;
}

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

bool Date::operator<=(const Date& d) const
{
	if (*this == d || *this < d)
		return true;
	else
		return false;
}

Date& Date::operator++()
{
	int maxday = getday(_year, _month);
	if (_day + 1 <= maxday)
		_day++;
	else
	{
		_day = 1;
		if (_month + 1 <= 12)
			_month++;
		else
		{
			_month = 1;
			_year++;
		}
	}
	return *this;
}

Date& Date::operator--()
{
	if (_day - 1 >= 1)
		_day--;
	else
	{
		if (_month - 1 >= 1)
		{
			_month--;
			_day = getday(_year, _month);
		}
		else
		{
			_year--;
			_month = 12;
			_day = 31;
		}
	}
	return *this;
}
//使用了传引用
Date& Date::operator+=(int day)
{
	if (day < 0)
		return *this -= (-day);

	_day += day;
	int maxday = getday(_year, _month);
	while (_day > maxday)
	{
		_day -= maxday;
		_month++;
		if (_month == 13)
		{
			_month = 1;
			_year++;
		}
		maxday = getday(_year, _month);
	}

	return *this;
}

Date& Date::operator-=(int day)
{
	if (day < 0)
		return *this += (-day);

	_day -= day;

	while (_day <= 0)
	{
		_month--;
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		int maxday = getday(_year, _month);
		_day += maxday;
	}
	return *this;
}

//使用传值(拷贝的方式)
Date Date::operator+(int day) const
{
	Date temp = *this;
	temp += day;
	return temp;
}

Date Date::operator-(int day) const
{
	Date temp = *this;
	temp -= day;
	return temp;
}
//日期-日期
int Date::operator-(const Date& d) const
{
	Date max = *this;
	Date min = d;
	
	if (max < min)
	{
		Date temp = max;
		max = min;
		min = temp;
	}

	int cnt = 0;
	//暴力
	while (min < max)
	{
		cnt++;
		min+=1;
	}
	return cnt;
}

//不加引用是值传递,会重新拷贝一份,此时的out不再是使用者传递的对象->导致编译器无法正确识别和重载该运算符,
// 因为返回类型和参数类型都没有形成一个唯一的标识。
//加引用作用:避免对象的复制并且能够保证参数的唯一性
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year<<' '<< d._month <<' ' << d._day << endl;
	return out;//返回out支持链式操作
}

istream& operator >> (istream& in, Date& d)
{
	while (1)
	{
		cout << "请输入日期:" << endl;
		in >> d._year >> d._month >> d._day;

		if (!d.checkdate())
		{
			d.Print();
			cout << "输入不合法,请重新输入!" << endl;
		}
		else break;
	}
	return in;//支持链式读取
}

test.cpp

#include "Date.h"

void test1()
{
	Date d1(2024, 10, 12);
	Date d2(1900, 6, 9);
	d1.Print();
	d2.Print();
}

void test2()
{
	Date d1(2024, 10, 12);
	Date d3 = d1 - 10;
	d3.Print();
	d3 += 100;
	d3.Print();
}

void test3()
{
	Date d1(2024, 10, 12);
	Date d2(1900, 6, 9);
	bool result1 = (d1 == d2);
	cout << "d1和d2是否相等:" << result1 << endl;
	bool result2 = (d1 >= d2);
	cout << "d1是否大于等于d2:" << result2 << endl;
	bool result3 = (d1 <= d2);
	cout << "d1是否小于等于d2:" << result3 << endl;
}

void test4()
{
	Date d1(2024, 10, 12);
	Date d2(1900, 6, 9);
	int count = d1 - d2;
	cout << "d1和d2差" << count << "天" << endl;
}

void test5()
{
	Date d;
	cin >> d;
	cout <<"当前月份的天数:" << d.getday(d) << endl;
	cout << "2023年7月的天数:" << d.getday(2023, 7) << endl;
}

int main()
{
	test1();
	test2();
	test3();
	test4();
	test5();
	return 0;
}

六、const成员

const修饰类成员函数时,实际上修饰的是类成员函数的this指针。表明在该成员函数中不能对类的任何成员进行修改。

在这里插入图片描述

#include<iostream>
using namespace std;

class Date
{
private:
	int _year;
	int _month;
	int _day;

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

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

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

int main()
{
	//常量对象
	const Date d1(2023, 6, 9);
	//非常量对象
	Date d2(2024, 10, 15);

	d1.Print();
	d2.Print();
	//常量对象调用常量成员函数(d1调用Print() const函数)   (权限平移,正确)
	//常量对象调用非常量成员函数(d1调用Print()函数)		(权限放大,错误)
	//非常量对象调用常量成员函数 (d2调用Print() const函数) (权限缩小,正确)
	//非常量对象调用非常量成员函数 (d2调用Print()函数) (权限平移,正确)


	return 0;
}

总结

(1)权限只能平移或缩小,不能放大
(2)const成员函数内部和非const成员函数内部均可以调用其他常量成员函数或非常量成员函数
(3)成员函数内部不需要修改成员变量时,都可以加上const,方便常量对象调用。
(4)const成员函数中不能对类的任何成员进行修改。

七、取地址操作符重载

class Date
{
public:
	Date* operator&()
	{
		return this;
	}
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

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


结语

本篇博客我们学习了类的六个默认成员函数,重点介绍了构造函数析构函数拷贝构造函数赋值运算符重载

如果有什么建议或补充,亦或是错误,大家可以在评论区提出。

END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值