【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,学习不迷路,我们下篇再见!(•̀ᴗ•́)و

相关推荐
Dxy12393102165 分钟前
Python使用XPath定位元素:动态计算与函数调用
开发语言·python
小柯博客9 分钟前
STM32MP2安全启动技术深度解析
c语言·c++·stm32·嵌入式硬件·安全·开源·github
cpp_250117 分钟前
P1832 A+B Problem(再升级)
数据结构·c++·算法·动态规划·题解·洛谷·背包dp
Evand J18 分钟前
【MATLAB代码介绍】三种CT模型的IMM(交互式多模型)对目标高精度定位
开发语言·matlab·ct·imm·交互式多模型·多模型·转弯
AC赳赳老秦21 分钟前
OpenClaw权限管理实操:团队共享Agent,设置操作权限,保障数据安全
服务器·开发语言·前端·javascript·excel·deepseek·openclaw
geovindu33 分钟前
go: Proxy Pattern
开发语言·后端·设计模式·golang·代理模式
langsiming38 分钟前
【无标题】
java·开发语言·数据库
꧁细听勿语情꧂39 分钟前
合并两个有序表、判断链表的回文结构、相交链表、环的链表一和二
c语言·开发语言·数据结构·算法
Rust语言中文社区41 分钟前
【Rust日报】2026-04-24 Vizia 0.4 发布——纯 Rust 声明式响应式 GUI 框架
开发语言·后端·rust
结衣结衣.43 分钟前
手把手教你实现文档搜索引擎
linux·c++·搜索引擎·开源·c++11