【C++类和对象(四)】手撕 Date 类:赋值运算符重载 + 日期计算

C++类和对象(四)

你知道为什么一个简单的 a = b 会导致程序崩溃吗?为什么有的类里 &obj 取到的地址不是真的地址?这两个看似基础的运算符,背后隐藏着资源管理、异常安全、甚至整个 C++ 设计哲学的秘密。这篇文章将带你揭开 operator= 和 operator& 的神秘面纱,顺便手撕一下Date类的代码 !ƪ(˘⌣˘)ʃ

文章目录

  • C++类和对象(四)
    • [1. 赋值运算符重载](#1. 赋值运算符重载)
      • [1.1 赋值运算符重载的特点:](#1.1 赋值运算符重载的特点:)
    • [2. 取地址运算符重载](#2. 取地址运算符重载)
      • [2.1 `const` 成员函数](#2.1 const 成员函数)
        • [2.1.1 语法与含义](#2.1.1 语法与含义)
        • [2.1.2 为什么需要 `const` 成员函数?](#2.1.2 为什么需要 const 成员函数?)
      • [2.2 取地址运算符重载](#2.2 取地址运算符重载)
    • [3. 日期类代码实现](#3. 日期类代码实现)
    • 结语

1. 赋值运算符重载

赋值运算符重载是 C++ 中一个特殊的成员函数,用于完成两个已经存在的对象之间的拷贝赋值。注意,它和拷贝构造函数有着本质区别:

  • 拷贝构造:用一个对象初始化另一个新创建的对象。
  • 赋值重载:将已有对象的值赋给另一个已经存在的对象。

当你写下 a = b 时,编译器会调用 a.operator=(b)。如果你没有显式定义,编译器会生成一个默认版本------逐成员拷贝(浅拷贝)。对于只包含内置类型的类(如 Date),这通常没问题。但一旦类管理了动态资源(如 Stack 中的指针),浅拷贝就会导致两个对象指向同一块内存,析构时引发双重释放或内存泄漏。


1.1 赋值运算符重载的特点:

先来看一段代码:՞˶・֊・˶՞

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

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	//传引用返回减少拷贝
	//d1 = d2;
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		//d1 = d2,把d2的值赋给了d1,应该返回d1的值,就是*this
		return *this;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2026, 3, 27);
	Date d2(d1);
	d1.Print();
	d2.Print();

	Date d3(2026, 3, 28);
	d1 = d3;
	d1.Print();
	// 需要注意这⾥是拷贝构造,不是赋值重载
	// 赋值重载完成两个已经存在的对象直接的拷贝赋值
	// ⽽拷贝构造⽤于⼀个对象拷贝初始化给另⼀个要创建的对象
	Date d4 = d1;
	d4.Print();

	return 0;
}

效果:


特点一:赋值运算符重载必须重载为成员函数,参数建议用 const 引用

cpp 复制代码
Date& operator=(const Date& d)

为什么必须是成员函数**(՞•Ꙫ•՞)ノ???**

赋值运算符=是 C++ 中少数几个必须作为非静态成员函数重载的运算符之一(与 []()-> 等类似)。如果试图定义为全局函数,编译器会报错。原因很简单:赋值操作天然与左操作数(即 this)紧密绑定,只有成员函数才能访问当前对象的私有成员。你可以理解为:赋值是"对象自己的事",只有它自己才能决定怎么把别人的值赋给自己。

为什么参数用 const 引用?

  • 避免拷贝 :如果参数写成 Date d,那么传参时会调用拷贝构造函数,产生一次额外的拷贝。对于大对象来说,这是不必要的开销。
  • 保护右操作数 :加 const 限定符表明赋值过程中不会修改右操作数,符合赋值操作的语义,同时也允许将常量对象(用const修饰的对象,一旦创建就不能被修改,只能读取)作为右操作数。

特点二:必须有返回值,且返回 *this 的引用

代码里这样写:

cpp 复制代码
return *this;

为什么要有返回值?

为了支持连续赋值,比如:

cpp 复制代码
d1 = d2 = d3;

它等价于 d1 = (d2 = d3)。如果 d2 = d3 没有返回值,d1 就不知道右边是什么,连续赋值便无法实现,编译就会报错。

为什么返回引用,而不是对象?

返回引用能避免一次拷贝。如果返回 Date 对象,那么 d2 = d3 结束后会生成一个临时对象 ,再把这个临时对象赋给 d1,多了一次拷贝构造和析构。返回 *this 的引用(即Date&),直接就是 d2 本身,效率更高,而且仍然支持连续赋值!


特点三:默认赋值运算符执行浅拷贝

如果你不写 operator=,编译器会自动帮你合成一个。它的行为是:

  • 对内置类型成员(如 intchar*)进行逐字节拷贝(浅拷贝)。
  • 对自定义类型成员,调用该成员自己的赋值运算符。

什么时候默认版本够用?

Date 类,所有成员都是 int,没有指向堆内存的指针,默认的浅拷贝就能正确工作。所以,即使我们删除上面代码中的 operator= 定义,程序依然完美运行。

什么时候默认版本不够用?

来看一个典型的 Stack 类:

cpp 复制代码
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc fail");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;

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

如果执行 s1 = s2,默认赋值运算符只会浅拷贝指针 _a,结果 s1._as2._a 指向同一块内存。当两个对象销毁时,同一块内存被释放两次,程序直接崩溃。这就是著名的浅拷贝陷阱。

因此,对于管理资源的类,我们必须自己实现深拷贝的赋值运算符:释放原有资源,重新分配内存,并复制数据。这也是"三法则"(有析构则应有拷贝构造和拷贝赋值)的由来。


特点四: 显式实现析构时,通常也需要显式实现赋值运算符

根据类成员的类型和资源管理情况,我们可以总结出以下规则:

  • Date 这样的类:成员变量全是内置类型,且没有指向外部资源,编译器生成的默认赋值运算符已经足够,因此不需要显式实现。
  • Stack 这样的类:虽然成员也都是内置类型,但 _a 指向了动态分配的内存。默认浅拷贝会导致资源被多个对象共享,必须自己实现深拷贝。
  • MyQueue 这样的类:内部主要包含自定义类型的成员(例如两个 Stack 对象)。编译器生成的默认赋值运算符会依次调用每个成员的赋值运算符,只要 Stack 的赋值运算符已经正确实现了深拷贝,MyQueue 就不需要再显式实现

敲黑板:

如果一个类显式实现了析构函数(释放资源),那么它几乎肯定需要显式实现拷贝构造函数和拷贝赋值运算符。

反过来,如果类没有资源管理,通常可以依赖编译器生成的默认版本。

这个技巧非常实用:看到 ~Stack(),立刻想到需要补上拷贝构造和赋值重载! ƪ(˘⌣˘)ʃ


2. 取地址运算符重载

2.1 const 成员函数

先看一段熟悉的 Date 类代码:

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

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//void Print(const Date* const this) const
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2026, 3, 28);
	d1.Print();

	const Date d2(2026, 3, 29);
	d2.Print();
	return 0;
}
2.1.1 语法与含义
  • 语法: 在成员函数的参数列表后面加上 const 关键字,就定义了一个 const 成员函数。
  • 作用: const修饰的是隐含的 this 指针,表明在该成员函数内部不能修改任何成员变量。也就是说,这个函数是"只读"的。

编译器会这样理解:

  • 普通成员函数: 隐含的 this 类型为 Date* const this(指针本身不可改,但指向的内容可改)。
  • const 成员函数: 隐含的 this 类型变为 const Date* const this(指针本身不可改,指向的内容也不可改)。
2.1.2 为什么需要 const 成员函数?

关键原因在于: const 对象只能调用 const 成员函数。

假设我们有一个 const Date 对象,如果 Print() 不是 const 成员函数,那么 d2.Print() 就会编译报错。因为编译器需要保证 const 对象不会被修改,而调用一个可能修改成员的非 const 成员函数是不允许的。

因此,对于那些只读取成员、不修改成员的函数,应该一律加上 const。这既是良好的设计习惯,也能让代码更通用。


2.2 取地址运算符重载

在 C++ 中,取地址运算符 & 用于获取对象的地址。你可能不知道,这个运算符其实也有重载版本,而且编译器默认就为我们生成了两个版本:一个用于普通对象,另一个用于 const 对象。绝大多数情况下,我们根本不需要去动它------除非你有一些非常特殊的需求,比如"禁止别人取我的地址"。

编译器会自动为每个类生成两个取地址运算符重载:

cpp 复制代码
// 普通对象的取地址
Date* operator&() 
{
    return this;
}

// const 对象的取地址
const Date* operator&() const 
{
    return this;
}

这两个函数很简单,就是返回 this 指针。第一个返回 Date*,第二个返回 const Date*,保证了 const 对象取到的地址也是 const 指针,防止通过该指针修改对象。

如果不想让别人取我的地址就可以这样写:

cpp 复制代码
Date* operator&()
{
	//return this;
	return nullptr;
}
const Date* operator&()const
{
	//return this;
	return nullptr; 
}

敲黑板:

  • 两个版本都必须提供,否则 const 对象无法取地址。
  • 返回类型必须是指针类型:普通版本返回 Date*const 版本返回 const Date*

嘿嘿,重载取地址运算符最致命的不是技术问题,而是社交问题------你的同事会困惑,甚至愤怒 ( ⩌ - ⩌ )

如果你写了一个类,重载了 operator& 返回 nullptr。然后你的同事写代码的时候用到了你的类中的取地址**╭(°A°`)╮** 。

同事花了两天时间调试,最后发现罪魁祸首是你重载了取地址运算符!你觉得他会怎么想?这种"惊喜"绝对会让人怀疑人生。所以一定要慎重使用取地址运算符重载!!


3. 日期类代码实现

补充一个小知识------友元函数: (以后会再详细讲解,这里只是浅浅提一下,帮助我们实现代码)

友元函数是类的"特殊朋友",它虽然不是这个类的成员函数,但被允许直接访问类的私有(private)和保护(protected)成员。
语法很简单: 在类定义中,加上 friend 关键字和函数声明,可以放在 publicprivateprotected 任何位置(通常放在开头,以便阅读)。

代码分了3个部分实现:
头文件 Date.h:

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

// Date 日期类
class Date
{
    // 声明友元函数,使全局的 << 和 >> 能访问私有成员
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	bool CheckDate() const; // 检查日期合法性
	Date(int year = 1900, int month = 1, int day = 1); // 构造函数(带默认值)
	void Print()const; // 打印日期

	// 静态成员函数:获取某年某月的天数
	static int GetMonthDay(int year,int month)
	{
		int MonthDay[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
        // 判断闰年:闰年2月返回29天
		if (month == 2 && ( (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) ))
		{
			return 29;
		}
		return MonthDay[month];
	}

    // 比较运算符重载
	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;

    // 算术运算符重载
	Date& operator+=(int day);
	Date operator+(int day) const;
	Date operator-(int day) const; // 日期减天数
	Date& operator-=(int day);

	// 后置自增 d++ (int参数用于区分前置后置,无实际意义)
	Date operator++(int);
	// 前置自增 ++d
	Date& operator++();
	// 后置自减 d--
	Date operator--(int);
	// 前置自减 --d
	Date& operator--();

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

private:
	int _year;  // 年
	int _month; // 月
	int _day;   // 日
};
// 全局运算符重载函数声明
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);

代码实现部分 Date.cpp:

cpp 复制代码
#include"Date.h"

// 检查日期合法性(年、月、日是否有效)
bool Date::CheckDate() const
{
	if (_month < 1 || _month>12 || _day<1 
		|| _day>GetMonthDay(_year, _month))
	{
		return false; // 非法日期
	}
	else {
		return true; // 合法日期
	}
}

// 构造函数:初始化并验证日期
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
	if (!CheckDate()) // 若日期非法,则提示
	{
		cout << "日期非法"<<endl;
		Print();
	}
}

// 打印日期
void Date::Print() const
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

// 比较运算符重载:依次比较年、月、日
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)
		{
			return _day < d._day;
		}
	}
	return false;
}
// 其他比较运算符基于 operator<和 operator==实现
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);
}
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);
}

// 重载 +=:当前日期加上天数,处理跨月、跨年
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) // 月份满12进位到1月,年份加1
		{
			_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;
		if (_month == 0) // 月份借位到上年12月
		{
			_month = 12;
			--_year;
		}
		_day += GetMonthDay(_year, _month); // 加上上个月的天数
	}
	return *this;
}
// 重载 - (日期-天数):不改变原对象
Date Date::operator-(int day)const
{
	Date tmp = *this;
	tmp -= day;
	return tmp;
}

// 后置自增 d++:返回自增前的值
Date Date::operator++(int)
{
	Date tmp = *this; // 保存原值
	*this += 1; // 当前对象加1天
	return tmp; // 返回旧值
}
// 前置自增 ++d:返回自增后的值
Date& Date::operator++()
{
	*this += 1;
	return *this;
}
// 后置自减 d--
Date Date::operator--(int)
{
	Date tmp = *this;
	*this -= 1;
	return tmp;
}
// 前置自减 --d
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

// 重载 - (日期-日期):计算两个日期间的天数差
int Date::operator-(const Date& d) const
{
	int flag = 1; // 结果符号,1表示this>=d,-1表示this<d
	Date max = *this, min = d;
	if (*this < d) // 确保max是较晚的日期
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while (min != max) // 循环,让较早的日期逐天加到等于较晚的日期
	{
		++min;
		++n;
	}
	return n * flag; // 返回带符号的天数差
}

// 重载流插入运算符 <<
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}
// 重载流提取运算符 >>,循环直到输入合法日期
istream& operator>>(istream& in, Date& d)
{
	while (1)
	{
		cout << "请输入 年 月 日 >";
		in >> d._year >> d._month >> d._day;
		if (!d.CheckDate())
		{
			cout << "日期非法"<< endl;
			d.Print();
			cout << "请重新输入" << endl;
		}
		else {
			break; // 输入合法,退出循环
		}
	}
	return in;
}

测试部分 DateTest.cpp:

cpp 复制代码
#include"Date.h"

// 测试 + 和 += 运算符
void Test01()
{
	Date d1(2026, 3, 25);
	Date d2 = d1 + 365; // d2是新对象,d1不变
	d1.Print();
	d2.Print();

	d1 += 365; // d1自身改变
	d1.Print();

	Date d3(2026, 3, 28);
	d3 += -1; // 加负数,相当于减一天
	d3.Print();// 预期输出: 2026/3/27
}

// 测试 - 和 -= 运算符
void Test02()
{
	Date d1(2026, 3, 28);
	d1 -= 30; // d1自身减去30天
	d1.Print();

	Date d3(2026, 3, 28);
	Date d2 = d3 - 30; // d2是新对象
	d2.Print();

	Date d4(2026, 3, 28);
	d4 -= -1; // 减负数,相当于加一天
	d4.Print();// 预期输出: 2026/3/29
}
// 测试前置和后置 ++
void Test03()
{
	Date d1(2026, 3, 28);
	Date ret1 = d1++; // 后置++,ret1得到旧值
	ret1.Print();// 输出: 2026/3/28
	d1.Print();  // 输出: 2026/3/29

	Date d2(2026, 3, 28);
	Date ret2 = ++d2; // 前置++,ret2得到新值
	ret2.Print();// 输出: 2026/3/29
	d2.Print();  // 输出: 2026/3/29
}
// 测试前置和后置 --
void Test04()
{
	Date d1(2026, 3, 28);
	Date ret1 = d1--; // 后置--
	ret1.Print();// 2026/3/28
	d1.Print();  // 2026/3/27

	Date d2(2026, 3, 28);
	Date ret2 = --d2; // 前置--
	ret2.Print();// 2026/3/27
	d2.Print();  // 2026/3/27
}
// 测试 日期-日期 运算符
void Test05()
{
	Date d1(2026, 3, 28);
	Date d2(2026, 3, 1);
	cout << d1 - d2 << endl; // 预期输出正数天数差

	Date d3(2026, 3, 28);
	Date d4(2026, 4, 28);
	cout << d3 - d4 << endl; // 预期输出负数天数差
}
// 测试 >> 和 << 运算符
void Test06()
{
	Date d1;
	Date d2;
	cin >> d1 >> d2; // 从控制台输入两个日期
	cout << d1 << d2; // 输出两个日期
	cout << d1 - d2 << endl; // 输出天数差
}

int main()
{
	//Test01(); // 测试 + +=
	//Test02(); // 测试 - -=
	//Test03(); // 测试 ++
	//Test04(); // 测试 --
	//Test05(); // 测试 日期-日期
	Test06(); // 测试 输入输出
	return 0;
}

结语

今天的内容到这里就结束了,希望你能有所收获~

代码无bug,学习不迷路,我们下篇再见!(•̀ᴗ•́)و

相关推荐
荒川之神19 小时前
拉链表概念与基本设计
java·开发语言·数据库
chushiyunen19 小时前
python中的@Property和@Setter
java·开发语言·python
小樱花的樱花19 小时前
C++ new和delete用法详解
linux·开发语言·c++
froginwe1119 小时前
C 运算符
开发语言
fengfuyao98520 小时前
低数据极限下模型预测控制的非线性动力学的稀疏识别 MATLAB实现
开发语言·matlab
摇滚侠20 小时前
搭建前端开发环境 安装 nodejs 设置淘宝镜像 最简化最标准版本 不使用 NVM NVM 高版本无法安装低版本 nodejs
java·开发语言·node.js
t1987512820 小时前
MATLAB十字路口车辆通行情况模拟系统
开发语言·matlab
yyk的萌21 小时前
AI 应用开发工程师基础学习计划
开发语言·python·学习·ai·lua
Amumu1213821 小时前
Js:正则表达式(一)
开发语言·javascript·正则表达式
努力的章鱼bro1 天前
操作系统-FileSystem
c++·操作系统·risc-v·filesystem