C++ 类和对象

一、类的定义

1.1 类的定义格式

class 是用于定义类的关键字,以 Stack 类为例,类名紧跟关键字,大括号内部为类主体,书写时需要注意类定义结束的分号不可省略。

大括号内的内容统称为类成员,其中变量称作成员变量或属性,函数称作成员函数或方法;

为区分成员变量与局部变量,工程中普遍存在命名惯例,会为成员变量添加下划线前缀、后缀或是以 m 开头,该规范不属于 C++ 语法强制要求,具体遵循对应项目或公司编码标准;

C++ 拓展了 struct 的能力,既兼容 C 语言仅存放数据的用法,也可以像 class 一样定义成员函数,二者都能实现类的定义,但日常开发中更推荐使用 class;

另外直接在类内部完成实现的成员函数,会被编译器默认为内联函数。

cpp 复制代码
#include<iostream>

using namespace std;

class stack
{
	void Push(int x)
	{ 
		//...
	}
	void pop()
	{
		//...
	}
	int top()
	{
		//...
	}

	int* a;
	int top;
	int capacity;
};

1.2 访问限定符

访问限定符是 C++ 实现面向对象封装的重要手段,我们可以借助类把对象的属性和方法整合在一起,再通过不同的访问权限,选择性向外部开放可用接口,提升代码的安全性与规范性。

C++ 共有publicprotectedprivate三种访问权限:

public修饰的成员能在类外部直接访问;

protectedprivate修饰的成员都无法在类外直接访问,二者的差异要到继承场景中才会体现出来。

访问权限的生效范围从该限定符声明处开始,一直延续到下一个访问限定符出现为止,若后续没有新的限定符,权限范围会持续到类定义结束的大括号处。

还要注意默认权限的区别:用class定义类时,未显式添加访问限定符的成员默认是private权限;用struct定义类时,成员默认权限为public

在日常开发的编码规范里,我们通常会把成员变量设置为privateprotected做隐藏保护,只把需要对外提供调用能力的成员函数设置为public,以此落实封装的设计思想。

cpp 复制代码
#include<iostream>

using namespace std;

class Stack
{
public:
	void Push(int x)
	{}
	void Pop()
	{}

	int Top()
	{
		return 0;
	}
private:
	int* a;
	int top;
	int capacity;
};

int main()
{
	Stack st;
	st.Pop();
	st.Push(1);
	//st.a;

	return 0;
}

1.3 类域

类会单独开辟专属的作用域,类内部所有成员均归属于该类域,若在类体外实现成员函数,必须借助::作用域解析运算符标注所属类名,以此明确该函数归属的类域;

类域会改变编译器标识符查找逻辑,以 Stack 类的 Init 函数为例,若定义函数时未标注 Stack::,编译器会将其识别为普通全局函数,无法检索到 array 这类类内成员从而触发编译报错,添加类域标识后编译器便知晓这是类成员函数,在当前作用域找不到对应变量时会自动进入类域完成查找。

二、实例化

2.1 实例化概念

实例化指依据自定义类的类型,在物理内存中创建实体对象的过程;类本身只是对对象的抽象描述,相当于一套数据模型,仅声明所需成员变量,不会分配任何内存空间,只有完成实例化生成对象时,系统才会为成员变量分配专属存储空间;同一个类能够实例化出多个相互独立的对象,每一个对象都会占用独立物理内存用于存放自身数据,我们可以将类类比建筑图纸,图纸仅规划房屋结构与功能,不存在实体建筑、无法承载实际内容,依托图纸建造出实体房屋才能投入使用,对应到代码中,类仅作为模板无法存储数据,经过实例化后的对象才具备内存空间、可保存各类成员数据。

cpp 复制代码
struct Date
{
public:
	void Init(int year, int month, int day);

	int _year; // year_ m_year
	int _month;
	int _day;
};

void Date::Init(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

int main()
{
    //实例化
	Date d2;
	Date d3;

	return 0;
}

2.2 对象的大小

类实例化后的对象仅存储自身独立的成员变量,不会保存成员函数相关数据;

成员函数编译后为指令集合,统一存放于程序代码段,若为每个对象单独存储函数指针会造成大量冗余开销,例如同一个类创建百个对象,便会重复存储百份完全一致的函数地址,极大浪费内存空间;

普通非多态场景下编译器在编译链接阶段即可确定成员函数的固定地址,调用时直接通过 call 指令跳转对应地址,无需在对象内部保存函数指针,仅动态多态场景需要在对象中存储函数地址。

类的内存对齐规则与C语言中学的结构体的内存对其规则一模一样:

  • 第一个成员在与结构体偏移量为 0 的地址处。
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  • 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
  • VS 中默认的对齐数为 8
  • 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

为什么要内存对齐呢?

CPU 读取内存不是按单个字节随便读的,而是整块批量读取(比如一次读 4 字节、8 字节),硬件总线只支持从对齐地址开始读取数据。

假设 CPU 一次能读 4 字节数据,如果 int 变量刚好放在 4 的倍数地址,CPU 一次总线请求就能完整取出这个 int;要是变量卡在 2、3 这种非对齐地址,一次读不全,就得发起两次内存读取,再拼接数据,读写速度直接减半。内存对齐就是把变量规整到对应整数倍地址,保证一次读取到位,大幅提升程序运行效率。

不少架构的 CPU(比如 ARM)根本不支持直接访问非对齐内存地址,一旦数据错位,程序会直接崩溃、触发硬件异常;x86 虽然能兼容非对齐访问,但会产生巨大性能损耗。对齐机制是为了代码能跨硬件平台稳定运行,避免报错。

CPU、内存控制器的电路在设计时,如果要求支持任意地址读取,电路逻辑会变得极度复杂、成本更高;规定对齐规则后,硬件只需要处理固定间隔的地址,芯片设计难度、制造成本都会下降。

所以,内存对齐本质是牺牲少量内存空间(填充对齐空白),换取更快的读写速度、更好的硬件兼容性,属于以空间换时间、适配硬件底层的设计。

如果类中没有成员变量,实例化的时候还是会分配一个字节,纯粹是为了占位标识对象存在。

2.3 this指针

同一个类的全部对象共用一套成员函数代码,函数本身分不清现在是哪个对象在调用自己,C++ 就用隐式的 this 指针解决这个区分问题。

编译代码时,编译器会偷偷给每个成员函数的参数最前面加上一个当前类的 const 指针,也就是 this。拿 Date 类的 Init 函数举例,我们写出来的函数看着只有年月日三个参数,底层真实原型其实是void Init(Date* const this, int year, int month, int day)

平时我们在函数里直接写_year = year修改成员变量,底层会自动转换成this->_year = year,本质全靠 this 指针找到当前调用函数的对象数据。

语法上我们不能手动在传参、函数形参位置写出 this,这一步完全交给编译器自动处理,不过在函数内部写代码时,可以手动写上 this-> 来访问成员。

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	// void Init(Date* const this, int year, int month, int day)
	void Init(int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

	// void Print(Date* const this)
	void Print()
	{
		cout << this->_year << "/" << this->_month << "/" << _day << endl;
	}

private:
	// 这里只是声明,没有开空间
	int _year;
	int _month;
	int _day;
};

int main()
{
	// Date类实例化出对象d1和d2
	Date d1;
	Date d2;

	d1.Init(2024, 3, 31);
	d1.Print();

	d2.Init(2024, 7, 5);
	d2.Print();
	return 0;
}

2.4 C语言与C++实现栈的对比

面向对象有封装、继承、多态三大核心特性,对比 C 语言、C++ 两种栈的实现思路,就能直观理解封装的含义,二者底层存储、运算逻辑完全一致,只是代码组织与使用方式差别很大。

最大改动来自 C++ 的封装设计:它把栈的数据、操作函数全部收纳进同一个类,搭配访问权限控制外部访问,外部无法随便直接篡改内部数据。封装本质就是对数据做规范化管控,杜绝非法读写引发 bug,这也是 C++ 面向对象最关键的优势。

同时 C++ 提供大量简化语法,大幅降低使用成本:支持函数默认参数,不用每次调用都传全参数;成员函数会自动隐式传递 this 指针,不用像 C 语言那样手动传入栈结构体地址;定义类型时也无需 typedef,直接使用类名即可,写法更简洁。

三、类的默认成员函数

默认成员函数指我们没有手动编写、编译器会自动生成的类内函数,在不自定义任何成员函数的情况下,编译器会自动生成六个默认成员函数,其中前四个是学习重点,后两个取地址重载实用性很低,简单了解即可;C++11 标准又新增移动构造与移动赋值两个默认成员函数,相关内容后续再展开讲解。

学习这类函数可以抓住两个核心思路,一是搞清楚编译器自动生成的函数具备什么样的行为,能否满足我们的业务需求,二是如果默认生成的逻辑不符合预期,我们该如何手动重写自定义版本,顺着这两个方向学习,就能理清复杂的默认成员函数相关知识点。

3.1 构造函数

构造函数是类里一种特殊成员函数,它不负责给对象分配内存,核心作用是在创建对象时自动完成成员初始化,替代了 C 语言里手动写的 Init 初始化函数,不用我们手动调用,使用更省心。

它有 7 个关键特性:

  1. 函数名字必须和类名完全一致;
  2. 没有任何返回值,不用写 void 也不用写其他类型,这是语法硬性规定;
  3. 只要创建实例化对象,程序会自动匹配并调用对应的构造函数;
  4. 支持函数重载,可以写多个参数列表不同的构造函数;
  5. 若我们完全不手动写构造函数,编译器会自动生成一个无参默认构造;只要我们手动定义了任意构造函数,编译器就不再自动生成;
  6. 无参构造、全缺省参数构造、编译器自动生成的无参构造,这三类都属于默认构造函数,满足 "不传实参就能直接调用" 的条件,但三者有且仅有一个存在,无参和全缺省构造一起写会出现调用歧义。默认构造参数调用时不能加括号,否则会和函数声明看起来一样。
  7. 编译器自动生成的构造对内置类型(int、char、指针等)不会做可靠初始化,变量值随机;如果类内包含自定义类型成员,会自动调用该成员的默认构造完成初始化,若这个自定义类没有默认构造,代码直接报错,这类场景需要借助初始化列表处理。

补充区分:C++ 的数据分为内置原生类型与我们自己用 class/struct 定义的自定义类型,二者在默认构造初始化时处理逻辑不一样。

cpp 复制代码
#include<iostream>
using namespace std;

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

	// 3.全缺省构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_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.Print();

	Date d2(2024, 7, 10);
	d2.Print();

	Date d3(2024);
	d3.Print();

	return 0;
}

3.2 析构函数

析构函数和构造函数作用正好相反,它不负责销毁对象本身(局部对象出作用域时栈空间会自动回收),核心作用是在对象销毁前自动清理、释放对象占用的堆内存、文件句柄这类资源,相当于之前栈代码里手动写的 Destroy 销毁函数;像只存普通数字的 Date 类没有额外资源,就不需要自定义析构函数。

析构函数核心特点:

  1. 函数名格式固定,在类名前面加波浪号~
  2. 没有任何参数、也没有返回值,不用标注 void;
  3. 一个类只能拥有一个析构函数,不手动编写的话,编译器会自动生成默认析构;
  4. 只要对象生命周期走到尽头,程序会自动调用析构,无需手动执行;
  5. 编译器自动生成的默认析构不会处理 int、char 这类内置类型成员,但会自动调用类内自定义类型成员的析构函数;
  6. 就算我们手动写了析构函数,类里的自定义类型成员依旧会自动执行自身的析构逻辑(也就是无论什么情况,对于自定义类型成员都会自动调用它的析构函数);
  7. 编写取舍规则:类内没有向堆申请资源时,直接用编译器默认生成的析构即可;如果存在动态内存、文件等资源申请,必须手动实现析构释放资源,否则会发生内存泄漏;
  8. 同一作用域创建多个局部对象,销毁顺序遵循后创建先销毁的规则。
cpp 复制代码
#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ...

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

3.3 拷贝构造

拷贝构造是一种特殊的构造函数,判定标准很简单:第一个参数是当前类类型的引用,其余所有参数都带有默认值,它本身也是普通构造函数的重载版本。拷贝构造有一条硬性语法要求,第一个参数只能用引用传递,如果写成值传递,调用时会持续触发自身拷贝逻辑,造成无限递归,编译器会直接报错;它也可以设置多个参数,但除第一个引用参数之外,剩下的参数都必须配置缺省值。在 C++ 规则里,只要出现自定义类对象的拷贝行为,就一定会调用拷贝构造,常见场景包括函数按值传递对象、函数按值返回对象,这两种情况都会自动走拷贝构造逻辑。

如果我们没有手动写出拷贝构造函数,编译器会自动生成一份默认拷贝构造,这份默认实现执行逐字节的浅拷贝逻辑,针对 int、char 这类内置类型成员会直接完整复制内存数据,遇到类内嵌套的自定义类型成员时,则会自动调用该成员自身的拷贝构造完成复制。我们可以根据类的资源占用情况判断是否需要自己实现拷贝构造,像 Date 类只存放普通内置变量、没有动态堆资源,或是 MyQueue 这类内部成员为其他自定义类的结构,依靠编译器默认生成的拷贝构造就可以正常工作,不用手动编写;但类似 Stack 这种内部存在指针指向堆内存的类,默认浅拷贝只会复制指针地址,新旧对象共享同一块堆空间,析构阶段会重复释放内存引发程序崩溃,这时就需要手动实现深拷贝,为新对象单独开辟堆内存并复制对应资源数据,这里有一个实用判断技巧:只要一个类手写了析构函数用于释放堆资源,就必须配套实现拷贝构造,没有资源管理需求则不需要。

另外我们需要区分值返回和引用返回带来的拷贝差异,函数采用值返回对象时会生成临时对象,全程调用拷贝构造产生额外性能开销;采用引用返回不会生成副本,只是返回原对象的别名,能省去拷贝步骤提升效率,但引用返回存在严格使用限制,绝对不能返回函数内部创建的局部对象,函数执行完毕后局部对象会直接销毁,此时返回的引用会变成非法野引用,风险等同于野指针,只有确定返回对象在函数结束后依旧存在,才适合使用引用返回优化代码。

cpp 复制代码
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	// st2(st1)
	Stack(const Stack& st)
	{
		cout << "Stack(const Stack& st)" << endl;

		// 需要对_a指向资源创建同样大的资源再拷贝值
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;

	}
private:
	STDataType* _a;
	int _capacity;
	int _top;
};

int main()
{
	Stack st1;
	//下面两种方式均为拷贝构造
	Stack st2 = st1;
	Stack st3(st1);
	return 0;
}

四、赋值运算符重载

4.1 运算符重载

4.1.1运算符重载基础概念

当我们自定义类的对象想要使用加减、等于这类运算符时,C++ 提供运算符重载机制,让我们自定义运算符在类身上的行为。只要类对象执行运算符操作,编译器就会自动去找对应的重载函数,找不到就直接编译报错。

运算符重载本质是一类特殊函数,函数名固定格式为operator+运算符符号,和普通函数一样支持返回值、参数列表与函数体。 参数数量遵循运算符操作数规则:一元运算符只需要 1 个参数,二元运算符需要 2 个参数;二元运算符会把左边参与运算的对象传给第一个参数,右边对象传给第二个参数。 如果把重载函数写成类的成员函数,左边运算对象会自动交给隐式的this指针,因此成员形式的运算符重载,参数个数会比运算符操作数少一个。 另外重载不会改变运算符原本的优先级、左右结合顺序,和内置类型运算规则保持一致。

4.1.2 运算符重载强制约束规则

  1. 不能创造语法里不存在的全新运算符,比如不能写operator@这种未定义符号。
  2. 有 5 个运算符完全禁止重载,分别是 ..*::sizeof?:,属于笔试选择题高频考点,需要牢记。
  3. 重载函数参数必须至少包含一个自定义类类型,不允许修改 int、double 等原生内置类型的运算符逻辑,比如不能重载两个 int 相加的 operator+。
  4. 只重载贴合业务逻辑的运算符,无实际作用的重载只会增加代码负担,例如日期类重载相减operator-可以计算日期间隔,有实际意义;重载加法operator+没有业务价值,不建议实现。

4.1.3 特殊运算符重载处理方案

  1. 自增运算符++区分前置、后置 前置 ++ 和后置 ++ 都对应函数名operator++,编译器无法自动区分,语法规定后置 ++ 重载函数额外增加一个 int 类型形参,依靠函数重载区分两种写法。
  2. 流运算符<<输出、>>输入不能作为类成员函数;成员函数第一个参数默认是 this 指针,会占据运算符左侧位置,调用时会变成对象<<cout,违背日常书写习惯。 正确做法是将operator<<operator>>实现为全局函数,把 ostream/istream 流对象放在第一个参数位置,自定义类对象作为第二个参数,就能写出符合阅读习惯的cout << 对象

4.2 赋值运算符重载

赋值运算符重载是编译器自动生成的默认成员函数,负责对两个已经存在的对象完成拷贝赋值,要区分开它和拷贝构造的使用场景:拷贝构造是用已有对象初始化一个全新待创建对象,赋值重载则针对两个已经创建完毕的对象完成赋值操作。

4.2.1 赋值运算符重载核心规则

  1. 函数形式限制 赋值运算符重载只能作为类的成员函数,不能写成全局函数;参数推荐使用const修饰的本类引用,若采用值传递传参,会额外触发对象拷贝,产生多余性能开销。
  2. 返回值规范 必须设置返回值,最优写法为返回当前类引用。引用返回可以避免生成临时对象、提升效率,同时只有返回自身引用,才能支持a=b=c这种连续赋值语法。
  3. 默认生成逻辑 如果我们没有手动实现赋值重载,编译器会自动生成默认版本。默认赋值重载执行浅拷贝逻辑:内置类型成员逐字节复制,自定义类型成员会自动调用自身的赋值重载完成赋值。

4.2.2 什么时候需要手动实现深拷贝赋值重载

  1. 无需手动实现:类中只有 int、char 等内置类型,没有堆动态资源(如 Date 类);或是类内成员为其他自定义类型(如 MyQueue 内含 Stack 成员),默认重载会自动调用内部类型的赋值逻辑。
  2. 必须手动实现:类内存在指针指向堆内存资源(如 Stack 类),默认浅拷贝只会复制指针地址,多个对象共享同一块堆空间,析构时重复释放内存会造成程序崩溃,需要手写深拷贝单独分配资源。

快速判断小技巧:只要一个类手动编写了析构函数用于释放堆资源,就必须配套手写拷贝构造和赋值运算符重载;无动态资源申请与释放的类,直接使用编译器默认生成的版本即可。

五、实现一个日期类Date

为了实践以上知识,我们着手实现一个日期类。

首先,定义出一个日期类以及我们想要实现的运算符重载函数。由于我们在实现这个类的过程中肯定会经常使用打印日期和获取某一月份总天数的功能,就先把它们实现一下:

cpp 复制代码
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1);
	void Print();
	int GetMonthDay(int year, int month)
	{
		assert(month >0 && month < 13);
		static int monthDayArray[13] = { -1, 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;
		return monthDayArray[month];
	}

	Date operator-(int day);
	Date operator-=(int day);
	Date operator+(int dat);
	Date operator+=(int day);

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

private:
	int _year;
	int _month;
	int _day;
};
cpp 复制代码
Date :: Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

5.1 加减运算符重载的实现

首先实现+=运算符的重载。+=是在当前对象的成员变量上直接加,不需要拷贝操作。这里的+=是一个进位逻辑:先让_day++,如果_day的值超过了当前月份的总天数,就进一位(让_month++),然后判断_month是否到了13,如果_month到了13就再进一位(让_year++),一直重复这个逻辑到_day的值合法为止。

cpp 复制代码
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;
}

+的运算符重载可以利用刚才已经写好的+=来实现:用拷贝构造来构造一个对象tmp,然后对它用+=,返回这个tmp就好了。(此处不能返回引用,因为tmp是一个临时变量,一旦返回引用就成了野引用)

cpp 复制代码
Date Date::operator+(int day)
{
	Date tmp = *this;
	tmp += day;
	return tmp;
}

而-=运算符的重载则是"借位"的逻辑:先让_day减去指定天数,如果减到零或负数,就进入循环:

此处循环中要先处理月份的逻辑:先让月份--,如果减到零就让年份--,月份回到12月;然后再让天数加上这个月份的总天数(借位),重复循环一直到天数合法。

cpp 复制代码
Date Date::operator-=(int day)
{
	_day -= day;
	while (_day <= 0)
	{
		//此处一定要先处理月的逻辑,因为月可能会减到零
		--_month;
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		_day += GetMonthDay(_year, (_month));
	}
	return *this;
}

-运算符的重载,也可以复用-=:

cpp 复制代码
Date Date::operator-(int day)
{
	Date tmp = *this;
	tmp -= day;
	return tmp;
}

5.2 比较运算符重载的实现

先实现>运算符的重载:

cpp 复制代码
bool Date::operator<(const Date& d)
{
	if (_year < d._year)
	{
		return true;
	}
	else if (_year == d._year)
	{
		if (_month < d._month)
		{
			return true;
		}
		else if (_month == d._month)
		{
			return _day < d._day;
		}
	}
	return false;
}

再实现==运算符的重载:

cpp 复制代码
bool Date::operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

其他比较运算符重载的实现,都可以复用写好的这两个:

cpp 复制代码
bool Date :: operator<=(const Date& d)
{
	return *this == d || *this < d;
}

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

5.3 单目运算符的重载实现

注意区分前置和后置的区别

cpp 复制代码
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

//后置++
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;
}

5.3 日期相减

复用之前写过的函数,先比较一下两个日期,让小的那个日期不断++,一直加到与大的日期相等,算一下中间加了多少次。

cpp 复制代码
int Date::operator- (const Date& d)
{
	int flag = 1;
	Date max = *this;
	Date min = d;

	if (min > max)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}

	return n * flag;
}

5.4 流运算符重载

为了符合日常书写逻辑,要把流运算符重载为全局函数。

流输入运算符要加入一个处理非法输入的逻辑。

cpp 复制代码
std::ostream& operator<<(std::ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << std::endl;
	return out;
}

std::istream& operator>>(std::istream& in, Date& d)
{
	while (1)
	{
		std::cout << "请依次输入年月日:>";
		in >> d._year >> d._month >> d._day;
		if (!d.CheckDate())
		{
			std::cout << "输入日期非法:";
			d.Print();
			std::cout << "请重新输入" << std::endl;
		}
		else
		{
			break;
		}
	}
	return in;
}

END

相关推荐
xxie1237941 小时前
Python装饰器与语法糖
开发语言·python
郑州光合科技余经理2 小时前
海外外卖平台源码改造实战——多语言核心代码实现
java·开发语言·前端·后端·mysql·架构·php
凯瑟琳.奥古斯特2 小时前
力扣1012数位DP解法详解
开发语言·c++·算法·leetcode·职场和发展
Railshiqian2 小时前
UserPickerActivity 内部逻辑分析
开发语言·python
喜欢打篮球的普通人2 小时前
Trition程序编写:从“Hello CUDA“到“Hello Triton“:向量加法背后的编译黑魔法
开发语言·后端·rust
code_std2 小时前
java WebSocket 使用
java·开发语言·websocket
gihigo19982 小时前
FastSLAM2.0(精度优于1.0)MATLAB 实现
开发语言·matlab
Yolo566Q2 小时前
Noah-MP陆面过程模型建模方法与站点、区域模拟实践技术应用
开发语言·python
落叶-IT2 小时前
Java Scanner 类精讲:控制台交互
java·开发语言
chh5632 小时前
C++--string
java·开发语言·网络·c++·学习