C言語で日時を扱う際に、カレンダーに近い形のstruct tmから、UNIX時刻として計算しやすいtime_tへ変換するのがmktimeです。
ローカルタイムとして解釈されること、戻り値の扱い、夏時間やタイムゾーンの影響を正しく理解すると、意図しない時刻ズレを防げます。
この記事ではmktimeの使い方と注意点を、初心者でも実践できるよう丁寧に解説します。
mktimeとは?struct tmからtime_tへ変換の基本
C言語のmktimeの役割
mktimeは、struct tmで表現された壊れた時刻(年や月、日、時分秒などの各要素)を、UNIX時刻(エポックからの経過秒数であるtime_t)に変換する関数です。
入力されるstruct tmはローカル時刻として解釈され、tmの内容は正規化されて戻ります。
典型的な用途としては、ユーザーが入力した年月日時分秒をtime_tに変換して差分計算や比較、ファイルのタイムスタンプとの照合を行う、などが挙げられます。
time_tとstruct tmの関係
time_tは内部的な秒数(エポックからの相対値)、struct tmは人にわかりやすいカレンダー形式です。
mktimeはこの2つの世界を相互接続する架け橋の役割を果たします。
逆方向の変換はlocaltime(ローカル時刻)やgmtime(UTC)が担いますが、本記事ではmktimeに絞って解説します。
戻り値とエラー判定
mktimeは変換したtime_tを返し、(time_t)-1を返した場合は失敗を意味します。
ただし(time_t)-1は「1969-12-31 23:59:59」など有効な時刻を表す可能性があり、戻り値だけでは失敗と区別できないことがあります。
実装依存でerrnoが設定される場合もありますが、規格上は未定義です。
そのため、アプリの仕様として「扱う日付範囲を限定する」などの方針が実務上は有効です。
補正される項目
mktimeは以下の補正と正規化を行います。
- 範囲外の値を繰り上げ・繰り下げして妥当化します(例: 月13→翌年1、分90→+1時間30分)。
tm_wday(曜日)とtm_yday(通算日)を自動計算します。tm_isdstが-1なら、夏時間の適用可否を推定して更新します。
mktime呼び出し後のstruct tmは、入力時よりも信頼できる正規化済みの内容になっている点が重要です。
struct tmの設定方法と基本ルール
年月日の指定
tm_yearは西暦年-1900、tm_monは0始まり(0が1月、11が12月)、tm_mdayは1から始まります。
初心者の方はtm_yearは1900年差分、tm_monは0始まりという2点で躓きやすいため特に注意してください。
下表は主要フィールドの意味と典型範囲です。
| フィールド | 意味 | 典型的な範囲・備考 |
|---|---|---|
| tm_year | 西暦年-1900 | 例えば2025年は125 |
| tm_mon | 月(0始まり) | 0から11 |
| tm_mday | 日 | 1から31 |
| tm_hour | 時 | 0から23 |
| tm_min | 分 | 0から59(範囲外は正規化) |
| tm_sec | 秒 | 0から59(一部実装で60を許容、ただし非推奨) |
| tm_wday | 曜日 | 0(日)から6(土)。mktimeが計算 |
| tm_yday | 年内の通算日 | 0から365。mktimeが計算 |
| tm_isdst | 夏時間フラグ | 1(適用)、0(非適用)、-1(自動判定) |
時分秒の指定
tm_hourは0から23、tm_minは0から59、tm_secは0から59が基本です。
閏秒として60を許容する実装もありますが、可搬性が低いためtm_sec=60の使用は避けるのが無難です。
範囲外をセットしてもmktimeが正規化します。
tm_isdstは-1が基本
特別な理由がなければtm_isdstは-1にします。
これにより、夏時間の適用可否はライブラリが自動的に判断します。
曖昧な時刻(夏時間の切替瞬間)を明示的に指定したい場合に限り、0または1を設定します。
範囲外は自動正規化
例えばtm_mon=12(13月)やtm_min=90などの範囲外値は、mktimeが繰り上げ・繰り下げで正規化します。
正規化結果はmktime呼び出し後のtmに反映されます。
ローカル時刻として解釈
mktimeは常にローカルタイムとして解釈します。
タイムゾーンは環境変数TZとtzsetの影響を受けます。
UTCとして解釈したい場合の方法は後述します。
mktimeの使い方
手順1: struct tmを0で初期化
まず全フィールドを0で初期化し、未設定項目の不定値を防ぎます。
C99以降ならstruct tm tm = {0};が簡潔です。
手順2: 必須フィールドを設定
年、月、日、時、分、秒を設定します。
tm_yearは西暦-1900、tm_monは0始まりを忘れずに。
tm_isdstは原則-1にします。
手順3: mktimeでtime_tへ変換
time_t t = mktime(&tm);で秒数に変換します。
戻り値が(time_t)-1かどうかを確認しましょう。
手順4: 戻り値とtmの補正を確認
mktime呼び出し後のtmは正規化されます。
曜日、通算日、夏時間フラグ、範囲外値の繰り上げなどが反映されています。
例: 指定日の0時0分0秒を変換
以下は2025-03-15 00:00:00(ローカル)をtime_tへ変換し、結果と正規化後のtmを表示する例です。
出力は環境のタイムゾーンに依存します。
// mktime_basic.c
// 2025-03-15 00:00:00 (ローカル) を time_t に変換する例
#include <stdio.h>
#include <time.h>
#include <inttypes.h> // PRIdMAX など
int main(void) {
struct tm tm = {0}; // まずはゼロ初期化
tm.tm_year = 2025 - 1900; // 西暦-1900
tm.tm_mon = 2; // 0が1月 → 2は3月
tm.tm_mday = 15; // 15日
tm.tm_hour = 0;
tm.tm_min = 0;
tm.tm_sec = 0;
tm.tm_isdst = -1; // 夏時間は自動判定
time_t t = mktime(&tm);
if (t == (time_t)-1) {
// 注意: -1 は有効な時刻の可能性がある。ここでは簡略にエラー表示。
fprintf(stderr, "mktime failed or returned ambiguous -1\n");
return 1;
}
// time_t を整数として表示 (intmax_t にキャストして可搬に出力)
printf("time_t: %" PRIdMAX "\n", (intmax_t)t);
// 正規化された struct tm の内容を確認
printf("正規化後: %04d-%02d-%02d %02d:%02d:%02d, wday=%d, yday=%d, isdst=%d\n",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec,
tm.tm_wday, tm.tm_yday, tm.tm_isdst);
// 表示用にstrftimeを軽く利用 (ISO 8601風、%zでUTCオフセット、%Zでタイムゾーン名)
char buf[64];
if (strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z (%Z)", &tm)) {
printf("表示: %s\n", buf);
}
return 0;
}
time_t: 1741974000
正規化後: 2025-03-15 00:00:00, wday=6, yday=73, isdst=0
表示: 2025-03-15 00:00:00 +0900 (JST)
失敗時(-1)の扱い
mktimeが(time_t)-1を返しても、それが「本当に失敗」か「有効時刻1969-12-31 23:59:59などを表した成功」か区別できない点が落とし穴です。
最も実務的な対策は、アプリで扱う日付範囲を決め、その範囲に「1969-12-31 23:59:59」相当が入り得ないようにすることです。
範囲外となる古い日時や遠い未来を扱う場合は、ライブラリやプラットフォームの仕様(64ビットtime_tの有無など)を確認しましょう。
以下は単純な範囲チェックを加えた例です。
// mktime_range_guard.c
// 入力を 1970年以降〜2100年までに制限し、-1の曖昧さを回避する方針の例
#include <stdio.h>
#include <time.h>
#include <inttypes.h>
int safe_mktime_1970_2100(struct tm *ptm, time_t *out) {
if (!ptm || !out) return 0;
int y = ptm->tm_year + 1900;
if (y < 1970 || y > 2100) {
return 0; // 仕様上この関数では扱わない
}
time_t t = mktime(ptm);
if (t == (time_t)-1) {
// この範囲で-1は通常起きない想定(実装依存のため絶対ではない)
return 0;
}
*out = t;
return 1;
}
int main(void) {
struct tm tm = {0};
tm.tm_year = 1970 - 1900;
tm.tm_mon = 0; // 1月
tm.tm_mday = 1;
tm.tm_isdst = -1;
time_t t;
if (safe_mktime_1970_2100(&tm, &t)) {
printf("OK: %" PRIdMAX "\n", (intmax_t)t);
} else {
printf("NG: 変換失敗または許容範囲外\n");
}
return 0;
}
OK: 0
mktimeの注意点と落とし穴
UTCにしたい場合
mktimeはローカル時刻として解釈します。
UTCのstruct tmをUTCとして変換したい場合、選択肢は主に2つです。
- 非標準の
timegmを使う(GlIBCやBSD、macOSにあり)。 - 一時的に
TZ=UTCに設定してtzset後にmktimeを呼ぶ。
後者のPOSIX的手法の例を示します。
環境変数の変更はプロセス全体に影響するため並行スレッドがある場合は注意してください。
// mktime_as_utc.c
// struct tm を UTC として time_t に変換する関数の例
// POSIX: setenv/tzset, Windows: _putenv/_tzset を使用
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
int tm_to_time_t_utc(struct tm tm_utc, time_t *out) {
if (!out) return 0;
// 保存: 既存TZ
const char *old_tz = getenv("TZ");
char old_tz_copy[256];
if (old_tz) {
strncpy(old_tz_copy, old_tz, sizeof(old_tz_copy));
old_tz_copy[sizeof(old_tz_copy)-1] = '// mktime_as_utc.c
// struct tm を UTC として time_t に変換する関数の例
// POSIX: setenv/tzset, Windows: _putenv/_tzset を使用
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
int tm_to_time_t_utc(struct tm tm_utc, time_t *out) {
if (!out) return 0;
// 保存: 既存TZ
const char *old_tz = getenv("TZ");
char old_tz_copy[256];
if (old_tz) {
strncpy(old_tz_copy, old_tz, sizeof(old_tz_copy));
old_tz_copy[sizeof(old_tz_copy)-1] = '\0';
} else {
old_tz_copy[0] = '\0';
}
// TZ=UTC を設定
#if defined(_WIN32)
_putenv("TZ=UTC");
_tzset();
#else
setenv("TZ", "UTC", 1);
tzset();
#endif
tm_utc.tm_isdst = 0; // UTCにDSTはないので0を明示(または-1でも可)
time_t t = mktime(&tm_utc);
// 元のTZを復元
#if defined(_WIN32)
if (old_tz) {
char buf[300];
snprintf(buf, sizeof(buf), "TZ=%s", old_tz_copy);
_putenv(buf);
} else {
_putenv("TZ="); // 未設定に戻す
}
_tzset();
#else
if (old_tz) setenv("TZ", old_tz_copy, 1);
else unsetenv("TZ");
tzset();
#endif
if (t == (time_t)-1) return 0;
*out = t;
return 1;
}
int main(void) {
struct tm tm = {0};
tm.tm_year = 2025 - 1900;
tm.tm_mon = 0; // 1月
tm.tm_mday = 1; // 1日
tm.tm_hour = 0; // 00:00:00 UTC
time_t t;
if (tm_to_time_t_utc(tm, &t)) {
printf("UTC 2025-01-01 00:00:00 -> time_t = %lld\n", (long long)t);
} else {
printf("変換失敗\n");
}
return 0;
}
';
} else {
old_tz_copy[0] = '// mktime_as_utc.c
// struct tm を UTC として time_t に変換する関数の例
// POSIX: setenv/tzset, Windows: _putenv/_tzset を使用
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
int tm_to_time_t_utc(struct tm tm_utc, time_t *out) {
if (!out) return 0;
// 保存: 既存TZ
const char *old_tz = getenv("TZ");
char old_tz_copy[256];
if (old_tz) {
strncpy(old_tz_copy, old_tz, sizeof(old_tz_copy));
old_tz_copy[sizeof(old_tz_copy)-1] = '\0';
} else {
old_tz_copy[0] = '\0';
}
// TZ=UTC を設定
#if defined(_WIN32)
_putenv("TZ=UTC");
_tzset();
#else
setenv("TZ", "UTC", 1);
tzset();
#endif
tm_utc.tm_isdst = 0; // UTCにDSTはないので0を明示(または-1でも可)
time_t t = mktime(&tm_utc);
// 元のTZを復元
#if defined(_WIN32)
if (old_tz) {
char buf[300];
snprintf(buf, sizeof(buf), "TZ=%s", old_tz_copy);
_putenv(buf);
} else {
_putenv("TZ="); // 未設定に戻す
}
_tzset();
#else
if (old_tz) setenv("TZ", old_tz_copy, 1);
else unsetenv("TZ");
tzset();
#endif
if (t == (time_t)-1) return 0;
*out = t;
return 1;
}
int main(void) {
struct tm tm = {0};
tm.tm_year = 2025 - 1900;
tm.tm_mon = 0; // 1月
tm.tm_mday = 1; // 1日
tm.tm_hour = 0; // 00:00:00 UTC
time_t t;
if (tm_to_time_t_utc(tm, &t)) {
printf("UTC 2025-01-01 00:00:00 -> time_t = %lld\n", (long long)t);
} else {
printf("変換失敗\n");
}
return 0;
}
';
}
// TZ=UTC を設定
#if defined(_WIN32)
_putenv("TZ=UTC");
_tzset();
#else
setenv("TZ", "UTC", 1);
tzset();
#endif
tm_utc.tm_isdst = 0; // UTCにDSTはないので0を明示(または-1でも可)
time_t t = mktime(&tm_utc);
// 元のTZを復元
#if defined(_WIN32)
if (old_tz) {
char buf[300];
snprintf(buf, sizeof(buf), "TZ=%s", old_tz_copy);
_putenv(buf);
} else {
_putenv("TZ="); // 未設定に戻す
}
_tzset();
#else
if (old_tz) setenv("TZ", old_tz_copy, 1);
else unsetenv("TZ");
tzset();
#endif
if (t == (time_t)-1) return 0;
*out = t;
return 1;
}
int main(void) {
struct tm tm = {0};
tm.tm_year = 2025 - 1900;
tm.tm_mon = 0; // 1月
tm.tm_mday = 1; // 1日
tm.tm_hour = 0; // 00:00:00 UTC
time_t t;
if (tm_to_time_t_utc(tm, &t)) {
printf("UTC 2025-01-01 00:00:00 -> time_t = %lld\n", (long long)t);
} else {
printf("変換失敗\n");
}
return 0;
}
UTC 2025-01-01 00:00:00 -> time_t = 1735689600
環境にtimegmがある場合はそちらが簡単です。
ポータブルに書くなら#ifdefで切り替える方針もあります。
夏時間(DST)の切替に注意
夏時間の導入や解除の瞬間には存在しない時刻や重複する時刻が生じます。
例えば、米国の多くの地域で春の前進時には「02:xx」が存在しない日があります。
- tm_isdst=-1: ライブラリが適用可否を推定します。存在しない時刻は次の有効な時刻に丸められることがあります。
- tm_isdst=0/1: 曖昧な時刻を明示的にどちらとして扱うか指定できます。
以下は「存在しないローカル時刻」を与えたときの挙動例です(実行環境がPOSIXでTZ=America/Los_Angelesをサポートしている前提)。
// mktime_dst_gap.c
// DSTの前進で存在しない時刻(例: 2023-03-12 02:30:00 in America/Los_Angeles)
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <inttypes.h>
int main(void) {
#if defined(_WIN32)
// Windows環境ではTZ名の解釈が異なることがある点に注意
_putenv("TZ=America/Los_Angeles");
_tzset();
#else
setenv("TZ", "America/Los_Angeles", 1);
tzset();
#endif
struct tm tm = {0};
tm.tm_year = 2023 - 1900;
tm.tm_mon = 2; // March
tm.tm_mday = 12;
tm.tm_hour = 2; // 02:30 は存在しない可能性
tm.tm_min = 30;
tm.tm_isdst = -1; // 自動判定
time_t t = mktime(&tm);
printf("mktime -> %" PRIdMAX "\n", (intmax_t)t);
printf("正規化後: %04d-%02d-%02d %02d:%02d:%02d, isdst=%d\n",
tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tm.tm_isdst);
return 0;
}
mktime -> 1678626600
正規化後: 2023-03-12 03:30:00, isdst=1
このように、存在しない「02:30」が「03:30」に丸められています。
スケジューラやアラームなどの実装ではDST境界をまたぐ入力に注意してください。
(time_t)-1は有効時刻の可能性
前述の通り(time_t)-1はエラーかもしれないし、有効時刻かもしれないため、戻り値だけで失敗を断定しないでください。
アプリで扱う時刻範囲を制限する、テストデータをその範囲に合わせるといった実務的ガードが効果的です。
タイムゾーン(TZ)に依存
mktimeの結果はTZとタイムゾーンデータベースに依存します。
同じコードでもマシンやOS設定によって秒数が変わることがあります。
再現性が必要なテストではTZを固定する(例: TZ=UTC)か、想定タイムゾーンでのみ実行するようにしましょう。
32ビット環境と2038年問題
32ビットのtime_tは2038-01-19 03:14:07 UTC付近でオーバーフローします。
対策としては以下が一般的です。
- 64ビット環境を利用する。
- 32ビットLinuxであっても、近年のglibcでは
-D_TIME_BITS=64を有効にしてビルドし、64ビットtime_tを使う。 - WindowsではVisual Studio 2015以降、既定で64ビット
time_t(_USE_32BIT_TIME_Tを定義しない)。 - 対応範囲外の年を入力しないようアプリ側で制限する。
長期運用するシステムほど、time_tのビット幅と2038年問題の有無を最初に決めておくのが賢明です。
追加例: 正規化の具体例
範囲外の値が自動で正規化される様子を確認します。
// mktime_normalize.c
// 2025-01-31 24:90:00 → 2025-02-01 01:30:00 に正規化される例
#include <stdio.h>
#include <time.h>
int main(void) {
struct tm tm = {0};
tm.tm_year = 2025 - 1900;
tm.tm_mon = 0; // 1月
tm.tm_mday = 31; // 31日
tm.tm_hour = 24; // 24時(=翌日0時)
tm.tm_min = 90; // 90分(=+1時間30分)
tm.tm_isdst = -1;
time_t t = mktime(&tm);
if (t == (time_t)-1) {
puts("mktime failed or ambiguous -1");
return 1;
}
printf("正規化後: %04d-%02d-%02d %02d:%02d:%02d\n",
tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
return 0;
}
正規化後: 2025-02-01 01:30:00
入力が多少ずれていても、mktimeは自然に通る形に整えてくれるため、ユーザー入力の救済にも役立ちます。
まとめ
mktimeはstruct tm(ローカル時刻)をtime_t(UNIX時刻)へ確実に変換し、入力値を正規化してくれる中心的な関数です。
使い方の要点は次の通りです。
まずstruct tmはゼロ初期化し、tm_yearは西暦-1900、tm_monは0始まりに注意して設定します。
tm_isdstは-1で自動判定が基本です。
戻り値(time_t)-1は失敗と有効時刻の両方の可能性があるため、扱う日付範囲を設計で制限する方針が実務上有効です。
UTCで扱いたい場合はtimegmの利用やTZ=UTCでの一時変換が選択肢になります。
夏時間の切替やタイムゾーン依存、32ビット環境の2038年問題といった落とし穴を理解しておけば、日時処理での思わぬズレや不具合を大幅に減らせます。
