C言語編 第51章 日付と時間、乱数

この章の概要

この章の概要です。

プロセッサ時間

プロセッサ時間とは、プログラムの実行が開始されてから、そのプログラムが使用した時間のことです。

同時に他のプログラム(プロセス)が動いている場合、他方が使用した時間の分は含まれません。

プロセッサ時間を取得するには、clock関数(⇒リファレンス)を使用します。 clock関数は、time.h にあります。

clock_t clock(void);

上記のように、呼び出せば素直にプロセッサ時間が返されるという単純な関数です。 戻り値の clock_t型(→リファレンス)は、 環境によって異なりますが、何らかの整数型や実数型を typedef したものです。

ここまでは単純なのですが、問題は返されるプロセッサ時間の精度です。
現実のストップウォッチでも、何千分の一秒の単位で細かく計測できるものもあれば、秒単位でしか計測できないキッチンタイマーのようなものもあります。 それと同様、コンピュータの世界におけるプロセッサ時間にも、環境ごとの精度の違いがあります。 実行環境におけるプロセッサ時間の精度は、CLOCKS_PER_SECマクロ(⇒リファレンス)で定義されています。 このマクロの置換結果は、「現実の1秒が、プロセッサ時間で幾つになるか」を表しています。

CLOCKS_PER_SECマクロの置換結果が 1000 であれば、1秒がプロセッサ時間で 1000 であるということですから、 逆に言えば、プロセッサ時間における 1 は、0.001秒だということになります。 もし、clock_t型が整数であれば、1 よりも細かい単位は表現できないので、その実行環境のプロセッサ時間の精度は 0.001秒単位だということになります。 これより細かい計測は、少なくとも clock関数では不可能だということです。

C言語標準ではない方法を使って、もっと細かい計測ができる可能性はあります。

これらを踏まえて、経過時間を秒単位で調べるプログラムは次のようになります。

#include <stdio.h>
#include <time.h>

void process(void);

int main(void)
{
	clock_t begin, end;

	begin = clock();
	process();
	end = clock();

	printf( "result: %f seconds\n", (double)(end - begin) / CLOCKS_PER_SEC ); 

	return 0;
}

void process(void)
{
	char buf[80];

	puts( "何か入力すると終了します。" );
	fgets( buf, sizeof(buf), stdin );
}

実行結果

何か入力すると終了します。
abcde
result: 3.679000 seconds

process関数の呼び出し前後で、clock関数を呼んで、プロセッサ時間を取得しておきます。 この場合、process関数にどれだけの時間を消費したかを計測できるという訳です。

ここまで解説してきた通り、プロセッサ時間をそのまま使っても意味のある値にはなりません。 process関数実行後の値から、実行前の値を引き、その結果を CLOCKS_PER_SECマクロの値で割ることによって、秒単位の時間を得られます。 これで意味のある結果になります。

なお、clock_t型は、環境によって整数型であったり、実数型であったりするため、double型にキャストして取り扱うと常に適切な結果が得られます。

カレンダー時間(暦時間)

カレンダー時間(暦時間)は、「2011年 4月 1日 12時 30分 30秒」のような現実の時刻を表現したものです。 具体的には、ある基準時刻からの経過時間によって表現されます。 多くの環境では、基準時刻を「1970年 1月 1日 0時 0分 0秒」としています。

現在のカレンダー時間を取得するには、time関数(⇒リファレンス)を使います。 time関数は、time.h にあります。

time_t time(time_t* timer);

time_t型(→リファレンス)は、 環境によって異なりますが、何らかの算術型を typedef したものです。
結果は戻り値で返される他、引数timer が NULL でなければ、そのアドレスにも格納されます。 不要であれば引数は NULL にして構いません。

time関数で返された結果だけを見ても、何のことだか分かりません。 基準時刻が環境ごとに異なる可能性がありますし、基準時刻が分かっていても、 そこからの経過時間を time_t型で表したときにどんな値になるかは直感的には分からないはずです。
そこで、他の標準関数を併用して、理解できる形式に変換します。 以下の関数が利用できます。

関数名 効果
(⇒gmtime カレンダー時間を、協定世界時(UTC) に変換する。
(⇒localtime カレンダー時間を、ローカル時間に変換する。
(⇒ctime カレンダー時間を、ローカル時間で表現した文字列に変換する。
(⇒difftime 2つのカレンダー時間の差を調べる。

ローカル時間協定世界時については、この後の項で説明することにして、 ここでは、difftime関数だけ取り上げておきます。

double difftime(time_t t1, time_t t0);

difftime関数は、t1 - t0 をした結果を double型で返します。 結果はいかなる環境でも必ず秒数です
ごく単純な関数ですが、time_t型の差を求めるとき、普通に減算で行うのではなく、必ずこの関数を使わないといけないことに注意して下さい。 time_t型は、算術型の typedef ですから、普通に減算できてしまいますが、どのようなフォーマットで表現されているかは環境によって異なるので、 普通の減算は必ずしも正しくありません。

次のサンプルプログラムは、clock関数のところの例と同様、経過時間を求めています。

#include <stdio.h>
#include <time.h>

void process(void);

int main(void)
{
	time_t begin, end;

	begin = time( NULL );
	process();
	end = time( NULL );

	printf( "result: %f seconds\n", difftime(end, begin) ); 

	return 0;
}

void process(void)
{
	char buf[80];

	puts( "何か入力すると終了します。" );
	fgets( buf, sizeof(buf), stdin );
}

実行結果

何か入力すると終了します。
abcde
result: 4.000000 seconds

協定世界時(UTC)

協定世界時 (UTC) とは、世界で統一されている標準時のことです。 具体的には、イギリスのグリニッジ(経度0度)における時間を基準時としているので、 グリニッジ標準時 (GMT) のことでもあります。

協定世界時を直接取得する関数はありませんが、time関数の結果を gmtime関数に渡せば得られます。

#include <stdio.h>
#include <time.h>
#include <assert.h>

static void printTm(const struct tm* t);

int main(void)
{
	struct tm* utc;
	time_t t;
	
	t = time( NULL );
	utc = gmtime( &t );

	printTm( utc );

	return 0;
}

void printTm(const struct tm* t)
{
	assert( t != NULL );
	
	printf( "年       : %d\n", t->tm_year );
	printf( "月       : %d\n", t->tm_mon );
	printf( "日       : %d\n", t->tm_mday );
	printf( "曜日     : %d\n", t->tm_wday );
	printf( "時       : %d\n", t->tm_hour );
	printf( "分       : %d\n", t->tm_min );
	printf( "秒       : %d\n", t->tm_sec );
	printf( "通算日   : %d\n", t->tm_yday );
	printf( "季節時間 : %d\n", t->tm_isdst );
}

実行結果

年       : 111
月       : 3
日       : 3
曜日     : 0
時       : 4
分       : 7
秒       : 27
通算日   : 92
季節時間 : 0

gmtime関数は次のように宣言されています。

struct tm* gmtime(const time_t *t);

引数は time_t型のポインタなので、time関数の戻り値は一旦変数に受け取ってから、そのアドレスを渡す必要があります。

戻り値は、tm構造体(→リファレンス)のポインタとして返されます。 この構造体は次のように定義されています。

struct tm {
	int tm_sec;     /* 秒 (0〜61) */
	int tm_min;     /* 分 (0〜59) */
	int tm_hour;    /* 時 (0〜23) */
	int tm_mday;    /* 日 (1〜31) */
	int tm_mon;     /* 月 (0〜11) */
	int tm_year;    /* 年 (1900 からの経過年) */
	int tm_wday;    /* 曜日 (0〜6 で 0 が日曜日) */
	int tm_yday;    /* 年始からの通算日 */
	int tm_isdst;   /* 0 なら季節時間無し。1以上で季節時間あり。負数は不明 */
};

上記のメンバがすべて、この名前と型で含まれていることは保証されていますが、 これら以外にもメンバが存在している可能性はあります。

この構造体のメンバについては、コメントに書いておきましたが、 幾つか補足しなければならない部分もあります。
まず、秒を表す tm_secメンバの最大値は 61 まであり得ます。 これは、閏秒(うるうびょう)を表現できるようにするための処置です。

閏秒は、現実の地球の自転との間にうまれるわずかな誤差を修正するために、 特定の月の最後に挿入、あるいは削除される調整のための 1秒のことを指します。

C99規格からは、上限が 60 に変更されました。

月を表す tm_monメンバは、0基準になっていますから、日本人感覚とは合致していません。 「3月」なら「2」になるという訳です。

年を表す tm_yearメンバは、西暦1900年を基準とした経過年です。 2011年ならば 111 になります。

曜日を表す tm_wdayメンバは、日曜日を 0 として、0〜6 の範囲で表現されます。

tm_isdstメンバは、季節によって時間に調整を加えている国・地域のためのフラグになっています。 具体的な例でいえば、アメリカなどで導入されている夏時間(サマータイム)がありますが、 事実上、季節時間=夏時間と考えて良いようです。
世界協定時には、このような制度が存在していないので、このフラグは 0 になっています。 tm構造体は他でも使われるので、そちらではこのフラグが意味を持っている可能性があります。

夏時間は、夏は日没が遅いので、仕事を早めに切り上げて、明るい時間を有効活用しようという考えで導入されています。 国・地域ごとに詳細は異なりますが、例えば、13:59 の1分後を 15:00 とすることで実現されます。 17:00 で終業の会社があるとすれば、結果的に1時間早い時刻(本来なら 16:00)で切り上げられることになります (しかし時計は間違いなく 17:00 を指している訳です)


tm構造体の中身は、asctime関数(⇒リファレンス)を使って文字列化できます。

char* asctime(const struct tm* t);

引数に tm構造体のポインタを渡すと、文字列化して返却します。

#include <stdio.h>
#include <time.h>

int main(void)
{
	struct tm* utc;
	time_t t;
	
	t = time( NULL );
	utc = gmtime( &t );

	puts( asctime( utc ) );

	return 0;
}

実行結果

Sun Apr 03 04:07:27 2011


最後に、gmtime関数が返すポインタは、静的な変数を指している可能性があります。 そのため、2回以上 gmtime関数を呼び出したとき、同じアドレスが返されるかも知れません。 最初の呼び出しのときの情報を後で使いたければ、一旦、呼び出し元でコピーを取っておくべきです

ローカル時間

ローカル時間(地域時間)は、特定の国や地域における標準時のことです。 日本であれば、明石市を通る東経135度を標準時と定め、全国で統一しています。

ローカル時間の表現にも tm構造体を使います。 また、ローカル時間を取得するには、time関数の結果を localtime関数に渡します。 この辺りは、世界協定時を得るための流れと似ています。

localtime関数は、次のように宣言されています。

struct tm* localtime(const time_t *t);

このように、形式は gmtime関数(⇒リファレンス)と同じです。 実際に使ってみましょう。

#include <stdio.h>
#include <time.h>
#include <assert.h>

void printTm(const struct tm* t);

int main(void)
{
	struct tm* jst;
	time_t t;
	
	t = time( NULL );
	jst = localtime( &t );

	printTm( jst );

	return 0;
}

void printTm(const struct tm* t)
{
	assert( t != NULL );
	
	printf( "年       : %d\n", t->tm_year );
	printf( "月       : %d\n", t->tm_mon );
	printf( "日       : %d\n", t->tm_mday );
	printf( "曜日     : %d\n", t->tm_wday );
	printf( "時       : %d\n", t->tm_hour );
	printf( "分       : %d\n", t->tm_min );
	printf( "秒       : %d\n", t->tm_sec );
	printf( "通算日   : %d\n", t->tm_yday );
	printf( "季節時間 : %d\n", t->tm_isdst );
}

実行結果

年       : 111
月       : 3
日       : 3
曜日     : 0
時       : 13
分       : 7
秒       : 27
通算日   : 92
季節時間 : 0

gmtime関数のときのサンプルプログラムを、そのまま localtime関数に変更しただけです。
世界協定時とローカル時間との違いが、「時」にだけ現れたのは、要するに「時差」のためです。 日本は、世界協定時に比べて、「+9時間」の時差があります。

localtime関数の結果が、日本国内のローカル時間になるかは、コンピュータの設定や環境変数の設定などに依ります。 この辺りは、使用している OS の種類などによっても事情が異なるので一概には言えませんが、 普通、デフォルトでは日本の時間が使われるようになっているはずです。


ローカル時間の場合、time関数の結果を ctime関数(⇒リファレンス)に渡せば、 ある程度綺麗に整形して文字列化してくれます。

char* ctime(const struct tm* t);

ctime関数がしていることは、localtime関数の戻り値を asctime関数に渡すだけです。 ですから、単なるショートカットのような関数に過ぎません。

#include <stdio.h>
#include <time.h>

int main(void)
{
	time_t t;
	
	t = time( NULL );
	puts( ctime(&t) );

	return 0;
}

実行結果

Sun Apr 03 13:07:27 2011


また、mktime関数(⇒リファレンス)を使うと、 ローカル時間が格納された tm構造体のポインタから、time_t型の値を作り出せます。

time_t mktime(struct tm* t);

この関数は、tm構造体の値を time_t型に変換すると考えても良いのですが、 どちらかというと、time_t型の値を作り出すと考えた方が適切です。 関数名の「mk」は「make(作る)」から来ています。

具体的には、引数に渡す tm構造体(のポインタ)のメンバ tm_wday と tm_yday は無視されて、time_t型の値が作り出されます。 他のメンバの値が適切であれば、mktime関数内で tm_wday と tm_yday に正しい値が格納されます。 (mktime関数の引数が const になっていない点に注目です。内部で書き換わるということです。第37章
もし、メンバの値が不適切で、正しい time_t型の値が作り出せない場合は、-1 を time_t型にキャストした値が返されます。

#include <stdio.h>
#include <time.h>

int main(void)
{
	struct tm* jst;
	time_t t;
	
	t = time( NULL );
	printf( "%ld\n", (long int)t );

	jst = localtime( &t );
	printf( "%ld\n", (long int)mktime( jst ) );

	return 0;
}

実行結果

1301809706
1301809706

乱数

乱数とは、規則性なく作られる数の並びのことです。 時間や日付の話とは外れますが、若干の関連を持つのでここで解説しておきます。

乱数を生成する方法を知っておくと、ランダムな部分を持ったプログラムが作れるようになります。 例えば、現実世界でのサイコロのように、出た目に応じて処理を変えるようなことができます。

最初に、「規則性なく〜」と書きましたが、これは残念ながらコンピュータでは不可能です。 そのため、コンピュータが作り出す乱数は、擬似乱数と呼ばれています。
擬似乱数を作り出すには、rand関数(⇒リファレンス)を使います。

int rand(void);

この関数は、呼び出すたびに異なる整数を返します。 返される整数の範囲は、0〜RAND_MAX(⇒リファレンス) です。
なお、この関数は stdlib.h に宣言されています。

呼び出されるたびに異なる数を返すと言っても、擬似乱数ですから規則性を持ってしまっています。 何千、何万回もの呼び出しを繰り返せば、同じ数の並びが周期的に表れることが分かります。 また、次のプログラムを何度か実行すると分かるように、そもそも同じ値の並びしか返してくれません。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int i;

	for( i = 0; i < 20; ++i ){
		printf( "%d\n", rand() );
	}

	return 0;
}

実行結果

41
18467
6334
26500
19169
15724
11478
29358
26962
24464
5705
28145
23281
16827
9961
491
2995
11942
4827
5436

何度実行しても、実行結果は変わりません。 この問題を解決するには、srand関数(⇒リファレンス)を使います。

void srand(unsigned int seed);

srand関数は、乱数の種(シード値)を設定します。
擬似乱数を生み出すには、その元となる値が必要で、それを乱数の種と呼びます。 乱数の種に対して、何らかの計算を行い、その結果が rand関数の戻り値となります。 それと同時に、新たな乱数の種が作られ、次回の乱数を作る元になります。
先ほどのサンプルプログラムでは、srand関数を使わなかったので、デフォルトの乱数の種が使われます。 デフォルトの乱数の種はいつも同じ定数であるため、プログラムを何回実行しても、同じ数の並びしか生成されなかった訳です

しかし、乱数の種そのものが同じであれば、結局、生成される数の並びは変わらないため、

srand( 100 );

のような感じで、定数を渡していたのでは無意味です。 これだと、srand関数を呼ばなかったときとは異なる結果になるでしょうが、 プログラムを何回実行しても同じ数の並びになる現象は変わりません。 毎回異なる乱数が欲しければ、(矛盾した話ですが)乱数の種も乱数にしないといけないのです。
この矛盾を解決する鍵が、この章のテーマだった「時間」です。 プログラムを起動したときの時間というのは、恐らく毎回異なるであろうことから、これを乱数の種として使うのです。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
	int i;

	srand( (unsigned int)time(NULL) );

	for( i = 0; i < 20; ++i ){
		printf( "%d\n", rand() );
	}

	return 0;
}

実行結果

24581
7509
3789
26783
4399
29529
32633
12466
5097
24208
22247
1387
4598
18428
26406
2410
6408
26077
12162
6298

このように、time関数の戻り値を乱数の種に利用します。 今度は、実行するたびに異なる結果になるはずです。

time関数が返す結果は秒単位なので、1秒以内に再実行すれば同じ結果になってしまいますが、 ほとんどの場合、それが問題になることはないでしょう。


練習問題

問題@ サイコロのように、1〜6 の整数をランダムで返す関数を作って下さい。

問題A 標準入力から受け取った西暦年から、現在の西暦までの年数を求めるプログラムを作って下さい。

問題B プログラムの処理の進行を、5秒間停止させる方法を考えて、試してみて下さい。

問題C 標準入力から受け取った西暦年・月・日から、その日の曜日を求めて表示するプログラムを作って下さい。


解答ページはこちら

参考リンク

更新履歴

'2013/8/31 「ローカル時間」のサンプルプログラム内で変数名が間違っていたのを修正。

'2011/4/7 新規作成。



前の章へ

次の章へ

C言語編のトップページへ

Programming Place Plus のトップページへ