
この記事の要点
- 乱数を生成する際は、rand関数を利用する
- 乱数の種をばらつかせることで、常に同じ乱数パターンになることを防ぐ
- time関数とsrand関数を組み合わせて、乱数の種を変更する
以前、C言語での副業の種類や必要なスキルについてご紹介していました。
今回は、C言語の乱数の生成について解説します。
C言語で乱数を生成する際は、「rand関数」を利用します。
乱数の種をばらつかせることで、常に同じ乱数パターンになることを防ぐことができるので、「time関数」と「srand関数」を組み合わせることがポイントです。
また、そもそも乱数とは?乱数の種とは?といった初心者向けの解説も丁寧に行っています。
C言語を使ってゲームを作ってみたい初心者の方には、必見の内容です。
乱数とは
乱数とは、ランダムな数字のことです。
乱数は、ゲームなどでよく使われます。
例えば、敵に与えるダメージや受けるダメージ、フィールドでエンカウントする敵の種類などをばらつかせるために用いられます。
ただし、rand関数で作るのは、計算によって生成する疑似乱数です。
同じ実装で、乱数の種と内部の状態が同じなら、同じ順番で数字が生成されます。
このように、計算によって「ランダムに見える数字」を生成します。
これを疑似乱数と呼びます。
当記事の「乱数」というワードは、全て疑似乱数のことです。
顧客の現場でAIやシステムを実装するFDEが、C言語の既存処理と連携する場合にも、乱数を使ったテストデータの作成にこの知識を役立てられます。
rand関数
多くのプログラミング言語では、乱数を作るためのライブラリが存在します。
C言語では、rand関数が用意されています。
rand関数の仕様は、以下の通りです。
| 概要 | 0~RAND_MAXの範囲の疑似乱数を生成し、返す |
|---|---|
| includeするヘッダ | stdlib.h |
| 形式 | int rand(void); |
| 引数 | なし |
| 戻り値 | 0~RAND_MAXの範囲の整数 ※RAND_MAXの値は環境により異なる |
rand関数を使ったサンプルと、実行結果はこちらです。
以下のコードは、for文の中で変数を宣言できるC99以降を想定しています。掲載している数値は実行結果の一例です。RAND_MAXや乱数列は利用する処理系によって異なります。
サンプルコード
#include <stdio.h>
#include <stdlib.h>
int main(void){
printf("RAND_MAXの値:%d\n", RAND_MAX);
for( int i = 0; i < 10; ++i ){
printf( "%d\n", rand() );
}
return 0;
}
実行結果
RAND_MAXの値:2147483647 1804289383 846930886 1681692777 1714636915 1957747793 424238335 719885386 1649760492 596516649 1189641421
こちらの環境では乱数の最大値であるRAND_MAXは「2147483647」でした。
実行結果で得られた乱数は、「0~2147483647」の範囲の値になっています。
しかし、実行するたびに異なるパターンにしたい場合、このプログラムには1つ問題があります。
それは、同じ実行環境では、何度実行しても毎回同じ乱数のパターンになるという点です。
こちらの環境で、このプログラムを2回目に実行した結果がこちらです。
2回目の実行結果
RAND_MAXの値:2147483647 1804289383 846930886 1681692777 1714636915 1957747793 424238335 719885386 1649760492 596516649 1189641421
1回目と全く同じパターンです。
これは、次に説明する「乱数の種」が変わっていないからです。
srand関数を呼ばずにrand関数を使った場合は、srand(1)を呼んだ場合と同じ乱数列になります。
乱数の種とは
乱数の種(シードとも呼ばれます)とは、乱数を計算する際の元になる値です。
詳細は省きますが、rand関数では以下のように乱数を生成しています。
↓
2回目の呼び出し:内部の状態を更新しながら、次の乱数を生成する。
↓
3回目の呼び出し:さらに内部の状態を更新しながら、次の乱数を生成し…(以下繰り返し)
そのため、同じ実装で乱数の種が同じなら、乱数のパターンは毎回同じになります。
つまり、実行するたびに乱数のパターンを変えたい場合は、実行開始時に乱数の種を変える方法があります。
time関数
よく使われるのは、現在の日時を利用するテクニックです。
C言語では、標準ライブラリのtime関数で現在の日時情報を取得できます。
time関数の仕様は、以下の通りです。
POSIX環境やWindowsのCランタイムでは、1970年1月1日0時0分0秒(UTC)からの経過秒数として扱います。ただし、C言語の規格自体はtime_tの具体的な表現を定めていません。
| 概要 | 現在の暦時刻をtime_t型で返す |
|---|---|
| includeするヘッダ | time.h |
| 形式 | time_t time(time_t * timer); |
| 引数 | 得られた日時情報の格納先(NULLの場合、格納しない) |
| 戻り値 | 得られた日時情報。取得できない場合は(time_t)-1 |
time関数で得た日時情報を乱数の種とすることで、実行時刻に応じて乱数のパターンを変えられます。ただし、同じ時刻の値を取得した場合や、unsigned intへの変換後に同じ値となった場合は、同じパターンになります。
srand関数
次に、乱数の種を指定する方法について説明します。
C言語では、乱数の種を指定する際にはsrand関数を使います。
srand関数の仕様は、以下の通りです。
| 概要 | rand関数で使う乱数の種を変更する |
|---|---|
| includeするヘッダ | stdlib.h |
| 形式 | void srand(unsigned int seed); |
| 引数 | 乱数の種とする値 |
| 戻り値 | なし |
サンプルと、実行結果はこちらです。
サンプルコード
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void){
time_t current = time(NULL);
if(current == (time_t)-1){
return 1;
}
unsigned int now = (unsigned int)current;
printf("乱数の種:%u\n", now);
srand(now);
for( int i = 0; i < 10; ++i ){
printf( "%d\n", rand() );
}
return 0;
}
1回目の実行結果
乱数の種:1665923559 89276058 696401342 623232829 1410674853 1959107951 1868423341 880103630 160157705 605191914 1341827754
2回目の実行結果
乱数の種:1665923584 1179583 320351406 1834644553 1415123332 559976261 833328016 2006943341 961421594 893285590 1590576049
今回は、乱数の種に現在の日時を利用しているため、1回目と2回目で生成される乱数のパターンが変わっています。
srand関数は通常、rand関数を繰り返し呼ぶ前に1回実行します。時刻を秒単位で取得する環境で短時間に何度も初期化すると、同じ種を設定し直し、同じ値を繰り返す場合があります。
範囲指定して乱数を生成する方法
実際には、「0~10までの範囲でランダムな値」など、範囲指定して乱数を生成したい場合がほとんどです。
範囲指定してランダムな整数を生成するサンプルコードをご紹介します。
サンプルコード
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int getRandomNumber(int min, int max){
return rand() % (max - min + 1) + min;
}
int main(void){
time_t current = time(NULL);
if(current == (time_t)-1){
return 1;
}
unsigned int now = (unsigned int)current;
srand(now);
for( int i = 0; i < 10; ++i ){
printf( "%d\n", getRandomNumber(0, 10) );
}
return 0;
}
使いやすいように、乱数生成処理は関数化しています。
第一引数に乱数の最小値、第二引数では乱数の最大値を指定します。
範囲指定には、「どんな自然数でも、自然数nで割った余りは0~n-1の範囲になる」ことを利用しています。
このサンプルは、0~10のような小さい範囲を扱う簡単な例です。minはmax以下とし、max - min + 1が正のintに収まる範囲で使ってください。範囲の幅がrand関数の出力範囲を超えると、生成できない値が出ます。
また、剰余(%)を使う方法では、範囲の幅で乱数の出力範囲を等分できない場合に偏りが出ます。厳密に均等な確率が必要な抽選などには、そのまま使わないようにしましょう。
乱数を使って、同じ条件でテストをやり直す
乱数はゲームだけでなく、プログラムを試すための架空データにも使えます。例えば、0~10の整数を受け取る処理を修正したとき、修正前後に同じ入力を与えれば結果を比較できます。
種と実行環境を記録する
先ほどの範囲指定の例で、時刻から種を作る部分を外し、ループの前にsrand(1234);を置いて試してみてください。同じ実装で同じ種を使い、randを同じ順番・回数で呼ぶことが、入力を再現する条件です。
不具合を見つけたら、種だけでなく、使った実行環境と生成した入力列も保存します。途中でrandを呼ぶ処理を追加した場合や実行環境を変えた場合は、同じ種だけでは比較条件をそろえられないことがあります。
境界値は乱数とは別に用意する
乱数で10件試しても、必要な値がすべて出るとは限りません。0と10、範囲外の-1と11は固定の入力として別に試します。入力件数0件のときの動作も確認すると、乱数任せでは見つけにくい問題を調べられます。
範囲外の値は乱数生成関数に無理な引数を渡して作るのではなく、生成した値を受け取る側の処理へテスト入力として渡します。この記事の剰余を使う例は、均等な抽選や認証コードには使いません。
C言語とAIをつなぐとき、乱数だけでは試せないこと
AIとC言語の接点には、学習済みモデルをCのプログラムから呼び出す方法があります。例えば、ONNX RuntimeのC APIでは、モデルを読み込み、入力データを渡して推論結果を受け取れます。
用途の一例として、Cで取得・加工したセンサーの値をモデルへ渡し、機器の状態を判定する構成が考えられます。その接続部分を作る段階なら、乱数のダミーデータで入力が流れるかを試せます。
ただし、乱数を流して動いたことと、実際の異常を正しく判定できることは別です。現場のデータには、値が徐々に変わる、一定値のまま止まる、通信が途切れて欠損するといった状況もあります。
0~10をばらばらに生成するだけでは、それらの状況を再現できません。固定値や欠損を含む入力、利用が認められた実測データなどを目的に応じて用意し、受け渡しのテストとAIの判定精度の評価を分けましょう。
C言語の乱数についてよくある質問
何度動かしても同じ数字になるのはなぜ?
srand関数を呼んでいないか、毎回同じ種を指定している可能性があります。time関数を使っていても、同じ秒のうちに起動すると同じ種になる場合があります。まずは、サンプルのように種の値を表示して確認してみてください。
テストのために、あえて同じ乱数を出せる?
srand(1234)のように種を固定すると、同じ実装では同じ乱数列を再現できます。不具合が出た条件をもう一度試したいときに役立ちます。ただし、異なる処理系でも同じ数列になるとは限りません。
パスワードや認証コードにもrand関数を使っていい?
rand関数は暗号用途の安全性を備えていないため、使わないようにしてください。こうした用途では、利用するOSやライブラリが提供する、暗号用途に適した乱数生成機能を選びます。
まとめ
今回は、乱数の生成について解説しました。
乱数生成は、RPGなどのゲームを制作する場合にはほとんど必須の処理です。
しかし、rand関数はよく仕様を調べてから使わないと、毎回乱数のパターンが同じになってしまうため、注意が必要です。
乱数生成する方法は、忘れたらまた調べればよいだけなので暗記する必要はありません。
rand関数で生成するのは疑似乱数であることや、同じ実装で同じ種を指定すると同じパターンを再現できることなど、根本的な知識はしっかり身に着けておきましょう。
また、今後ゲーム開発をしていきたい方に向けて、C言語でゲーム制作をするための具体的なステップや、便利なライブラリについて解説しています。
C言語ならメモリが少ないハードでもゲーム開発は可能なので、参考にしてください。