实战:日期时间类(运算符重载+格式化)
日期时间处理,是每个C++程序员迟早要面对的硬骨头。我记得刚入行那会儿,接手一个老项目,里面日期计算全是手写的,各种闰年判断、月份天数表散落在代码各个角落。改一个bug能牵扯出三个新bug,那叫一个酸爽。
今天我们就来手撸一个日期时间类,把运算符重载和格式化输出这两个核心技能一次性练透。你想想看,如果能写出date + days、date1 - date2这种自然语法,代码读起来得多舒服?
一、整体设计思路
先搭个架子。我们需要一个DateTime类,包含年、月、日、时、分、秒。核心功能分三块:
- 基本运算:加减天数、比较大小、计算差值
- 格式化输出:支持
YYYY-MM-DD HH:MM:SS等常见格式 - 合法性校验:自动处理闰年、月份天数、进位借位
下面这张图能帮你快速理解整个类的结构:
二、数据成员与基础校验
先定义成员变量。我习惯用int存储各个分量,简单直接:
class DateTime {
private:
int year_;
int month_; // 1-12
int day_; // 1-31
int hour_; // 0-23
int minute_; // 0-59
int second_; // 0-59
// 月份天数表(非闰年)
static const int daysInMonth_[12];
// 月份累计天数表(用于快速计算)
static const int cumDays_[12];
};
这里有个细节——月份天数表。我见过不少新手直接写死31,28,31,30...,然后到处判断闰年。更好的做法是把闰年逻辑封装成一个函数:
bool DateTime::isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
int DateTime::daysInMonth(int year, int month) {
if (month < 1 || month > 12) return 0;
int days = daysInMonth_[month - 1];
if (month == 2 && isLeapYear(year)) days += 1;
return days;
}
static const成员,所有实例共享一份数据,省内存。初始化放在源文件里:const int DateTime::daysInMonth_[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
三、核心算法:日期 ↔ 天数互转
为什么要转天数?因为日期加减本质上就是整数加减。把日期转成从某个基准点(比如公元1年1月1日)开始的天数,加减完再转回来,逻辑就清晰了。
我曾经在一个金融项目中用过这个方案,处理跨年、跨世纪的计算,稳得很。
// 日期 → 天数
int DateTime::toDays(int year, int month, int day) {
// 先算整年的天数
int y = year - 1;
int days = y * 365 + y / 4 - y / 100 + y / 400;
// 加上当年已过月份的天数
days += cumDays_[month - 1];
if (month > 2 && isLeapYear(year)) days += 1;
// 加上当月天数
days += day;
return days;
}
// 天数 → 日期
void DateTime::fromDays(int days, int& year, int& month, int& day) {
// 先估算年份
year = days / 366; // 粗略估计
while (days >= toDays(year + 1, 1, 1)) year++;
// 计算月份和日期
int leap = isLeapYear(year) ? 1 : 0;
int remaining = days - toDays(year, 1, 1) + 1;
for (month = 1; month <= 12; month++) {
int dim = daysInMonth(year, month);
if (remaining <= dim) break;
remaining -= dim;
}
day = remaining;
}
fromDays是简化版,实际生产环境建议用查表法或二分查找优化。年份估算循环在极端情况下(比如天数很大)可能较慢。不过对于教学演示,这个版本已经够用了。
四、运算符重载实战
好,重头戏来了。运算符重载说白了就是让自定义类型支持C++内置运算符的语法。我们逐个实现:
4.1 加法:日期 + 天数
DateTime DateTime::operator+(int days) const {
int totalDays = toDays(year_, month_, day_) + days;
int y, m, d;
fromDays(totalDays, y, m, d);
return DateTime(y, m, d, hour_, minute_, second_);
}
DateTime& DateTime::operator+=(int days) {
*this = *this + days;
return *this;
}
这里我用了const成员函数,保证原对象不被修改。你想想看,date + 5应该返回一个新日期,而不是把date本身改了,对吧?
4.2 减法:日期 - 天数 / 日期 - 日期
DateTime DateTime::operator-(int days) const {
return *this + (-days);
}
int DateTime::operator-(const DateTime& other) const {
int d1 = toDays(year_, month_, day_);
int d2 = toDays(other.year_, other.month_, other.day_);
return d1 - d2;
}
两个日期相减返回天数差,这个在计算工龄、合同剩余天数时特别常用。我在做人力资源系统时就写过类似的逻辑。
4.3 比较运算符
bool DateTime::operator==(const DateTime& other) const {
return year_ == other.year_ && month_ == other.month_
&& day_ == other.day_ && hour_ == other.hour_
&& minute_ == other.minute_ && second_ == other.second_;
}
bool DateTime::operator<(const DateTime& other) const {
if (year_ != other.year_) return year_ < other.year_;
if (month_ != other.month_) return month_ < other.month_;
if (day_ != other.day_) return day_ < other.day_;
if (hour_ != other.hour_) return hour_ < other.hour_;
if (minute_ != other.minute_) return minute_ < other.minute_;
return second_ < other.second_;
}
// 其他比较运算符可以基于 < 和 == 实现
bool DateTime::operator!=(const DateTime& other) const { return !(*this == other); }
bool DateTime::operator>(const DateTime& other) const { return other < *this; }
bool DateTime::operator<=(const DateTime& other) const { return !(other < *this); }
bool DateTime::operator>=(const DateTime& other) const { return !(*this < other); }
operator<和operator==后,其他比较运算符都可以通过组合这两个来生成。这是C++标准库推荐的写法,减少重复代码。
4.4 自增自减
// 前置++
DateTime& DateTime::operator++() {
*this += 1;
return *this;
}
// 后置++
DateTime DateTime::operator++(int) {
DateTime temp = *this;
++(*this);
return temp;
}
// 前置-- 和后置-- 类似,略
后置自增返回旧值,前置自增返回新值。这个区别我当年面试时被问过,后来在项目里写迭代器时才算真正理解。
五、格式化输出
格式化输出是日期时间类的门面。用户不关心你内部怎么算的,只关心能不能按2025-03-15 14:30:00这种格式显示出来。
std::string DateTime::format(const std::string& fmt) const {
std::string result;
for (size_t i = 0; i < fmt.size(); i++) {
if (fmt[i] == '%' && i + 1 < fmt.size()) {
char c = fmt[++i];
switch (c) {
case 'Y': result += std::to_string(year_); break;
case 'y': result += std::to_string(year_ % 100); break;
case 'M': result += padZero(month_); break;
case 'D': result += padZero(day_); break;
case 'H': result += padZero(hour_); break;
case 'm': result += padZero(minute_); break;
case 'S': result += padZero(second_); break;
default: result += c; break;
}
} else {
result += fmt[i];
}
}
return result;
}
// 辅助:补零到两位
std::string DateTime::padZero(int num) const {
if (num < 10) return "0" + std::to_string(num);
return std::to_string(num);
}
使用示例:
DateTime dt(2025, 3, 15, 14, 30, 5);
std::cout << dt.format("%Y-%M-%D %H:%m:%S") << std::endl;
// 输出:2025-03-15 14:30:05
std::cout << dt.format("%y/%M/%D") << std::endl;
// 输出:25/03/15
strftime的格式,支持更多占位符,比如%W(星期几)、%j(年中的第几天)。我自己的工具类里还加了%Q表示季度,业务上经常用到。
六、完整使用示例
把上面所有功能串起来跑一遍:
int main() {
DateTime d1(2025, 3, 15, 10, 0, 0);
DateTime d2(2025, 1, 1, 0, 0, 0);
// 加减天数
DateTime d3 = d1 + 10;
std::cout << d3.format("%Y-%M-%D") << std::endl; // 2025-03-25
// 日期差值
int diff = d1 - d2;
std::cout << "相差天数: " << diff << std::endl; // 73
// 比较
if (d1 > d2) {
std::cout << "d1 晚于 d2" << std::endl;
}
// 自增
++d1;
std::cout << d1.format("%Y-%M-%D") << std::endl; // 2025-03-16
return 0;
}
七、避坑指南
最后分享几个我踩过的坑:
- 月份和日期从1开始:内部存储和用户输入保持一致,别搞0-based索引,否则容易晕。我见过一个同事把月份存成0-11,结果格式化时忘了加1,所有日期都少一个月。
- 运算符返回类型要一致:
operator+返回新对象,operator+=返回引用。混用会导致链式调用出问题。 - 边界测试不能省:闰年2月29日、跨年12月31日加1天、1月1日减1天,这些边界情况必须测。我曾经在跨年计算上翻过车,从那以后边界测试成了我的标配。
- 格式化字符串的转义:如果用户想输出
%字符本身,需要支持%%转义。上面代码里没加,你可以自己补上。
好了,日期时间类的核心内容就这些。运算符重载让代码更自然,格式化输出让结果更直观。这两个技能在C++实战中经常一起出现,值得你花时间练透。