時間と2038年問題

時間と2038年問題


サイト全体の目次

稿


稿

時間の扱い方

このセクションのソース

さて、章題には「2038年問題」などという意味深な表記がありますが、これはコラムで説明するとして、まずはC言語で時間を扱う方法を説明しましょう。なお、今回出てくる時間にかかわる関数はすべて「time.h」に定義されています。

/*01*/ #include <stdio.h>
/*02*/ #include <time.h>
/*03*/ 
/*04*/ main()
/*05*/ {
/*06*/   time_t tp;    /*1:*/
/*07*/   struct tm *u_tp, *l_tp;
/*08*/ 
/*09*/ 
/*10*/   tp = time(NULL);    /*2:*/
/*11*/   printf("1970年1月1日 00:00:00 から標準時で\n");
/*12*/   printf("%ld 秒経過しました。現在の日時は\n", tp);
/*13*/ 
/*14*/   u_tp = gmtime(&tp);    /*3:*/
/*15*/   printf("万国標準時 -> %d年%d月%d日 %d:%d:%d です。\n",    /*4:*/
/*16*/     u_tp->tm_year + 1900,
/*17*/     u_tp->tm_mon + 1,
/*18*/     u_tp->tm_mday,
/*19*/     u_tp->tm_hour,
/*20*/     u_tp->tm_min,
/*21*/     u_tp->tm_sec);
/*22*/ 
/*23*/   l_tp = localtime(&tp);    /*3:*/
/*24*/   printf("現地時刻   -> %d年%d月%d日 %d:%d:%d です。\n",    /*4:*/
/*25*/     l_tp->tm_year + 1900,
/*26*/     l_tp->tm_mon + 1,
/*27*/     l_tp->tm_mday,
/*28*/     l_tp->tm_hour,
/*29*/     l_tp->tm_min,
/*30*/     l_tp->tm_sec);
/*31*/ 
/*32*/   return 0;
/*33*/ }

list-7.5.1-06

time_tというのは、これから使うtime関数で得られる秒数を格納するための変数型で、time.hに定義されています。具体的には typedef で long 型だと定義されていますから、long 型として宣言しても問題ありませんし、printf などの変換文字では「%ld」を使います (*1) 。

将来的にはさらに大きな型が使われるようになるでしょう。この辺はコンパイラのマニュアルできちんと確認しておいたほうがよいかと思います。

つぎのstruct tmも time.h で定義されていて、これは上の time_t 型変数に基づいて細かく解析したデータを格納するための構造体です。具体的には、次のようになっています。

struct tm{
    int tm_sec;      /*分の後の秒       (0,59)*/
    int tm_min;      /*毎時の後の分     (0,59)*/
    int tm_hour;     /*真夜中以来の時間  (0,23)*/
    int tm_mday;     /*月の日          (1,31)*/
    int tm_mon;      /*1月以来の月      (0,11)*/
    int tm_year;     /*1900年以来の年*/
    int tm_wday;     /*日曜以来の日     (0,6)*/
    int tm_yday;     /*1月1日以来の日   (0,365)*/
    int tm_isdst;    /*夏時間のフラグ*/
};    /*()内は値の範囲*/

list-7.5.1-10

「time」関数は、「1970年1月1日 00:00:00」からの経過秒をシステムから取得します。ただし、この秒数は UTC(万国標準時) です。この関数のプロトタイプは

time_t time(time_t *tp)

となっています。ここで、取得した秒数は戻り値として返され、さらに引数で指定したポインタにも値が格納されます。ですが実際には二つも値はいらないという場合が多いので、そのときはどちらかを省略します。ちなみに、引数のほうを省略する場合は「NULL」ポインタを使います。
tp = time(NULL);    /*この二つは同じで*/
time(&tp);          /*「tp」に秒数が格納される*/

そして、「time_t」型は「long」型ですから、printf では変換文字「%dl」で表示させます。

list-7.5.1-14

gmtime関数は time 関数などで得た値を元に、時間を細かく解析してその結果を tm 構造体に格納します。この関数のプロトタイプは

struct tm *gmtime(const time_t *tp)

となっています。まず、「const」なる修飾子が出てきました。これは以前にも説明したとおり宣言時にしか初期化できないことを示し、int などの型の前につけます。ちなみに、文字列定数などはよく「const char *」などと表記されます。

さて gmtime 関数は引数に、秒数を格納した time_t 型変数のポインタをとり、それをもとに時間を解析し、結果を格納した tm 構造体を自動的に生成してそのポインタを返します。

ここで注意してほしいのは、まず、gmtime 関数は引数、戻り値ともにポインタで処理されるということです。特に戻り値もポインタですから、最初の宣言でも「構造体へのポインタ変数」を用意しました。

u_tp = gmtime(&tp);

もう一点注意してほしいのは、この結果はまだ UTC(万国標準時)だということです。これに対し、23行目のlocaltime関数は、gmtime 関数と同じ働きですが、解析したデータは現地時刻になおされています。

list-7.5.1-15

gmtime や localtime 関数で構造体に格納されたデータを元に現在の日時を表示させます。

tm_year には1900年 からの年数が格納されています。例えば今が 2000 年なら「100」が格納されていることになります。ですから、4桁で西暦年を表示させたい場合は 1900 を足してやる必要がある点に注意してください。

u_tp->tm_year + 1900

2桁で表示させたい場合は100で割ったあまりを使えばよいでしょう。また tm_mon に 1 足しているのもおなじ理由です。

ところで、printf でこんなにズラズラ書かなくても、tm 構造体を元に所定の形の文字列を生成してくれる関数があります。次のプロトタイプで示される二つの関数です。

char *asctime(const struct tm *tp)    /*UTC で*/
char *ctime(const time_t *tp)      /*現地時刻で*/

また、これで生成される文字列は次のような形式になります。
Sun Jan  3 15:14:13  1988\n\0

まず、asctime関数は、gmtime 関数や localtime 関数で得られた構造体へのポインタを引数に取り、それをもとに上で示した形式の文字列を自動生成して、その文字列へのポインタを返します。ですから、char 型のポインタで受け取ってやりましょう。
sp = asctime(u_tp)    /*sp は char 型ポインタ変数*/

これに対し、ctime関数は、time 関数で得られた秒数を格納する変数へのポインタを引数に取り、それをもとに上で示した形式で「現地時刻」を生成して、その文字列へのポインタを返します。
sp = ctime(&tp)    /*sp は char 型ポインタ変数*/

ちなみに、これは次と同じことです。
sp = asctime(localtime(&tp))

コラム

2038年問題 - 1

「Y2K」つまり「西暦2000年問題」というのは大きな話題になった話ですね。これはまだコンピュータの性能が低かったころ、データサイズを少しでも小さくしようと、西暦年を二桁で処理していたことによっと生じる問題で、つまり、「99年」は「1999年」と解釈しても、「00年」は「1900年」であると解釈してしまうため、「2000年」以降が表現できないという問題でした。

では、「2038年問題」とは何でしょうか?この章の説明を思い出してください。「time」関数の戻り値型、time_t 型は何型でしたか?long 型でしたね。さぁ、long 型で扱える値の範囲はいくつだったでしょうか?第4章に載っていますが、long 型では -2,147,483,648 ~ 2,147,483,647 の範囲の整数が扱えます。そして「time」関数は「1970年1月1日 00:00:00」からの経過秒を返してきます・・・。

もうなんとなくわかったでしょうか?time 関数で扱える時刻は「1970年1月1日 00:00:00」から 2,147,483,647 秒経過したときまでで、それをこえるとオーバーフローを起こしてしまうのです。

ここで、うるう年などを考えずに「1年 = 60秒 * 60分 * 24時間 * 365日 = 31,536,000秒」としても、これで 2,147,483,647 を割ると「68年 余り ・・・」になります。ですから、「1970年」から数えて「68年後」、つまり「2038年中」に「time」関数を使った処理は破綻します。特にC言語の利用が多い UNIX 系 OS では深刻な問題といえるかもしれません。

2038年問題 - 2

さて、上で書いた記事には厳密さの書ける点があるので、もう少し詳しく説明することにします。

このページで time_t 型は long であると書きましたが、そうである必要はありません。従来のC言語では整数型の最大が long だったためにそのような定義がなされていたのでしょう。ですが、1999年に策定されたC99では64ビットの long long 型が加わり、現在では多くのコンパイラが対応しています。それにあわせて time_t 型が long long 型として定義(VC++.net 2003 では64ビット long として定義済み)されれば2038年問題も無くなるでしょう・・・、と思いたいのですがそういうわけにも行きません。たとえ最新のコンパイラが64ビットに対応しようが、CPUが64ビットになろうが、OSが64ビットになろうが、はたまたそれ以上のビット(128ビットとか)に対応しようが、time_t が long 型であると定義されたコンパイラでコンパイルされていれば相変わらずそのプログラムは2038年に破綻します。特に、銀行システムのような巨大なシステムはず~っと昔に書かれたプログラム(ちなみに COBOL らしいです)をつぎはぎしながら使っているという話も聞きます。そういった巨大システムや、どこかに潜んでいる古いプログラムが2038年に破綻しないよう切に願うばかりです。


読み込み中・・・
10秒待っても表示が変わらない場合、次の理由が考えられます。
・Javascript が無効になっています。
・検索エンジンのキャッシュを見ています。
サイトホームへ / 上位ページへ / ページトップへ / PAROFトレンドショッピングへ
Copyright (C) 2010 totobon all right reserved.