この章の概要です。
プロセッサ時間とは、プログラムの実行が開始されてから、そのプログラムが使用した時間のことです。
同時に他のプログラム(プロセス)が動いている場合、他方が使用した時間の分は含まれません。
現在のプロセッサ時間を取得するには、clock関数を使用します。clock関数は、time.h で、以下のように宣言されています。
clock_t clock(void);
引数はありません。
戻り値は、現在のプロセッサ時間が返されます。ただし、表現できない数になっている場合や、何らかの理由でプロセッサ時間を取得することができない場合には -1(を clock_t型にキャストした値)が返されます。clock_t型は、環境によって異なりますが、何らかの算術型(整数型と浮動小数点型の総称)を typedef したものです。
ここまでは単純なのですが、問題は返されるプロセッサ時間の精度です。
現実のストップウォッチなどをイメージすると分かりますが、何千分の一秒の単位で細かく計測できるものもあれば、秒単位でしか計測できないキッチンタイマーのようなものもあります。それと同じで、コンピュータの世界におけるプロセッサ時間も、環境ごとの精度の違いがあります。
実行環境のプロセッサ時間の精度は、CLOCKS_PER_SECマクロで定義されています。このマクロの置換結果は、「現実の1秒が、プロセッサ時間で幾つになるか」を表しています。
例えば、CLOCKS_PER_SECマクロの置換結果が 1000 であれば、現実の1秒は、プロセッサ時間で 1000 であるということです。逆に言えば、プロセッサ時間における 1 は、0.001秒だということになります。もし、clock_t型が整数型であれば、1 よりも細かい単位は表現できないので、その実行環境のプロセッサ時間の精度は 0.001秒単位だということになります。これより細かい時間を表すことは、プロセッサ時間では不可能だということです。
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関数を呼んで、プロセッサ時間を取得しておきます。2つのプロセッサ時間の差を見れば、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関数だけ取り上げておきます。difftime関数は、time.h に以下のように宣言されています。
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) とは、世界で統一されている標準時のことです。具体的には、イギリスのグリニッジ(経度0度)における時間を基準時としているので、グリニッジ標準時 (GMT) のことでもあります。
協定世界時を直接取得する標準ライブラリ関数はありませんが、time関数の結果を、gmtime関数に渡せば得られます。gmtime関数は、time.h に以下のように宣言されています。
struct tm* gmtime(const time_t *t);
time_t型の値は引数から渡しますが、ポインタ型になっています。少し面倒ですが、time関数の結果を渡したいのなら、time関数の結果を変数に受け取った後、その変数のメモリアドレスを渡す必要があります。
戻り値は、tm構造体のポインタです。tm構造体には多数のメンバがあり、そこに協定世界時を表現する各要素が格納されています。なお、変換に失敗した場合には NULL が返されます。
戻り値がポインタ型なのが心配になりますが(第33章)、実際そこは注意しなければなりません。このポインタは、静的記憶域期間を持った tm構造体変数を指している可能性があり、2回以上 gmtime関数を呼び出したときに返されるポインタは恐らく同一のものです。そのため、1回目の呼び出しで取得した情報を後で使いたいというケースでは、呼び出し元でコピーを取っておかないと、2回目の呼び出しで上書きされてしまいます。
tm構造体は、time.h に以下のように宣言されています。
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(C99 からは 60)まであり得ます。これは、閏秒(うるうびょう)を表現できるようにするための処置です。閏秒は、地球の自転速度がほんのわずかに変化していることで、生まれてしまう誤差を修正するために、挿入あるいは削除される調整のための秒のことを指します。
C99規格からは、上限が 60 に変更されました。以前はうるう秒が 2秒発生することを想定していたようです。
2018年までに秒が削除されたことは無いようです。もし、秒が削除された場合、tm_secメンバの値が 58 までしか増えない分があるはずです。
月を表す tm_monメンバは、0基準になっていますから、日本人感覚とは合致していません。例えば、「3月」なら「2」になります。
年を表す tm_yearメンバは、西暦1900年を基準とした経過年です。例えば、2018年ならば 118 になります。
曜日を表す tm_wdayメンバは、日曜日を 0 として、0~6 の範囲で表現されます。
tm_isdstメンバは、季節によって時間に調整を加えている国・地域のためのフラグになっています。季節時間になっているときは 1以上、なっていないときは 0、不明な場合は負の値です。
季節時間の具体例は、アメリカなどで導入されている夏時間(サマータイム)があります。これは一例ではあ
りますが、事実上、季節時間=夏時間と考えても良いようです。
世界協定時としては、季節時間の制度はないので、tm_isdstメンバは常に 0 になっています。tm構造体は他でも使われるので、そちらではこのフラグが意味を持っている可能性があります。
夏時間は、夏は日没が遅いので、仕事を早めに切り上げて、明るい時間を有効活用しようという考えで導入されています。国・地域ごとに詳細は異なりますが、例えば、夏時間に突入する瞬間に、時計の針を 1時間進めることで実現されます。普段 17:00 で終業の会社があるとすると、いつも通りに時計が 17:00 になったときに退社すると、それは本来は 16:00 なので、まだ明るいうちに帰宅できることになります。夏時間期間が終わったら、時計の針を 1時間逆戻りさせます。
次のプログラムは、プログラムを実行した時刻を、協定世界時で出力します。
#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 ); }
実行結果
年 : 118 月 : 2 日 : 26 曜日 : 1 時 : 1 分 : 37 秒 : 56 通算日 : 84 季節時間 : 0
tm構造体の内容は、asctime関数を使って文字列化することができます。asctime関数は、time.h に以下のように宣言されています。
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; }
実行結果
Mon Mar 26 01:37:56 2018
ローカル時間(地域時間)は、特定の国や地域における標準時のことです。日本であれば、明石市を通る東経135度を標準時と定め、全国で統一しています。
ローカル時間を表現するためにも tm構造体が使われます。また、ローカル時間を取得するには、time関数の結果を localtime関数に渡します。この辺りの流れは、世界協定時を得るための流れと似ています。
localtime関数は、time.h に以下のように宣言されています。
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 ); }
実行結果
年 : 118 月 : 2 日 : 26 曜日 : 1 時 : 10 分 : 37 秒 : 56 通算日 : 84 季節時間 : 0
gmtime関数のときのサンプルプログラムを、そのまま localtime関数に変更しただけです。世界協定時とローカル時間との違いが「時」にだけ現れていますが、これは要するに時差です。日本のローカル時間は、世界協定時に比べて「+9時間」の時差があります。
また、ローカル時間の場合には、tm_isdstメンバに季節時間に応じた値が格納される可能性があります。日本では無関係なので、常に 0 のはずです。
localtime関数の結果が、どの地域のローカル時間になるのかは実行環境の設定に依ります。この辺りは、使用している OS の種類などによっても事情が異なるので一概には言えませんが、普通、日本の製品はデフォルトで日本の時間が使われるようになっているはずです。
ローカル時間の場合、time関数の結果を ctime関数に渡せば、文字列化してくれます。ctime関数は、time.h に以下のように宣言されています。
char* ctime(const struct tm* t);
ctime関数がしていることは、localtime関数の戻り値を asctime関数に渡すだけです。そのため、単なるショートカットのような関数に過ぎません。asctime関数が返す文字列のフォーマットは規格で定められているので、ctime関数の結果もまた、常に同じ形式になります。
#include <stdio.h> #include <time.h> int main(void) { time_t t; t = time( NULL ); puts( ctime(&t) ); return 0; }
実行結果
Mon Mar 26 10:37:56 2018
また、mktime関数を使うと、ローカル時間が格納された tm構造体のポインタから、time_t型の値を作り出せます。mktime関数は、time.h に以下のように宣言されています。
time_t mktime(struct tm* t);
この関数は、tm構造体の値を time_t型に変換すると考えても良いのですが、どちらかというと、time_t型の値を作り出すと考えた方が適切です。関数名の「mk」は「make(作る)」から来ています。引数で指定したポインタが指す tm構造体に格納されている情報を、ローカル時間であるとみなし、その時刻と一致する time_t型の値を作って返します。
このとき、メンバ tm_wday(曜日)と tm_yday(年始からの通算日)の値は無視されます。また、これら以外のメンバについては、gmtime関数や localtime関数が返すときのような値の範囲を逸脱していても構いません。例えば、tm_min(分)が 90 になっていても、1時間と 29分のことであると認識されます。
また、メンバtm_isdst(季節時間)の値が、結果の生成に影響を与えます。この値が 1以上なら、季節時間の影響下にあるとして結果を決めますし、0 なら季節時間の影響下にないと判断します。負数の場合は、mktime関数自身が判断を下します。やはり日本では関係ないので、0 を入れておきます。
各メンバの値に問題がなく、time_t型の値を生成できた場合は、メンバ tm_wday(曜日)と tm_yday(年始からの通算日)に、正しい値が格納されます。また、ほかのメンバについても、適切な範囲に収まるように修正されます。例えば、tm_min(分)が 90 になっていたら、tm_min を 30 に直し、tm_hour に +1 するでしょう。
以下は mktime関数の使用例です。
#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; }
実行結果
1522030822 1522030822
乱数とは、規則性なく作られる数の並びのことです。時間や日付の話とは外れますが、若干の関連を持つのでここで解説しておきます。乱数を生成する方法を知っておくと、ランダムな部分を持ったプログラムが作れるようになります。例えば、現実世界でのサイコロのように、出た目に応じて処理を変えるようなことができます。
最初に「規則性なく~」と書きましたが、これは残念ながらコンピュータには難しいことです。通常、コンピュータが作り出す乱数は、特定の計算式を使って作り出したものです。これは最初の値が固定されていたら、何度やっても同じ値しか生み出せません。このような乱数は、擬似乱数と呼ばれています。
擬似乱数を作り出すには、rand関数を使います。rand関数は、stdlib.h に以下のように宣言されています。
int rand(void);
この関数は、呼び出すたびに異なる整数を返します。返される整数の範囲は、0~RAND_MAX です。RAND_MAX は最低でも 32767 です。
呼び出されるたびに異なる数を返すと言っても、擬似乱数ですから規則性はあります。多くの回数、繰り返し呼び出し続ければ、いずれ同じ並びが現れます。
また、次のプログラムを何度も実行してみると分かりますが、そもそも同じ値の並びしか返ってきません。
#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
実行結果に並ぶ数値は、環境によって異なりますが、何度実行しても結果が変わることはありません。これでは乱数を使う意味があまりなさそうです。
こうなってしまうのは、前述したように、疑似乱数は特定の計算式で乱数を作るからです。例えば「x = a * B + C」のような計算式を使うとして、B と C を定数、a だけを変化させます。計算結果 x を次回の a として使うようにして、繰り返し異なる値を生成します。
すると、1回目の a をどう決めるのかという部分が問題になります。1回目の a の値を、シード値(乱数の種)と呼びます。
シード値は、特に指定しなければデフォルトの値が使われます。そのため、先ほどのサンプルプログラムの場合、いつも同じ結果しか生み出せないのです。
そこで、シード値を設定するsrand関数を使います。srand関数は stdlib.h に、以下のように宣言されています。
void srand(unsigned int seed);
引数に、シード値を指定します。
しかし、シード値を設定できるといっても、以下のように使うのでは、結局同じ乱数しか生成できないことに変わりがありません。
srand( 100 );
これでは、srand関数を呼ばなかったときとは異なる結果にはなるでしょうが、「x = a * B + C」という計算式の1回目の a が 100 になるだけですから、結局、いつも順番で乱数が生み出されることに変わりはありません。つまり、プログラムを実行するたびに毎回異なる乱数が欲しければ、(矛盾した話ですが)シード値も乱数でなければならないのです。
#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秒以内に再実行すれば同じ結果になってしまいます。これは(時刻の設定が同じ)複数のコンピュータで一斉に同じプログラムを実行したら、すべてが同じ結果になるかも知れないことを意味しています。
また、rand関数の質はそれほど高いものではありません。むしろ、現代的にはかなり悪いものとされることが多いため注意が必要です。乱数は、サイコロとか、無作為の抽選の実装に使われることがあり、不正行為や、セキュリティの穴を突くような行為と無関係では済みません。質の悪い乱数生成機能は、こういう観点で問題になる恐れがあります。
信頼性の高いプログラムを作らねばならない場合には、その環境に合った、より良い乱数生成機能を調査し、慎重に実装して下さい。
問題① サイコロのように、1~6 の整数をランダムで返す関数を作って下さい。
問題② 標準入力から受け取った西暦年から、現在までの年数を求めるプログラムを作って下さい。
問題③ プログラムの処理の進行を、5秒間停止させる方法を考えて、試してみて下さい。
問題④ 標準入力から受け取った西暦年・月・日から、その日の曜日を求めて表示するプログラムを作って下さい。
'2018/3/26 全面的に文章を見直し、修正を行った。
'2013/8/31 「ローカル時間」のサンプルプログラム内で変数名が間違っていたのを修正。
'2011/4/7 新規作成。
![]() |
ツイート | Follow @pplace_ky | |
![]() |
前の章へ(第50章 列挙型、共用体、ビットフィールド)
次の章へ(第52章 可変個引数)
Programming Place Plus のトップページへ