C言語で時間を扱うとき、現在時刻をそのまま秒数として使うだけではなく、人間にとって読みやすい「年・月・日・時・分・秒」の形に変換したくなる場面が多くあります。
この記事では、C言語の時間処理で欠かせないlocaltime関数を取り上げ、現在時刻をローカルタイムのstruct tm構造体へ変換する方法や、使う際の注意点、実用的な活用例まで、初心者の方にも分かりやすく解説します。
localtimeとは何かを理解する
localtimeの役割と概要
C言語の標準ライブラリには、時間を扱うためのtime.hヘッダが用意されています。
その中に定義されているlocaltime関数は、次のような役割を持ちます。
「UNIX時間などで表された時刻(time_t)を、その環境のローカルタイム(タイムゾーンやサマータイムを考慮した時刻)の構造体(struct tm)に分解する」
プロトタイプ宣言は次のようになっています。
struct tm *localtime(const time_t *timer);
ここで押さえておきたいポイントを文章で整理すると、次のようになります。
- 引数は
time_t型へのポインタであり、「何秒経過したか」という形で表現されている時刻を渡します。 - 戻り値は
struct tm構造体へのポインタであり、「年・月・日・時・分・秒」などに分解されたローカル時刻が格納されています。 - ローカルタイムとは、そのシステムのタイムゾーン設定に従った時刻のことです。日本環境であれば通常はJST(UTC+9)になります。
このように、localtimeは機械が扱いやすい「秒数の時刻」を、人間が扱いやすい「カレンダー形式の時刻」へ変換する関数と考えると理解しやすいです。
localtimeが返す構造体(struct tm)とは
localtimeが返すstruct tmは、時間を「要素ごと」に分けて保存するための構造体です。
定義は処理系によって多少異なる場合がありますが、代表的なメンバは次の通りです。
struct tm {
int tm_sec; /* 秒 (0〜60) うるう秒を考慮して最大60 */
int tm_min; /* 分 (0〜59) */
int tm_hour; /* 時 (0〜23) */
int tm_mday; /* 日 (1〜31) */
int tm_mon; /* 月 (0〜11) 0が1月、11が12月 */
int tm_year; /* 年 (1900年からの経過年数) 例: 2025年なら2025-1900=125 */
int tm_wday; /* 曜日 (0〜6) 0が日曜、1が月曜... */
int tm_yday; /* 年内通算日 (0〜365) 0が1月1日 */
int tm_isdst; /* サマータイムかどうか 正の値:有効、0:無効、負:不明 */
};
ここで初心者が特につまずきやすい点は次の2つです。
- tm_monは0が1月、11が12月という点です。そのため表示するときには
tm_mon + 1のように調整する必要があります。 - tm_yearは1900年からの経過年数です。そのため西暦を表示したい場合は
tm_year + 1900とする必要があります。
これらの補正を忘れると、表示される年や月がずれてしまうので注意が必要です。
グローバルな静的領域を使うことの注意点
localtimeの戻り値はstruct tm *ですが、このポインタが指す構造体は関数内で静的に確保された「共通の領域」に保存されています。
つまり、次のような性質があります。
- localtimeを呼ぶたびに同じ領域に新しい時刻データが上書きされます。
- free関数で解放してはいけません。自分でmallocした領域ではないためです。
- ポインタが指す構造体のアドレス自体は変わらないことが多く、使い回されます。
この設計はメモリ管理を簡単にする一方で、次のような問題を引き起こします。
「localtimeが返すstruct tmを複数同時に保持したい」「マルチスレッドで同時に使いたい」といった場面に向いていない
この点が、localtimeを使う際の最重要注意点と言えます。
後の章で、具体的な落とし穴や対策を詳しく説明します。
localtimeの基本的な使い方
time関数で現在時刻(time_t)を取得する
localtimeを使う前に、変換元となるtime_t型の値を準備する必要があります。
最もよく使われるのがtime関数です。
time_t time(time_t *t);
引数にNULLを渡すと、現在時刻を表すtime_tの値を戻り値として返してくれます。
また、引数に有効なポインタを渡すと、そのアドレス先にも同じ値が書き込まれます。
初心者のうちは、次のような使い方を覚えておくとよいです。
time_t now;
now = time(NULL); /* 現在時刻を取得 */
このnowが、localtimeへ渡す元の時刻データになります。
time_tの中身は「1970年1月1日0時0分0秒(UTC)からの経過秒数」ですので、人がそのまま見ても意味が分かりません。
そのためlocaltimeで分解する必要があります。
localtimeでtime_tからstruct tmへ変換する
time関数で取得したtime_tを、localtimeへそのまま渡すことで、ローカルタイムのstruct tmへ変換できます。
time_t now = time(NULL); /* 現在時刻を取得 */
struct tm *local = localtime(&now); /* ローカルタイムへ変換 */
ここではlocaltimeに&nowを渡しています。
戻り値のlocalはstruct tm構造体へのポインタです。
このポインタを通じて、年・月・日などの要素を参照します。
なお、localtimeは失敗するとNULLを返す可能性がありますが、通常の環境で現在時刻を変換するだけであれば、失敗することはほとんどありません。
それでも、実用的なプログラムではエラー時のNULLチェックを入れておくことが望ましいです。
struct tmから年・月・日・時刻を取り出す方法
localtimeから得たstruct tm *の各メンバを使うことで、年・月・日・時・分・秒などを取り出せます。
先ほど説明したように、一部の値は補正が必要です。
代表的な取り出し方は次の通りです。
- 年は
1900 + tm_year - 月は
1 + tm_mon - 日は
tm_mdayそのまま - 時は
tm_hourそのまま - 分は
tm_minそのまま - 秒は
tm_secそのまま
たとえば、2025年3月15日 10時20分30秒であれば、構造体の中身は次のようになります。
tm_year = 2025 - 1900 = 125tm_mon = 3 - 1 = 2tm_mday = 15tm_hour = 10tm_min = 20tm_sec = 30
このルールを覚えておけば、構造体から人間にとって読みやすい形式へ変換できます。
サンプルコードで確認する
ここまでの内容を踏まえ、現在時刻を取得し、localtimeでローカルタイムへ変換し、年月日時分秒を表示する一連の流れをサンプルコードで確認します。
#include <stdio.h> /* printfのためにインクルード */
#include <time.h> /* time, localtime, struct tmのためにインクルード */
int main(void)
{
/* 現在時刻を表すtime_t型の変数を用意 */
time_t now;
/* 現在時刻を取得する。引数NULLは「ポインタ渡しはしない」という意味 */
now = time(NULL);
/* 取得に失敗した場合は(time_t)-1が返るのでチェックしておくと安心です */
if (now == (time_t)-1) {
printf("現在時刻の取得に失敗しました。\n");
return 1;
}
/* time_tからローカルタイムのstruct tmへ変換する */
struct tm *local_time = localtime(&now);
/* localtimeが失敗した場合はNULLが返る可能性があります */
if (local_time == NULL) {
printf("localtimeによる変換に失敗しました。\n");
return 1;
}
/* struct tmから年・月・日・時・分・秒を取り出して表示する
tm_yearは1900年からの年数、tm_monは0が1月なので補正が必要です */
int year = local_time->tm_year + 1900; /* 西暦年へ変換 */
int month = local_time->tm_mon + 1; /* 1〜12の月へ変換 */
printf("現在のローカル時刻は %04d-%02d-%02d %02d:%02d:%02d です。\n",
year,
month,
local_time->tm_mday,
local_time->tm_hour,
local_time->tm_min,
local_time->tm_sec);
return 0;
}
現在のローカル時刻は 2025-03-15 10:20:30 です。
実際の実行環境や日時によって表示される数値は変わりますが、「年月日 時分秒」の形式で、ローカルタイムが表示されることを確認してください。
localtime使用時の注意点と落とし穴
ポインタが指す領域を書き換えない
localtimeが返すstruct tm *は、ライブラリ内部で共有されている静的な領域を指しています。
そのため、原則として自分で書き換えない方が安全です。
実際には、書き換えてもクラッシュしないことが多いですが、次のような問題を引き起こす可能性があります。
- 他のコードも同じ構造体を参照している場合、意図しない値が見えてしまう
- 他の時間関連関数が内部でこの領域を使っている場合、挙動が不定になる
どうしてもstruct tmの値を編集したい場合は、自分で別の変数にコピーしてから操作するのが安全です。
struct tm *local_time = localtime(&now);
/* いきなりlocal_time->tm_hourなどを書き換えるのではなく、
まずは別の変数にコピーしてから編集します */
struct tm my_time = *local_time; /* 構造体を丸ごとコピー */
/* my_timeに対して自由に編集してよい */
my_time.tm_hour += 1; /* 1時間後にしてみる、など */
/* 必要に応じてmktimeに渡してtime_tへ戻すこともできます */
このように値を安全に扱うためには、ポインタが指す共有領域を直接書き換えず、自前の構造体にコピーする習慣をつけることが大切です。
連続してlocaltimeを呼び出すとデータが上書きされる
localtimeの静的領域は1つしかありません。
そのため、連続してlocaltimeを呼び出すと、前の結果が上書きされてしまうという落とし穴があります。
次のようなコードを考えてみます。
#include <stdio.h>
#include <time.h>
int main(void)
{
time_t t1 = time(NULL);
time_t t2 = t1 + 60; /* 仮に60秒後の時刻を用意 */
struct tm *tm1 = localtime(&t1);
struct tm *tm2 = localtime(&t2);
/* tm1とtm2は同じ静的領域を指している可能性が高い */
printf("1つ目の時刻: %02d:%02d:%02d\n",
tm1->tm_hour, tm1->tm_min, tm1->tm_sec);
printf("2つ目の時刻: %02d:%02d:%02d\n",
tm2->tm_hour, tm2->tm_min, tm2->tm_sec);
return 0;
}
このプログラムの問題点は、tm1とtm2が同じアドレスを指している可能性が高いことです。
その場合、2回目のlocaltime呼び出しで1回目の内容が上書きされ、2つのprintfがどちらも「2つ目の時刻」を表示してしまうことになります。
これを避けるためには、localtimeの結果をすぐに自前のstruct tm変数にコピーしておくことが有効です。
struct tm local1 = *localtime(&t1); /* 結果をローカル変数にコピー */
struct tm local2 = *localtime(&t2); /* こちらもコピー */
printf("1つ目の時刻: %02d:%02d:%02d\n",
local1.tm_hour, local1.tm_min, local1.tm_sec);
printf("2つ目の時刻: %02d:%02d:%02d\n",
local2.tm_hour, local2.tm_min, local2.tm_sec);
このようにすれば、2つのstruct tmが別々のスタック領域に保存されるため、互いに上書きされることはありません。
マルチスレッド環境ではlocaltimeは非スレッドセーフ
スレッドを使うプログラム(マルチスレッドプログラム)では、同時に複数のスレッドから同じ関数が呼ばれることが想定されます。
localtimeは静的な共有領域を使っているため、複数のスレッドから同時に呼び出されると、結果が互いに上書きされてしまうという問題があります。
このような関数はスレッドセーフではないと言われます。
スレッドセーフでない関数をマルチスレッド環境でそのまま使用すると、次のような不具合が起きる可能性があります。
- あるスレッドで取得した時刻を使おうとした瞬間に、別スレッドに上書きされ、値が変わってしまう
- 表示される時刻が意図しない値になったり、不定な動作を引き起こしたりする
そのため、マルチスレッド環境ではlocaltimeの代わりにスレッドセーフな関数を使用することが推奨されます。
それが次で説明するlocaltime_rやlocaltime_sです。
localtime_r(localtime_s)との違いと使い分け
各プラットフォームでは、スレッドセーフに動作するlocaltimeのバリエーションが用意されています。
代表的なものは次の2つです。
- POSIX系(Unix/Linuxなど):
localtime_r - Windows系:
localtime_s
これらは結果を書き込む先のstruct tmを呼び出し側が用意することで、共有の静的領域を使わず、スレッドセーフにしています。
代表的な使い方のイメージを見てみます。
localtime_r(POSIX系)
#include <time.h>
/* プロトタイプの一例
struct tm *localtime_r(const time_t *timer, struct tm *result);
*/
time_t now = time(NULL);
struct tm result;
/* 結果をresultに書き込む。戻り値もresultへのポインタが返る */
struct tm *p = localtime_r(&now, &result);
if (p == NULL) {
/* エラー処理 */
}
この場合、resultはスレッドごとに別々のローカル変数として用意できるので、スレッド間で上書きし合う問題が起こりにくくなります。
localtime_s(Windows系)
#include <time.h>
/* プロトタイプの一例
errno_t localtime_s(struct tm *result, const time_t *timer);
*/
time_t now = time(NULL);
struct tm result;
errno_t err = localtime_s(&result, &now);
if (err != 0) {
/* エラー処理 */
}
こちらは戻り値の型や引数の順序がlocaltime_rと異なりますが、「呼び出し側が用意した構造体に結果を書き込む」という考え方は同じです。
使い分けのまとめ
- シングルスレッドの簡単なプログラムであれば、手軽な
localtimeを使ってかまいません。 - マルチスレッドのプログラムでは、
localtime_rやlocaltime_sを使うことで、スレッドセーフに時刻変換を行うことができます。 - 移植性を重視する場合は、条件付きコンパイルでOSごとに適切な関数を選び分けることが多いです。
localtimeを使った実用的な時刻処理
ローカルタイムをフォーマットして表示する
localtimeと組み合わせてよく使われる関数にstrftimeがあります。
これはstruct tmの内容を、指定したフォーマット文字列に従って文字列に変換してくれる関数です。
プロトタイプは次のようになっています。
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
ここで重要なのは、format文字列の中で特定の指定子(例えば%cst-code>%Yや%m)を使うと、年や月などに自動的に置き換えてくれるという点です。
簡単な例を示します。
#include <stdio.h>
#include <time.h>
int main(void)
{
time_t now = time(NULL);
struct tm *local_time = localtime(&now);
if (local_time == NULL) {
printf("localtimeに失敗しました。\n");
return 1;
}
/* 結果を格納する文字列バッファを用意します */
char buffer[64];
/* フォーマット指定:
%Y: 4桁の年
%m: 2桁の月
%d: 2桁の日
%H: 2桁の時(24時間制)
%M: 2桁の分
%S: 2桁の秒
*/
size_t len = strftime(buffer, sizeof(buffer),
"%Y-%m-%d %H:%M:%S", local_time);
if (len == 0) {
/* バッファが足りない場合などは0が返ります */
printf("strftimeに失敗しました。\n");
return 1;
}
printf("現在のローカル時刻(書式付き): %s\n", buffer);
return 0;
}
現在のローカル時刻(書式付き): 2025-03-15 10:20:30
strftimeを使えば、自分でtm_yearやtm_monに足し算をする必要なく、柔軟なフォーマットで日付や時刻を表示できるため、ログ表示や画面表示などで非常に便利です。
ローカルタイムを使ったログ出力の基本
プログラムが動作している様子を記録する「ログ出力」では、いつ何が起きたかを残すことが重要です。
そのため、1行ごとのログにローカルタイムのタイムスタンプを付けることがよくあります。
ログ出力の基本的な流れは次のようになります。
timeで現在時刻を取得するlocaltimeでローカルタイムへ変換するstrftimeやprintfで時刻を文字列に整形する- メッセージと一緒に標準出力やファイルへ書き出す
簡単なログ出力の例を示します。
#include <stdio.h>
#include <time.h>
/* ログメッセージを出力する関数の例 */
void log_message(const char *level, const char *message)
{
time_t now = time(NULL);
struct tm *local_time = localtime(&now);
if (local_time == NULL) {
/* 時刻が取得できない場合でも、とりあえずメッセージは表示する */
printf("[UNKNOWN TIME] [%s] %s\n", level, message);
return;
}
char time_str[32];
/* ログ用に「YYYY-MM-DD HH:MM:SS」形式で整形する */
if (strftime(time_str, sizeof(time_str),
"%Y-%m-%d %H:%M:%S", local_time) == 0) {
/* フォーマットに失敗した場合 */
printf("[UNKNOWN TIME] [%s] %s\n", level, message);
return;
}
/* 実際のログ出力 */
printf("[%s] [%s] %s\n", time_str, level, message);
}
int main(void)
{
log_message("INFO", "プログラムを開始しました。");
log_message("WARN", "設定ファイルが見つかりません。デフォルトを使用します。");
log_message("ERROR", "ファイルの読み込みに失敗しました。");
return 0;
}
[2025-03-15 10:20:30] [INFO] プログラムを開始しました。
[2025-03-15 10:20:30] [WARN] 設定ファイルが見つかりません。デフォルトを使用します。
[2025-03-15 10:20:30] [ERROR] ファイルの読み込みに失敗しました。
実際には時刻は呼び出しごとに多少変わるはずですが、このようにログに日時を付けておくことで、後から問題が起きたタイミングを追跡しやすくなります。
UTCとの違いとmktimeを使った時間計算
localtimeで扱うのはローカルタイムですが、サーバ開発やネットワークプログラミングの世界では、UTC(協定世界時)を基準に時刻を扱うことが多くあります。
C言語にはgmtimeという関数があり、time_tをUTCにおけるstruct tmへ変換できます。
一方、localtimeはローカルタイム(タイムゾーン適用後)へ変換する点が異なります。
さらに、struct tmからtime_tへ戻す関数としてmktimeがあります。
time_t mktime(struct tm *timeptr);
mktimeは、struct tmに書かれたローカル時刻をtime_tに変換します。
この性質を利用すると、時間の加減算(例えば「1時間後」「翌日」など)を直感的に行うことができます。
簡単な例として、「現在のローカルタイムから1時間後のローカルタイムを求める」サンプルを示します。
#include <stdio.h>
#include <time.h>
int main(void)
{
time_t now = time(NULL);
struct tm *local_time = localtime(&now);
if (local_time == NULL) {
printf("localtimeに失敗しました。\n");
return 1;
}
/* 現在のローカルタイムを表示 */
char buf_now[32];
strftime(buf_now, sizeof(buf_now), "%Y-%m-%d %H:%M:%S", local_time);
printf("現在のローカル時刻: %s\n", buf_now);
/* 構造体をコピーして編集用の変数を用意 */
struct tm future = *local_time;
/* 1時間後にしたいので、tm_hourに1を加算します */
future.tm_hour += 1;
/* mktimeを呼ぶことで、日付や月をまたぐ場合も正しく補正されます。
さらに、futureは標準化され、tm_wdayなども正しい値に更新されます。 */
time_t future_time_t = mktime(&future);
if (future_time_t == (time_t)-1) {
printf("mktimeに失敗しました。\n");
return 1;
}
/* 1時間後のローカルタイムを表示 */
char buf_future[32];
strftime(buf_future, sizeof(buf_future), "%Y-%m-%d %H:%M:%S", &future);
printf("1時間後のローカル時刻: %s\n", buf_future);
return 0;
}
現在のローカル時刻: 2025-03-15 23:30:00
1時間後のローカル時刻: 2025-03-16 00:30:00
この例からわかるように、mktimeは日付や月をまたいだ場合でも自動的に補正してくれるため、複雑な日付計算を自前で行う必要がありません。
UTCとの関係についてまとめると、次のようになります。
- time_tはUTCを基準とした経過秒数で表現されています。
- gmtimeはtime_tをUTCのstruct tmに変換します。
- localtimeはtime_tをローカルタイムのstruct tmに変換します。
- mktimeはローカルタイムのstruct tmをtime_tに戻すため、タイムゾーンやサマータイムも考慮されます。
この関係を理解しておけば、UTCとローカルタイムの変換や、時刻計算を安全かつ正確に行いやすくなります。
まとめ
localtimeは、C言語で現在時刻や任意の時刻を人間に分かりやすい「年・月・日・時・分・秒」に変換するための基本関数です。
time関数で取得したtime_tをlocaltimeに渡し、返ってきたstruct tmからtm_yearやtm_monを補正して使うことで、ローカルタイムを自在に扱えます。
ただし、戻り値が指す領域は共有の静的メモリであるため、連続呼び出しやマルチスレッド環境では上書きに注意し、自前の構造体へコピーしたり、localtime_rやlocaltime_sを使ったりすることが大切です。
strftimeやmktimeと組み合わせれば、フォーマット表示や時間計算、ログ出力など、実用的な時刻処理を安全に実装できるようになります。
