さて、章題には「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*/ }
つぎの
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; /*夏時間のフラグ*/
}; /*()内は値の範囲*/
「time」関数は、「1970年1月1日 00:00:00」からの経過秒をシステムから取得します。ただし、この秒数は UTC(万国標準時) です。この関数のプロトタイプは
time_t time(time_t *tp)
tp = time(NULL); /*この二つは同じで*/ time(&tp); /*「tp」に秒数が格納される*/
そして、「time_t」型は「long」型ですから、printf では変換文字「%dl」で表示させます。
struct tm *gmtime(const time_t *tp)
さて gmtime 関数は引数に、秒数を格納した time_t 型変数のポインタをとり、それをもとに時間を解析し、結果を格納した tm 構造体を自動的に生成してそのポインタを返します。
ここで注意してほしいのは、まず、gmtime 関数は引数、戻り値ともにポインタで処理されるということです。特に戻り値もポインタですから、最初の宣言でも「構造体へのポインタ変数」を用意しました。
u_tp = gmtime(&tp);
もう一点注意してほしいのは、この結果はまだ UTC(万国標準時)だということです。これに対し、23行目の
gmtime や localtime 関数で構造体に格納されたデータを元に現在の日時を表示させます。
tm_year には1900年 からの年数が格納されています。例えば今が 2000 年なら「100」が格納されていることになります。ですから、4桁で西暦年を表示させたい場合は 1900 を足してやる必要がある点に注意してください。
u_tp->tm_year + 1900
ところで、printf でこんなにズラズラ書かなくても、tm 構造体を元に所定の形の文字列を生成してくれる関数があります。次のプロトタイプで示される二つの関数です。
char *asctime(const struct tm *tp) /*UTC で*/ char *ctime(const time_t *tp) /*現地時刻で*/
Sun Jan 3 15:14:13 1988\n\0
sp = asctime(u_tp) /*sp は char 型ポインタ変数*/
sp = ctime(&tp) /*sp は char 型ポインタ変数*/
sp = asctime(localtime(&tp))
「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 では深刻な問題といえるかもしれません。
さて、上で書いた記事には厳密さの書ける点があるので、もう少し詳しく説明することにします。
このページで 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年に破綻しないよう切に願うばかりです。