Menu

C言語の乱数:rand、srand、範囲内の数を作る方法

C言語で rand() と RAND_MAX を使って乱数を生成する方法、srand(time(NULL)) でちょうど一度だけ種を与える理由、% による範囲への写像とそこで生じるわずかな偏り、乱数の double、そして再現可能な系列。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

C言語の乱数は <stdlib.h> の2つの関数から来ています。次の値を生む rand() と、出発点を決める srand() です。これらは真の乱数ではありません - 種から決定的に計算される疑似乱数の系列であり、これは暗号にとっては制約であり、テストにとっては利点です。

rand() と RAND_MAX

rand()0 以上 RAND_MAX 以下のどこかの int を返します。RAND_MAX は少なくとも 32767 であることが保証されたマクロで、LinuxとmacOSでは 2147483647 です。

これを2回実行してみてください。どちらも同じ数が並びます - そしてこれはバグではありません。

srand で種を与える

srand を呼ばなければ、系列は srand(1) を呼んだのと同じように振る舞います。同じ種、同じ系列が毎回続きます。実行ごとに違う数を得るには、変化する何かを種にします - 慣習的には現在時刻です。

<time.h>time(NULL) は1970年の始まりからの秒数を返すので、実行ごとに違う種が得られます。unsigned int へのキャストは、time_t を狭めることに対する警告を黙らせるためのものです。

種の与え方には3つの規則があり、そのどれもがよく間違われます。

種はちょうど一度だけmain の先頭で与えます。rand() のたびに srand を呼ぶのが古典的なアンチパターンです - 1秒未満で終わるループの中では time(NULL) は毎周同じ値を返すので、同じ数で種を与え直すことになり、rand() は毎回同じ最初の値を返します。出力は同じ「乱数」が並ぶ一列になります。

ランダム性を「良くする」ために種を与え直さないこと。 生成器の品質は内部状態が進んでいくことから来ています。その状態をリセットするのは、系列を捨てることです。

time(NULL) の分解能は1秒です。 同じ秒内に起動した2つのプログラムは同じ系列を得ます。ゲームなら問題ありませんが、独立性が重要な場面では誤りです。

範囲内の数

標準的な書き方は剰余演算子を使います。

rand() % n            /* 0 から n-1 まで */
rand() % n + min      /* min から min+n-1 まで */

min 以上 max 以下を得るには、取り得る値の個数は max - min + 1 です。

off-by-one(1つずれ)のバグが住んでいるのは + 1 の部分です。rand() % 6 は 0 から 5 を返すので、サイコロは rand() % 6 + 1 になります。「6を含めるため」に rand() % 7 + 1 と書くと、7面のサイコロができあがります。

剰余バイアスについての正直な注記

rand() % n は、nRAND_MAX + 1 をちょうど割り切る場合を除いて、完全に一様ではありません。

小さな数で考えてみましょう。RAND_MAX が 9 だとすると - rand() は等確率な10個の値、0から9を返します - rand() % 3 は 0,3,6,9 を 0 に、1,4,7 を 1 に、2,5,8 を 2 に写像します。結果 0 は10通り中4通り、結果 1 と 2 はそれぞれ3通りです。0 は 33% 出やすいことになります。

同じ偏りが実際の RAND_MAX でも存在しますが、はるかに小さいだけです。余った値は最初の (RAND_MAX + 1) % n 個の結果で、それぞれがおよそ21億分の1のチャンスを1回余分に得ます。サイコロ、シャッフルしたトランプ、シミュレーションにとっては測定できない差です - % を使って先に進みましょう。

それが本当に問題になるとき - 統計的な仕事や、セキュリティに関わるもの - は、余った値を折り込むのではなく棄却します。

このループは偏りを生む小さな範囲の値を捨てて引き直します。すぐに終了します - 棄却される部分は全体のごくわずかな割合だからです。

本当にセキュリティが重要なランダム性については、どれだけ注意を払っても rand() は不適切な道具です。macOSとBSDでは arc4random_buf、Linuxでは getrandom()、Windowsでは BCryptGenRandom を使ってください。

乱数の double

RAND_MAX で割って [0.0, 1.0] に収め、それから拡大縮小します。

(double) rand() のキャストは必須です。これがないと rand() / RAND_MAX は整数除算になり、ほとんど常に 0 へ、20億分の1の確率で最大値に当たったときだけ 1 へ評価されます - 「乱数の double が全部ゼロになる」ように見えるバグです。理由は型キャストを参照してください。

再現可能な系列

固定した種は毎回まったく同じ系列を与えます。これはテスト、デバッグ作業、あるいは共有できるレベルコードを持つゲームにまさに欲しいものです。

種 42 は、使うたびに同じ5つの数を生みます。この実行でも、同じライブラリ上の他のどの実行でも同じです。この再現性こそ、シミュレーションで種を選べるようにすべき理由です。通常は時計で実行し、バグを再現するときは固定した種を渡します。

ひとつ注意があります。ある種に対する系列は可搬ではありません。Cライブラリごとに生成器が異なるので、glibc の種 42 と Windows の種 42 は違う数を返します。1台のマシンの中では再現できますが、マシンをまたぐと再現できません。

サイコロゲーム

すべてを合わせたもの - 一度だけ種を与え、範囲のためのヘルパーを作り、配列で結果を集計します。

ヒストグラムは 7 で山を作り、2 と 12 に向かって細くなるはずです - 7 を作る方法は6通りあり、2 や 12 を作る方法はそれぞれ1通りだからです。ここで平らな分布を出す生成器は壊れています。

関連する2つのページ:標準ライブラリは <stdlib.h> の残りを地図にし、数学関数は <math.h> を扱います。乱数が実際の計算に流れ込むようになれば、すぐに必要になるものです。

よくある質問

C言語で乱数を生成するには?

<stdlib.h> をインクルードし、main の先頭で一度だけ srand((unsigned) time(NULL)) により種を与え(<time.h> が必要です)、あとは値ごとに rand() を呼びます。rand() は 0 以上 RAND_MAX 以下の int を返します。

C言語で2つの値の間の乱数を得るには?

rand() % (max - min + 1) + min を使います。1から6のサイコロなら rand() % 6 + 1 です。% n が結果を 0..n-1 に写像し、min を足すと範囲がずれます - 両端を含む個数になるよう、+ 1 を忘れないでください。

なぜC言語のプログラムは毎回同じ乱数を表示するのですか?

srand を一度も呼んでいないからです。種がなければ rand() は 1 を種としたのと同じように振る舞うので、どの実行でも同じ系列になります。プログラム開始時に srand((unsigned) time(NULL)) を一度だけ呼びましょう - 一度だけです。rand() の呼び出しごとに種を与えると、かえって悪化します。

乱数生成における剰余バイアスとは何ですか?

rand() % n が完全に一様になるのは、nRAND_MAX + 1 を割り切るときだけです。そうでなければ最初のいくつかの値が全範囲で1回多く現れ、ごくわずかに出やすくなります。RAND_MAX が 2147483647 で n が小さければ、その偏りはゲームやシミュレーションが気づく水準をはるかに下回ります。しかし暗号や統計の用途では、棄却ループか本格的な生成器を使ってください。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める