实战:日期时间类(运算符重载+格式化)

日期时间处理,是每个C++程序员迟早要面对的硬骨头。我记得刚入行那会儿,接手一个老项目,里面日期计算全是手写的,各种闰年判断、月份天数表散落在代码各个角落。改一个bug能牵扯出三个新bug,那叫一个酸爽。

今天我们就来手撸一个日期时间类,把运算符重载和格式化输出这两个核心技能一次性练透。你想想看,如果能写出date + daysdate1 - date2这种自然语法,代码读起来得多舒服?

一、整体设计思路

先搭个架子。我们需要一个DateTime类,包含年、月、日、时、分、秒。核心功能分三块:

  • 基本运算:加减天数、比较大小、计算差值
  • 格式化输出:支持YYYY-MM-DD HH:MM:SS等常见格式
  • 合法性校验:自动处理闰年、月份天数、进位借位

下面这张图能帮你快速理解整个类的结构:

DateTime 类核心结构 数据成员 int year, month, day int hour, minute, second bool isValid() 校验 运算符重载 operator+ / operator- operator== / operator< operator++ / operator-- 格式化输出 format(const string& fmt) toDateString() toTimeString() 辅助函数(内部使用) daysInMonth() · isLeapYear() · normalize() · toDays() / fromDays() 核心思路:将日期转换为天数进行运算,再转回日期格式 这样加减天数、比较大小都变得简单直观

二、数据成员与基础校验

先定义成员变量。我习惯用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++实战中经常一起出现,值得你花时间练透。


公众号:蓝海资料掘金营,微信deep3321