需求:「取得 2 年 3 個月 20 天前的日期」。若手動減法:
解法:善用 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);
避免靜態緩衝區競爭,多執行緒環境必用。
time_t 與 2038 年問題time_t 最大表達到 2038-01-19 03:14:07 UTC。time_t 為 long long,範圍達 ±2920 億年。_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
適用於效能測量、日誌時間戳、分散式系統時鐘同步。
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 應填 124 | tm.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,即可構建健壯、可移植、長期有效的時間服務模組。