[前へ]   [目次へ]   [次へ]

疑似乱数の生成(1)(C/C++)

さて、それでは改めて疑似乱数の生成をやってみます。
疑似乱数を生成、取得するには rand 関数を使います。
rand 関数は stdlib.h に定義されています。
rand 関数には引数がなく、生成された疑似乱数値を戻り値(関数の実行後の値)として返します。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
   
int n;
   n=rand();
//(1)1回目の疑似乱数を取得
   
printf("%d\n",n);
   n=rand();
//2回目の疑似乱数を取得
   
printf("%d\n",n);
   n=rand();
//3回目の疑似乱数を取得
   
printf("%d\n",n);
   n=rand();
//4回目の疑似乱数を取得
   
printf("%d\n",n);
   n=rand();
//5回目の疑似乱数を取得
   
printf("%d\n",n);
   
//終了待ち
   
getchar();
   
return 0;
実行例:
41
18467
6334
26500
19169


7行目(1)の解釈について、解説します。
A B C    D D'
n = rand ( )


通し記号種別
Aint不定(未初期化)変数(n)
B演算子(=)(2項、算術、優先2、結合←) 
Cint(*)(void)0x004014B0関数(rand)
D演算子( () )(その他、優先16、結合→)
D'演算子Dの終点
優先順位から、演算子D(関数呼び出し、左辺値 関数C )から処理します。
この処理の結果、 rand 関数が呼び出され、疑似乱数が返され、一時変数に格納されます。

A B C
n = vtemp1


通し記号種別
Aint不定(未初期化)変数(n)
B演算子(=)(2項、算術、優先2、結合←) 
Cint16254一時変数

ここで、一時変数Cの値は基本的にコンパイラによって異なります。(理由は後述)
この後、一時変数Cが変数Aに代入されて処理終了です。


さて、「乱数」とはその名の通り「ランダムな値」であり、取得するたびに異なる値が取れるはずです。
上の実行例と実際の実行結果が異なったとしても、それは当然のことです。

では、何故「疑似」乱数等と呼んでいるのか?と言えば・・・
上のソースを数回繰り返し実行してみてください。
大抵のコンパイラでは、毎回必ず同じ値が取得されていることが分かるはずです。

実は、 rand 関数はある種の計算式を実行するものであり、
その計算結果は見掛け上バラバラではあるものの、同じ条件を使えば同じ値が返されるのです。
そして、計算式に使用する値は標準ライブラリ内にあるただの変数です。
そのため、 プログラム起動時には必ず同じ状態が生成されている のです。

この変数は rand 関数を呼び出す度に更新されるため、呼び出すごとに値は変化します。
そのため、同じプログラム内で実行された1〜5回目は異なる値を返すのに、
プログラムを再実行すると同じ値が返されてくることになります。

これでは、ちょっと困るので、 srand 関数を使って rand 関数が疑似乱数の生成に使用する変数を変更できるようになっています。
srand 関数は stdlib.h に定義されていて、引数として新しい疑似乱数生成用の値(型は unsigned int 、乱数の種とも呼びます)を一つ取り、戻り値はありません。
とはいえ、 srand 関数に渡す値がいつも同じでは結局、 rand 関数は毎回同じ値を返すことになってしまいます。
毎回違う値が入っていると言える値として、現在時刻を使用するという常套手段があります。

現在時刻を取得する関数として time 関数があります。(time=時間。そのまんまですね(笑))
time 関数は time.h に定義されているので、 #include <time.h> を追加しておいてください。

time 関数は第一引数に現在時間の格納先(型は time_t* )、
戻り値として現在時間を返します(型は time_t )。
time_t 型は初登場ですが、古いコンパイラでは大抵32ビットの符号付き整数( long あたりと大体等価です)、
新しいコンパイラでは64ビットの符号付き整数だったりします。

この関数は何故か戻り値と引数に同じ値を格納します。
何故このような仕様なのかは私の知るところではありませんが、
戻り値を使えば十分なので引数には NULL を指定しておいてください。

なお、 time 関数は1154193672のような異様に大きい値を返しますが、
この値は1970年1月1日0時0分0秒(UTC)からの累積経過秒数です。
UTCってのは「協定世界時」といって世界全体の時間の原点です。
各国の時間はUTCに時差を加減算して求めます。
日本時間は「JST」(日本標準時)で、UTC+9時間とされています。
なので、日本時間ベースだと1970年1月1日午前9時0分0秒からになります。
なお、Windowsのタイムゾーンの設定にはGMTと書いてありますが、GMTは実質UTCと同じ時間を指します。


なお、 time_t 型が32ビットである場合、2038年問題が指摘されています。
これは、32ビット符号付き変数の保持限界である約21億に、2038年1月に1970年からの累積秒が達し、
オーバーフローを発生させることによります。
これを回避するために、新しいコンパイラは time_t 型を64ビットに変更しています。
リミットまではおよそ31年強ですが、それまでには全てのプログラムが time_t 型が64ビットのコンパイラで
コンパイルし直され、2000年問題の時のような騒動にならないことを祈ります。


さて、上記のように time 関数は整数値を返すので、
srand 関数の引数にそのまま渡すことができます。
srand 関数の引数の型は unsigned int で、 time 関数が返すのは time_t 型なので、一見型が合わないのですが、
大抵のコンパイラなら暗黙の型変換(代入時バージョン)により渡せるようになっています。

という訳で、これらの処理を加えてやり直してみましょう。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
   
int n;
   srand(time(NULL));
//(1)rand関数を初期化する。
   //人によっては明示的型変換(基本型をキャスト)を行い、以下のように書く場合もある。
   //srand((unsigned int)time(NULL));
   
   
n=rand();//1回目の疑似乱数を取得
   
printf("%d\n",n);
   n=rand();
//2回目の疑似乱数を取得
   
printf("%d\n",n);
   n=rand();
//3回目の疑似乱数を取得
   
printf("%d\n",n);
   n=rand();
//4回目の疑似乱数を取得
   
printf("%d\n",n);
   n=rand();
//5回目の疑似乱数を取得
   
printf("%d\n",n);
   
//終了待ち
   
getchar();
   
return 0;
実行例1:
13895
7686
23800
32131
1004

実行例2:
13983
2981
14613
26480
17353

今回は上記のように、実行するたびに異なる値が出力されるはずです。
ただし、 time 関数は秒単位の値を返すので、1秒以内に再実行すると同じ結果になってしまいます。
最も、普通乱数を使うプログラムを1秒以内に再実行することはないので、気にしなくてもいいでしょう。
注意点は、 最初に一度だけsrand関数を呼ぶ ことです。
実行中に何度も srand 関数を呼ぶと上の「1秒以内に再実行」が発生しやすくなり、乱数っぽくなくなることがあります。

ここで、8行目(1)の解釈について、解説します。
A     B C    D E    D' B'
srand ( time ( NULL )  )


通し記号種別
Avoid(*)(unsigned int)0x00401510関数(srand)
B演算子( () )(その他、優先16、結合→) 
Ctime_t(*)(time_t*)0x00401550関数(time)
D演算子( () )(その他、優先16、結合→)
Evoid*0x00000000定数(NULL)
D'演算子Dの終点
B'演算子Bの終点
優先順位および結合規則から、演算子B(関数呼び出し、左辺値 関数A 右辺値 複合式C〜D' )から処理します。
しかし、右辺値が複合式のままでは処理が続かないので複合式を先に処理します。

複合式 C〜D'
C    D E    D'
time ( NULL )


通し記号種別
Ctime_t(*)(time_t*)0x00401550関数(time)
D演算子( () )(その他、優先16、結合→)
Evoid*0x00000000定数(NULL)
D'演算子Dの終点
演算子は 演算子D(関数呼び出し、左辺値 関数C 右辺値 定数E )一つなのでそれが呼び出されます。

と、ここで NULL について少し解説しておきます。
NULL はC/C++において、「無効なポインタ」と定義されています。
基本的にポインタ型( time_t* など * が含まれる型)を引数に取る関数には
有効な変数へのポインタを指定しなければいけません。
そうしないと、大抵プログラムが異常終了します。

しかし、例外的に「NULL可」とされている場合は、その引数を NULL にすることができます。
また、「NULL可」とされている場合は、 NULL の場合の動作が明記されているはずです。
time 関数の場合、第一引数は「NULL可」で、 NULL の場合は「無視」とされています。

また、戻り値に NULL を返してくる可能性のある関数もあります。
このような関数では、必ず NULL かどうかを確認しなければいけません。
この辺はまだ先なので今は深く語りません。

なお、 NULL の型はC言語では通常 void* ですが、C++ではただの0です。
実は、C++では0が特別扱いされているので、 int 型としてもポインタ型としても解釈されます。
この奇妙な仕様のせいで時々変な現象が起きることがあるので、このことは覚えておいたほうがいいです。
具体的に、C++で以下のようなことが起きます。(Cだと両方とも通りますが、実行すると異常終了します)
printf(10);//エラー「const intからconst char*へ変換できない」
printf(0);//通ってしまう。実行すると異常終了する。


さて、長くなったのでここで切ります。
次回は、 rand 関数が返す値の範囲と任意の範囲の値を取得する方法についてです。

[前へ]   [目次へ]   [次へ]

プログラミング講座 総合目次

最終更新 2008/10/16