首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C 語言時間運算全攻略:tm 結構、mktime、localtime 實現精準日期偏移

     編輯:關於C語言

痛點:手動算日期為何容易出錯?

需求:「取得 2 年 3 個月 20 天前的日期」。若手動減法:

  • 月份天數不固定 (28/29/30/31)。
  • 閏年規則:能被 4 整除但不被 100 整除,或被 400 整除。
  • 跨月、跨年借位邏輯極易寫錯 (如 3/1 減 1 天 = 2/28 或 2/29)。

解法:善用 C 標準庫 <time.h> 提供的 struct tm、mktime、localtime/gmtime 三步走策略,讓庫函數處理曆法複雜度。

核心函數與資料結構

struct tm {
    int tm_sec;   // 0-60 (含閏秒)
    int tm_min;   // 0-59
    int tm_hour;  // 0-23
    int tm_mday;  // 1-31
    int tm_mon;   // 0-11 (1月=0)
    int tm_year;  // 年份-1900 (2024年=124)
    int tm_wday;  // 0-6 (週日=0)
    int tm_yday;  // 0-365
    int tm_isdst; // 夏令時標誌
};

time_t time(time_t *t);                 // 取得當前時間戳 (自 1970-01-01 00:00:00 UTC 秒數)
struct tm *localtime(const time_t *t);  // 時間戳 → 本地時間 tm (靜態緩衝區,非執行緒安全)
struct tm *gmtime(const time_t *t);     // 時間戳 → UTC 時間 tm
time_t mktime(struct tm *tm);           // tm (本地時間) → 時間戳,自動正規化欄位 (處理溢位/借位)
char *asctime(const struct tm *tm);     // tm → "Wed Jun 30 21:49:08 1993\n"
char *ctime(const time_t *t);           // 時間戳 → 同 asctime(localtime(t))
size_t strftime(char *s, size_t max, const char *fmt, const struct tm *tm); // 格式化輸出

通用日期偏移函數實作


#include <time.h>
#include <stdbool.h>

/**
 * 計算基準時間加減指定年/月/日/時/分/秒後的時間
 * @param base    基準時間 (若為 NULL 使用當前時間)
 * @param result  輸出結果
 * @param years   年偏移 (正數=未來, 負數=過去)
 * @param months  月偏移
 * @param days    日偏移
 * @param hours   時偏移
 * @param mins    分偏移
 * @param secs    秒偏移
 * @return true 成功, false 失敗 (參數無效或 mktime 失敗)
 */
bool DateTimeOffset(const struct tm *base, struct tm *result,
                    int years, int months, int days,
                    int hours, int mins, int secs)
{
    if (!result) return false;

    struct tm work;
    time_t now;

    if (base) {
        work = *base;           // 複製基準
    } else {
        time(&now);
        struct tm *lt = localtime(&now);
        if (!lt) return false;
        work = *lt;
    }

    // 1. 直接在 tm 欄位做加減 (允許溢位,如 mon=14, mday=40)
    work.tm_year += years;
    work.tm_mon  += months;
    work.tm_mday += days;
    work.tm_hour += hours;
    work.tm_min  += mins;
    work.tm_sec  += secs;

    // 2. mktime 自動正規化:處理閏年、月份天數、跨月跨年、夏令時
    time_t normalized = mktime(&work);
    if (normalized == (time_t)-1) return false; // 失敗 (超出 time_t 範圍)

    // 3. localtime/gmtime 取回正規化後的 tm
    struct tm *final = localtime(&normalized); // 或 gmtime 取 UTC
    if (!final) return false;

    *result = *final;
    return true;
}

使用範例


#include <stdio.h>
#include <time.h>

int main()
{
    struct tm now, target;
    time_t tnow, ttarget;

    time(&tnow);
    struct tm *lt = localtime(&tnow);
    if (!lt) return 1;
    now = *lt;

    printf("Now: %04d-%02d-%02d %02d:%02d:%02d\n",
           now.tm_year+1900, now.tm_mon+1, now.tm_mday,
           now.tm_hour, now.tm_min, now.tm_sec);

    // 計算 2 年 3 月 20 天前
    if (DateTimeOffset(&now, &target, -2, -3, -20, 0, 0, 0)) {
        ttarget = mktime(&target);
        printf("Target: %04d-%02d-%02d %02d:%02d:%02d\n",
               target.tm_year+1900, target.tm_mon+1, target.tm_mday,
               target.tm_hour, target.tm_min, target.tm_sec);
        printf("Diff seconds: %lld\n", (long long)(ttarget - tnow));
    } else {
        puts("Offset calculation failed");
    }
    return 0;
}

進階議題

執行緒安全版本:localtime_r / gmtime_r (POSIX) / localtime_s (C11 Annex K)


// POSIX
struct tm result;
localtime_r(×tamp, &result);

// C11 (MSVC 支援)
struct tm result;
localtime_s(&result, ×tamp);

避免靜態緩衝區競爭,多執行緒環境必用。

64 位元 time_t 與 2038 年問題

  • 32 位元 time_t 最大表達到 2038-01-19 03:14:07 UTC。
  • 現代 64 位元系統 time_t 為 long long,範圍達 ±2920 億年。
  • 編譯 32 位元程式時定義 _FILE_OFFSET_BITS=64 或使用 __time64_t / _localtime64 (MSVC)。

高精度時間:clock_gettime (POSIX) / GetSystemTimePreciseAsFileTime (Windows)


struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);    // 奈秒精度,含秒與奈秒
// ts.tv_sec, ts.tv_nsec

適用於效能測量、日誌時間戳、分散式系統時鐘同步。

時區資料庫 (tzdata / IANA Time Zone)

  • localtime 依賴系統 /usr/share/zoneinfo 或 Windows 登錄時區資訊。
  • 嵌入式裝置可靜態連結 tzdata 或使用 Howard Hinnant's date.h (C++20 <chrono> 基礎) 處理複雜時區規則 (夏令時變更、歷史時區)。

常見錯誤避坑指南

錯誤寫法問題正確做法
tm.tm_mon = 12月份 0-11,12 會被 mktime 視為次年 1 月輸入 1-12 時先 -1;mktime 會正規化
tm.tm_year = 2024年份基準 1900,2024 應填 124tm.tm_year = year - 1900
直接修改 localtime() 回傳指標的內容靜態緩衝區共用,後續呼叫會覆蓋先複製到區域 struct tm 再修改
忽略 tm_isdst 夏令時旗標春秋轉換日可能產生 1 小時偏差設為 -1 讓 mktime 自動判斷,或明確指定 0/1
用 difftime 計算天數再除 86400夏令時變更日非 86400 秒轉 tm 比較 tm_yday/tm_year 或用日期庫

結語

C 語言時間操作的精髓在於信任 mktime 的正規化能力:只要將偏移量直接加到 tm 欄位 (允許負值、超範圍),呼叫 mktime 即可自動處理閏年、月份天數、跨月跨年、夏令時等所有曆法邏輯。搭配執行緒安全版本與 64 位元 time_t,即可構建健壯、可移植、長期有效的時間服務模組。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved