C言語で乱数を生成する方法:入門者向け解説


C言語で乱数を生成する方法:入門者向け詳細解説

プログラミングの世界では、時に予測不可能な要素を取り入れたい場合があります。ゲームの敵の出現パターン、シミュレーションにおけるランダムなイベント、セキュリティに関わる鍵の生成など、様々な場面で「乱数」が必要になります。C言語も例外ではなく、標準ライブラリを用いて乱数を生成する機能を提供しています。

しかし、「乱数」と聞くと、どうすればいいのか難しそうに感じる入門者の方もいるかもしれません。この記事では、C言語で乱数を生成するための基本的な考え方から、具体的なコードの書き方、さらには知っておくべき注意点まで、初心者の方にも分かりやすく、かつ詳細に解説していきます。約5000語というボリュームで、皆さんがC言語での乱数生成をマスターできるよう、じっくりと掘り下げていきましょう。

1. はじめに:なぜ乱数が必要なのか

まず、「乱数」とは一体何でしょうか?簡単に言えば、次にどのような数が出るか予測できない、でたらめな数の並びのことです。しかし、コンピュータは基本的に決定的な機械です。同じ入力に対しては常に同じ処理を行い、同じ出力を返します。このような機械が、どのようにして「でたらめ」な数を作り出すのでしょうか?これについては後ほど詳しく説明しますが、まずはなぜプログラミングにおいて乱数が必要になるのか、その用途を見ていきましょう。

プログラミングにおける乱数の役割

  • ゲーム:
    • 敵キャラクターのランダムな動きや出現位置
    • アイテムや宝箱の中身の抽選
    • カードゲームのデッキのシャッフル
    • ダメージ量のばらつき
    • 自動生成されるマップやダンジョン
  • シミュレーション:
    • 物理現象のランダムな揺らぎの再現(ブラウン運動など)
    • 待ち行列理論における顧客の到着間隔やサービスの処理時間
    • モンテカルロ法などの確率的シミュレーション
    • 生態系モデルにおける個体の行動や繁殖
  • 統計学とデータ分析:
    • ランダムなサンプルデータの抽出
    • 統計的検定における乱択アルゴリズム
  • セキュリティと暗号:
    • 暗号鍵や初期化ベクトルの生成
    • パスワードやセッションIDの生成
  • その他:
    • ランダムなテストデータの生成
    • グラフィックスにおけるノイズやテクスチャ生成
    • 音楽やアートの生成

このように、乱数はコンピュータプログラムに「非決定性」や「多様性」をもたらすために不可欠な要素です。C言語を学ぶ上で、乱数の生成方法を理解することは、よりリッチで実用的なプログラムを作るための重要なステップとなります。

C言語標準ライブラリによる乱数生成

C言語には、標準ライブラリの一部として乱数を生成するための関数が用意されています。主に利用するのは、<stdlib.h> ヘッダーに含まれる rand() 関数と srand() 関数、そして乱数ジェネレータの「種(シード)」を設定するために利用されることが多い <time.h> ヘッダーの time() 関数です。

この記事では、これらの標準関数を使って、どのように乱数を生成し、制御するのかを具体的に学んでいきます。

この記事で学ぶこと

  • C言語で乱数を生成するための基本的な関数(rand, srand)の使い方。
  • 乱数ジェネレータの「シード(種)」とは何か、なぜ重要なのか。
  • 時間をシードとして利用する方法とその理由。
  • 生成される乱数の範囲を調整する方法(特定の整数範囲や浮動小数点数)。
  • C言語の標準関数で生成される乱数が「擬似乱数」であることの意味。
  • 乱数生成に関するよくある疑問とその回答。

さあ、C言語での乱数生成の世界へ踏み出しましょう。

2. C言語における乱数生成の基本要素

C言語で乱数を扱う際に中心となるのは、以下の二つの標準ライブラリ関数です。

  • int rand(void);
  • void srand(unsigned int seed);

これらの関数を利用するためには、通常 <stdlib.h> ヘッダーをインクルードする必要があります。また、シードに時間を設定するためには <time.h> ヘッダーと time() 関数も利用します。

“`c

include // rand() と srand() のために必要

include // time() のために必要

“`

まずは、それぞれの関数の役割を見ていきましょう。

rand() 関数:乱数の生成

rand() 関数は、呼び出されるたびに一つの擬似乱数を返します。この関数は引数を取りません。

c
int random_number = rand();

rand() が返す値は整数型 (int) です。その範囲は処理系によって定義されている 0 から RAND_MAX までの間の値です。RAND_MAX<stdlib.h> でマクロとして定義されており、その最小値は 32767 です。つまり、rand() は少なくとも 0 から 32767 までの範囲のいずれかの値を返します。多くの現代的なシステムでは、RAND_MAXint 型の最大値に近い大きな値になっています。

srand() 関数:乱数ジェネレータのシード設定

srand() 関数は、乱数ジェネレータの「シード(seed、種)」を設定するために使用します。シードとは、乱数数列の出発点となる初期値のことです。

c
srand(unsigned int seed);

srand()unsigned int 型の引数を受け取ります。この引数がシードとして使われます。

なぜシード設定が必要なのか

C言語の rand() 関数は、実は「擬似乱数ジェネレータ(Pseudo-Random Number Generator, PRNG)」と呼ばれるものです。これは、完全にランダムな数値を生成するわけではなく、ある初期値(シード)から計算によって求められる数列を生成します。この数列は一見するとランダムに見えますが、シードが決まれば、その後の数列は完全に決定されます。

これは、コンピュータが内部状態に基づいて次の状態を決定するという性質に基づいています。rand() 関数も、内部に持っている「状態」をシードで初期化し、その状態を更新しながら次の乱数を計算して返す、という仕組みで動作しています。

もし srand() を呼び出さずに rand() を使うと、デフォルトのシード値(通常は 1)が使用されます。この場合、プログラムを何度実行しても、rand() は常に同じシード値から開始するため、常に全く同じ乱数数列が生成されます

例えば、以下のようなプログラムを実行してみましょう。

“`c

include

include

int main() {
printf(“Seed なしでの乱数:\n”);
for (int i = 0; i < 5; i++) {
printf(“%d “, rand());
}
printf(“\n”);

printf("もう一度実行 (Seed なし):\n");
for (int i = 0; i < 5; i++) {
    printf("%d ", rand());
}
printf("\n");

return 0;

}
“`

このプログラムをコンパイルして実行すると、おそらく以下のような出力が得られるでしょう(具体的な数値は環境によりますが、同じ環境なら何度実行しても同じ数値が出ます)。

Seed なしでの乱数:
1804289383 846930886 1681692777 1714636915 1957747793
もう一度実行 (Seed なし):
1804289383 846930886 1681692777 1714636915 1957747793

見てわかるように、2回目の実行でも全く同じ乱数数列が生成されています。これでは、例えばゲームで敵の出現パターンをランダムにしたつもりでも、プレイするたびに同じパターンになってしまい、「ランダム」とは言えません。

プログラムを実行するたびに異なる乱数数列を得るためには、実行するたびに異なるシード値を設定する必要があります。この目的で最も一般的に利用されるのが、「現在の時刻」をシードとして使用する方法です。

3. シード設定の実際:srand() と時間

プログラムを実行するたびに異なるシード値を設定するために、多くのC言語プログラムでは現在の時刻をシードとして使用します。なぜなら、プログラムを実行するタイミングは通常毎回異なるため、時刻を利用すれば異なるシード値が得られる可能性が高いからです。

時間をシードとして使う理由

前述の通り、同じシードからは同じ乱数数列が生成されます。異なる乱数数列を得るには、異なるシードが必要です。プログラムを実行するたびに手動でシード値を指定するのは現実的ではありません。

そこで、実行するたびに値が変わる「現在の時刻」を利用することを考えます。時刻は通常、経過した時間を秒単位などで表すため、プログラムの実行開始時刻が異なれば、得られる時刻の値も異なり、結果として異なるシード値が得られます。

time() 関数:エポック秒の取得

C言語で現在の時刻を取得するためによく使われるのが、<time.h> ヘッダーに含まれる time() 関数です。

c
time_t time(time_t *arg);

time() 関数は、エポック(通常は協定世界時 (UTC) の1970年1月1日 00:00:00)からの経過秒数を返します。この経過秒数は time_t 型で表現されます。

引数 arg には time_t 型のポインタを渡すことができ、そのポインタが指す場所にも同じ経過秒数が格納されます。しかし、通常は戻り値だけを利用したい場合が多く、その場合は引数に NULL を指定します。

c
time_t current_time = time(NULL);

time(NULL) は、現在のエポック秒を返します。この戻り値は、srand() が要求する unsigned int 型とは異なりますが、多くのシステムでは time_t 型は unsigned int 型に安全にキャストできるサイズ(またはそれ以上)を持っており、シードとして利用可能です。

srand(time(NULL)); の使い方と注意点

時間をシードとして使うための典型的なコードは以下のようになります。

“`c

include

include

include

int main() {
// 現在の時刻をシードとして乱数ジェネレータを初期化
srand(time(NULL));

printf("Seed を時間で設定した後の乱数:\n");
for (int i = 0; i < 5; i++) {
    printf("%d ", rand());
}
printf("\n");

return 0;

}
“`

このプログラムを複数回実行すると、おそらく毎回異なる乱数数列が出力されるはずです(ただし、非常に短い間隔で連続して実行した場合を除く)。

重要:srand() は一度だけ呼ぶべき

srand() 関数は、プログラムの実行中に一度だけ呼び出すべきです。通常はプログラムの開始直後、乱数を生成する必要がある前に呼び出します。

なぜなら、srand() は乱数ジェネレータの内部状態を初期化する関数であり、これを何度も呼び出してしまうと、かえって乱数のランダム性を損なう可能性があるからです。特に、ループの中で毎回 srand(time(NULL)); を呼び出すようなコードは避けてください。time(NULL) は1秒単位の精度であることが多く、同じ秒の中に何度も srand を呼ぶと、毎回同じシード値でジェネレータが初期化されてしまい、結果として毎回同じ(または予測可能な)短い数列が生成されてしまいます。

間違った例:

“`c

include

include

include

int main() {
printf(“ループ内で Seed を設定 (悪い例):\n”);
for (int i = 0; i < 5; i++) {
// この位置で srand を呼ぶのは避けるべき
srand(time(NULL));
printf(“%d “, rand());
}
printf(“\n”);

return 0;

}
“`

このコードを実行すると、短い間隔で rand() が呼ばれるため、同じ秒内に複数の rand() が呼ばれる可能性が高く、結果として同じ数字が繰り返されるかもしれません(例えば、実行開始が1秒間ずっと同じなら、毎回同じ数字が出ます)。

正しい使い方は、プログラムの main 関数の最初など、乱数を使う処理が始まる前に srand(time(NULL)); を一度だけ呼び出すことです。

time() の精度と限界

time() 関数は通常、システムの時刻を秒単位で取得します。これは、異なるプログラム実行間で異なるシードを得るには十分ですが、同じ秒の中に複数回の rand() 呼び出しが必要な場合(例えば、非常に高速なシミュレーションやゲームのフレーム内など)には、同じシードが使われる可能性があります。

ほとんどの一般的なアプリケーションでは、秒単位の精度で十分ですが、より高い精度の時刻が必要な場合や、同じ秒内に非常に多くの異なる乱数が必要な場合は、time() 以外の方法(例えば、より高精度なタイマー関数や、異なる乱数アルゴリズム)を検討する必要があります。ただし、入門レベルでは time(NULL) をシードとして一度だけ設定する方法で十分なケースがほとんどです。

NULLを渡す理由

time(time_t *arg) 関数の引数に NULL を渡すのは、戻り値としてエポック秒が必要だが、その値を別の time_t 型変数に格納する必要がない場合に慣習的に行われる方法です。time_t 型のポインタを渡すと、関数はそのポインタが指すメモリ位置にも時刻情報を書き込みます。もし、その情報を利用しないのであれば、有効なポインタを渡す代わりに NULL を渡すことで、単に戻り値として時刻を取得することを示します。

4. 生成される乱数の範囲と調整

rand() 関数は 0 から RAND_MAX までの範囲の整数を生成します。しかし、実際には特定の範囲の乱数、例えば1から6までのサイコロの目や、0.0から1.0までの浮動小数点数などが欲しい場合がほとんどです。ここでは、生成される乱数の範囲を調整する方法を学びます。

rand() の出力範囲 (0 から RAND_MAX)

改めて確認すると、rand()0 <= rand() <= RAND_MAX の範囲の整数を返します。RAND_MAX の値は処理系によって異なり、少なくとも 32767 です。実際のプログラムで RAND_MAX の値を確認するには、以下のように出力してみると良いでしょう。

“`c

include

include

int main() {
printf(“RAND_MAX の値: %d\n”, RAND_MAX);
return 0;
}
“`

出力例(環境による):
RAND_MAX の値: 2147483647
多くのシステムでは、RAND_MAXint 型の最大値と同じかそれに近い値(符号付き32ビット整数の最大値 2^31 – 1 = 2147483647)になっています。

特定の整数範囲の乱数生成

特定の範囲の整数乱数を生成するには、剰余演算子 (%) や除算を利用します。

a) 0 から N-1 の範囲 (rand() % N)

最も一般的な使い方は、rand() の結果を剰余演算子 % と組み合わせて使う方法です。rand() % N は、rand() が返した値を N で割った余りを計算します。数学的に、任意の非負整数を N で割った余りは、常に 0 から N-1 の範囲になります。

c
int N = 10; // 0 から 9 の範囲の乱数が欲しい場合
int random_0_to_N_minus_1 = rand() % N;

例えば、rand() が 50 を返し、N が 10 なら、50 % 100 です。rand() が 53 を返し、N が 10 なら、53 % 103 です。このようにして、結果は常に 0, 1, 2, ..., N-1 のいずれかになります。

b) A から B の範囲 (A + rand() % (B - A + 1))

0 から N-1 の範囲の乱数生成を応用して、任意の範囲 A から B までの整数乱数(A ≤ 乱数 ≤ B)を生成することもできます。

まず、範囲の幅を考えます。A から B までの整数の個数は B - A + 1 個です。例えば、1から6までの範囲なら、整数の個数は 6 - 1 + 1 = 6 個です(1, 2, 3, 4, 5, 6)。

0 から B - A までの範囲の乱数を生成するには、(B - A + 1)N として、rand() % (B - A + 1) とします。
得られる値は 0, 1, 2, ..., B - A のいずれかになります。

次に、この結果に開始値 A を加算します。
A + (rand() % (B - A + 1))
これにより、得られる値の範囲は以下のようになります。
最小値: A + 0 = A
最大値: A + (B - A) = B
したがって、この式は A から B までの整数乱数を生成します。

例:1から6までのサイコロの目を生成する場合 (A=1, B=6)
範囲の幅は 6 - 1 + 1 = 6 です。
式は 1 + rand() % 6 となります。

c
int min = 1;
int max = 6;
int dice_roll = min + rand() % (max - min + 1); // 1 から 6 の乱数

この式は非常に汎用性が高く、様々な範囲の整数乱数生成に利用できます。

c) 剰余演算子による偏り(簡単な説明)

rand() % N の方法は、RAND_MAXN の倍数に近い大きな値である場合は問題になりにくいですが、RAND_MAX + 1N で割り切れない場合、厳密には生成される数にわずかな偏りが発生します

例として、RAND_MAX = 32767 で、1から6の乱数(N=6)を生成する場合を考えます。
0 から 32767 までの数は 32768 個あります。
32768 / 6 = 5461 余り 2
これは、0 % 6 から 5 % 6 までの値は、32768個の元の乱数の中で5462回ずつ出現する可能性があるのに対し、0 % 61 % 6 はそれに加えてさらに1回ずつ出現する可能性があることを意味します。
つまり、rand() の結果が 0 から 32767 の範囲で完全に一様分布していると仮定した場合でも、% 6 の結果は完全に一様にはなりません。特に、NRAND_MAX + 1 に比べて大きい場合、この偏りは無視できないことがあります。

ただし、ほとんどの一般的なアプリケーションにおいて、RAND_MAX が非常に大きい値であるため、この偏りは非常に小さく、無視できるレベルであることが多いです。入門レベルでは、A + rand() % (B - A + 1) の方法で十分でしょう。より高品質で一様性の高い乱数が必要な場合は、C++11の <random> ライブラリのような、より洗練された乱数生成機構を検討することになります(C言語標準には含まれません)。

浮動小数点数(実数)の乱数生成

rand() は整数を生成しますが、浮動小数点数(floatdouble)の乱数が必要な場合もあります。例えば、0.0から1.0までの範囲の実数乱数などが考えられます。

rand()0 から RAND_MAX までの値を返すことを利用して、これを浮動小数点数の範囲にマッピングします。rand() の結果を RAND_MAX で割ると、0 から 1 の間の浮動小数点数が得られます。

c
// 0.0 から 1.0 の範囲の乱数を生成
double random_0_to_1 = (double)rand() / RAND_MAX;

ここで、(double) へのキャストが重要です。もしキャストしないと、整数同士の除算が行われ、結果が常に 0 になってしまう可能性があります。(double)rand() とすることで、rand() の戻り値が double 型に変換され、浮動小数点数での除算が行われます。

正確には、(double)rand() / (double)RAND_MAX とするのがより安全で明確ですが、分子か分母のいずれかが浮動小数点型であれば、除算結果は浮動小数点型になります。

この方法で得られる乱数は、厳密には 0.0 から 1.0 の範囲ですが、RAND_MAX が整数の最大値であるため、1.0 になるのは rand() がちょうど RAND_MAX を返した場合のみです。したがって、実際には 0.0 <= random_0_to_1 <= 1.0 の範囲の値を生成します。

特定の浮動小数点数範囲の乱数生成

0.0 から 1.0 の範囲の乱数が生成できれば、それをスケーリングして任意の範囲 A から B までの浮動小数点数乱数(A ≤ 乱数 ≤ B または A ≤ 乱数 < B)を生成できます。

例えば、0.0 から 100.0 までの乱数が欲しい場合、0.0 から 1.0 の乱数に 100 を掛けます。
double random_0_to_100 = ((double)rand() / RAND_MAX) * 100.0;

任意の範囲 A から B までの乱数(A ≤ 乱数 ≤ B または A ≤ 乱数 < B)を生成するには、まず 0.0 から 1.0 の乱数を生成し、それを範囲の幅 (B - A) でスケーリングし、開始値 A を加えます。

(B - A) * ((double)rand() / RAND_MAX) + A

例:10.0から20.0までの範囲の乱数を生成する場合 (A=10.0, B=20.0)
範囲の幅は 20.0 - 10.0 = 10.0 です。
式は 10.0 * ((double)rand() / RAND_MAX) + 10.0 となります。

c
double min_f = 10.0;
double max_f = 20.0;
double random_f = (max_f - min_f) * ((double)rand() / RAND_MAX) + min_f; // 10.0 から 20.0 の乱数

この方法で生成される乱数は、厳密には min_f <= random_f <= max_f の範囲となりますが、RAND_MAX が十分大きい場合、max_f になる確率は非常に低いです。もし min_f から max_f の間の「左閉右開区間」つまり [min_f, max_f) の範囲(max_f を含まない)が必要な場合は、(double)rand() / (RAND_MAX + 1.0) のように分母に 1.0 を加えることで対応できる場合がありますが、これは RAND_MAX の正確な値と乱数ジェネレータの実装に依存するため、注意が必要です。一般的な入門レベルでは、rand() / (double)RAND_MAX で得られる [0.0, 1.0] の範囲をスケーリングする方法で十分です。

5. 具体的なコード例で理解を深める

これまでに学んだことを踏まえて、いくつかの具体的なコード例を見ていきましょう。実際にコードを書いて実行してみることで、理解が深まります。

例1:シードなしで乱数を生成する

シードを設定しない場合に、毎回同じ乱数数列が生成されることを確認するコードです。

“`c

include

include // rand(), srand() のために必要

int main() {
// srand() を呼ばない場合、デフォルトのシード (通常 1) が使われる

printf("--- シードなし (初回実行) ---\n");
for (int i = 0; i < 5; i++) {
    // rand() を呼び出して乱数を取得
    int r = rand();
    printf("%d ", r);
}
printf("\n\n");

// プログラムを終了し、もう一度同じプログラムを実行すると...

// 再度実行されたと仮定して、再び srand() なしで rand() を呼び出す
// (実際には、このコードブロックは上記のブロックと同時に実行されるのではなく、
// プログラムを終了・再起動した後に実行されることを想定しています)

// 例示のため、ここでは便宜的にコメントアウトして、
// 同じプログラムを2回実行した場合のシミュレーションとして見てください。
/*
printf("--- シードなし (2回目実行) ---\n");
for (int i = 0; i < 5; i++) {
    int r = rand();
    printf("%d ", r);
}
printf("\n");
*/

return 0;

}
“`

このコードをコンパイル・実行し、その後プログラムを終了させてからもう一度全く同じプログラムを実行してみてください。出力される最初の5つの乱数が、どちらの実行でも同じになるはずです。

例2:時間をシードとして整数乱数を生成する

プログラムを実行するたびに異なる乱数を得るために、現在の時刻をシードとして設定します。

“`c

include

include // rand(), srand() のために必要

include // time() のために必要

int main() {
// 現在の時刻 (エポック秒) を取得し、unsigned int にキャストしてシードとして設定
// srand() はプログラムの開始直後に一度だけ呼び出す
srand((unsigned int)time(NULL));

printf("--- シードを時間で設定 ---\n");
printf("乱数1: %d\n", rand());
printf("乱数2: %d\n", rand());
printf("乱数3: %d\n", rand());
printf("乱数4: %d\n", rand());
printf("乱数5: %d\n", rand());

return 0;

}
“`

このコードをコンパイルして実行し、その後プログラムを終了させてからもう一度全く同じプログラムを実行してみてください。今度は、出力される5つの乱数が、ほとんどの場合で異なる値になるはずです。ただし、非常に短時間(1秒以内)で何度も実行すると、time(NULL) が同じ値を返し、同じシードになる可能性があることに注意してください。

例3:サイコロの目をシミュレーションする(1から6の範囲)

1から6までの整数乱数を生成する方法です。これは「AからBまでの範囲の乱数」の典型例です。

“`c

include

include // rand(), srand(), RAND_MAX のために必要

include // time() のために必要

int main() {
// 乱数ジェネレータを初期化 (一度だけ)
srand((unsigned int)time(NULL));

printf("--- サイコロ投げシミュレーション ---\n");

// 10回サイコロを振る
for (int i = 0; i < 10; i++) {
    // 1 から 6 までの乱数を生成する計算式:
    // rand() % 6  -> 0 から 5 の範囲
    // 1 + (rand() % 6) -> 1 から 6 の範囲
    int dice_roll = 1 + rand() % 6;
    printf("%d回目のサイコロの目: %d\n", i + 1, dice_roll);
}

return 0;

}
“`

このコードを実行すると、1から6までの数字がランダムに出力されるはずです。ゲームでサイコロの目を使う場合などに直接利用できるでしょう。

例4:コイン投げをシミュレーションする(表か裏)

確率50%で「表」か「裏」かを決定するシミュレーションです。これは「0か1の範囲の乱数」として考えることができます。

“`c

include

include // rand(), srand() のために必要

include // time() のために必要

int main() {
// 乱数ジェネレータを初期化 (一度だけ)
srand((unsigned int)time(NULL));

printf("--- コイン投げシミュレーション ---\n");

// 10回コインを投げる
for (int i = 0; i < 10; i++) {
    // 0 か 1 の乱数を生成
    // rand() % 2  -> 0 または 1
    int coin_side = rand() % 2;

    if (coin_side == 0) {
        printf("%d回目の結果: 裏\n", i + 1);
    } else {
        printf("%d回目の結果: 表\n", i + 1);
    }
}

return 0;

}
“`

このコードでは、rand() % 2 が 0 または 1 を返します。これを「裏」と「表」に対応させています。応用として、確率を50%以外にすることも可能です(例:rand() % 100 < 30 で30%の確率を表現するなど)。

例5:指定した文字数のランダムな文字列を生成する(応用例)

少し応用的な例として、英小文字からなるランダムな文字列を生成してみましょう。

“`c

include

include // rand(), srand() のために必要

include // time() のために必要

int main() {
// 乱数ジェネレータを初期化 (一度だけ)
srand((unsigned int)time(NULL));

int length = 8; // 生成したい文字列の長さ
char random_string[length + 1]; // 文字列終端ヌル文字のために+1

printf("--- ランダムな文字列生成 ---\n");

for (int i = 0; i < length; i++) {
    // 'a' から 'z' までの範囲の乱数を生成
    // 'a' の ASCII コードは 97
    // 'z' の ASCII コードは 122
    // 範囲の文字数は 'z' - 'a' + 1 = 122 - 97 + 1 = 26
    // rand() % 26  -> 0 から 25 の範囲
    // 'a' + (rand() % 26) -> 'a' から 'a'+25 ('z') の範囲
    random_string[i] = 'a' + (rand() % 26);
}
random_string[length] = '\0'; // 文字列の終端にヌル文字を追加

printf("生成された文字列: %s\n", random_string);

return 0;

}
“`

この例では、文字のASCIIコードを利用しています。'a' という文字は、C言語ではそのASCIIコードを表す整数値として扱われます。したがって、'a' + 整数'a' のASCIIコードにその整数を加算した新しい文字のASCIIコードとなり、結果として文字が生成されます。ここでは、'a' から 'z' までの26文字を対象として、'a' + (rand() % 26) という計算でランダムな英小文字を生成しています。

例6:特定の範囲の浮動小数点数乱数を生成する

0.0から1.0、または10.0から20.0のような特定の範囲の浮動小数点数乱数を生成します。

“`c

include

include // rand(), srand(), RAND_MAX のために必要

include // time() のために必要

int main() {
// 乱数ジェネレータを初期化 (一度だけ)
srand((unsigned int)time(NULL));

printf("--- 浮動小数点数乱数生成 ---\n");

// 0.0 から 1.0 の範囲の乱数を10個生成
printf("0.0 から 1.0 の範囲:\n");
for (int i = 0; i < 10; i++) {
    double random_val = (double)rand() / RAND_MAX;
    printf("%.6f ", random_val); // 小数点以下6桁で表示
}
printf("\n\n");

// 10.0 から 20.0 の範囲の乱数を10個生成
printf("10.0 から 20.0 の範囲:\n");
double min_f = 10.0;
double max_f = 20.0;
for (int i = 0; i < 10; i++) {
    double random_val_ranged = (max_f - min_f) * ((double)rand() / RAND_MAX) + min_f;
    printf("%.6f ", random_val_ranged); // 小数点以下6桁で表示
}
printf("\n");

return 0;

}
“`

この例では、まず (double)rand() / RAND_MAX で0.0から1.0の範囲の乱数を生成し、次にそれをスケーリングして10.0から20.0の範囲の乱数を生成しています。小数点以下の桁数を制御するために %.6f のフォーマット指定子を使っています。

6. 擬似乱数について知っておくべきこと

これまでに rand() 関数が「擬似乱数」を生成することを何度か述べました。ここでは、擬似乱数とは具体的にどのようなもので、C言語の標準ライブラリで生成される乱数の質について簡単に触れておきます。

擬似乱数とは?真の乱数との違い

真の乱数(True Random Number, TRN)は、予測不可能な物理現象(例:放射性崩壊、熱雑音、大気ノイズなど)を利用して生成される乱数です。これらの現象は本質的に非決定的であると考えられており、その結果は事前の情報からは予測できません。

一方、擬似乱数(Pseudo-Random Number, PRN)は、決定的なアルゴリズムによって生成される数列です。ある初期値(シード)から出発して、数学的な計算を繰り返すことで次の数を生成します。この数列は、十分に長ければ統計的にランダムに見える性質(例:分布の一様性、相関のなさなど)を持つように設計されています。しかし、一度シードが決まると、その後の数列は完全に決まってしまうため、「予測可能」であると言えます。

rand() が生成する乱数の質

C言語標準の rand() 関数が使用する擬似乱数アルゴリズムは、標準によって具体的なアルゴリズムが定められているわけではありません。これは処理系(コンパイラや実行環境)に依存します。歴史的に、あまり高品質ではない線形合同法(Linear Congruential Generator, LCG)が使われることが多かったですが、より新しい処理系では改善されたアルゴリズムが採用されている場合もあります。

一般的に、C標準の rand() で生成される擬似乱数は、以下の点で限界を持つことがあります。

  • 周期が短い: 擬似乱数数列は必ずどこかで繰り返し(周期)を持ちます。LCGなどの古いアルゴリズムでは周期が比較的短く、同じ数列パターンが繰り返されてしまうことがあります。
  • 低位ビットのランダム性が低い: 下位のビット列(特に最下位ビット)の並びに偏りが見られることがあります。
  • 多次元的な分布に偏り: 複数の乱数を組み合わせて座標を作る場合など、高次元で見たときに均等に分布せず、パターンが見られることがあります。

これらの限界は、要求されるランダム性のレベルによっては問題となる可能性があります。

どのような場合にrand()で十分か

C標準の rand() で生成される擬似乱数は、以下のような一般的な用途には十分なランダム性を提供することがほとんどです。

  • 簡単なゲームの要素(敵の出現、アイテム抽選など)
  • シミュレーションの基本的なランダム要素
  • 教育目的のプログラム
  • テストデータの生成

これらの用途では、厳密な統計的ランダム性や予測不可能性は通常必要とされません。プログラムの実行ごとに異なる結果が得られれば十分であり、その目的は srand(time(NULL))rand() の組み合わせで達成できます。

より高品質な乱数が必要な場合(暗号など)

もし、暗号鍵の生成、セキュリティトークンの発行、あるいは厳密な科学シミュレーションなど、高品質な乱数暗号学的に安全な乱数(Cryptographically Secure Pseudo-Random Number Generator, CSPRNG)が必要な場合は、C標準の rand() は使用すべきではありません。

暗号学的に安全な乱数ジェネレータは、その出力から過去または将来の出力を予測することが計算上困難であるように設計されています。C標準ライブラリにはそのような機能は含まれていません。

より高品質な乱数が必要な場合は、以下の選択肢を検討する必要があります。

  • OS提供の乱数源: 多くのオペレーティングシステムは、高品質または暗号学的に安全な乱数源を提供しています(例:Linux/Unix の /dev/urandom/dev/random、Windows の CryptGenRandom)。これらはOSのAPIを通じて利用できますが、OS依存となります。
  • 外部ライブラリ: GnuPG の乱数ライブラリ(libgcrypt)、OpenSSL の乱数関数(RAND_bytes など)など、強力な乱数アルゴリズム(メルセンヌ・ツイスターなど)を実装した外部ライブラリを利用する。
  • C++11 <random>: C++11以降では、様々な高品質な乱数生成エンジンと分布を扱う <random> ヘッダーが標準化されています。C言語とは異なりますが、C++を使う場合はこちらが推奨されます。

この記事はC言語入門者向けであるため、これらの発展的な内容は詳細に扱いませんが、rand() には限界があることを知っておくことは重要です。日常的なプログラミングでは rand() で十分ですが、セキュリティや厳密な科学計算においては代替手段が必要です。

7. よくある質問(FAQ)

C言語の乱数生成について、入門者がよく疑問に思う点についてまとめます。

Q1: srand() はどこで呼び出すべき?

A1: srand() 関数は、プログラムの実行が開始されてから、乱数を生成する処理を行う前に、一度だけ呼び出すべきです。通常は main 関数の最初の方で呼び出します。これにより、プログラムが実行されるたびに異なるシード値で乱数ジェネレータが初期化され、異なる乱数数列が得られるようになります。

Q2: ループ内でsrand()を毎回呼び出すとどうなる?

A2: ループ内で srand(time(NULL)); のように毎回シードを設定するのは、避けるべき悪い習慣です。time(NULL) は通常秒単位の精度しかありません。したがって、短いループの中で何度も srand(time(NULL)); を呼び出すと、同じ秒数内に何度も呼び出しが発生し、毎回同じシード値で乱数ジェネレータが初期化されてしまう可能性が高くなります。結果として、生成される乱数が非常に短い周期で繰り返されたり、予測可能なパターンになったりして、乱数性が大きく損なわれます。srand() はプログラム全体で一度だけにしてください。

Q3: RAND_MAX はいつも同じ値?

A3: RAND_MAX<stdlib.h> で定義されているマクロで、rand() が返すことができる最大値です。この値は処理系に依存します。C標準は RAND_MAX の最小値として 32767 を定めていますが、多くの現代的なシステムでは int 型の最大値に近い、より大きな値(例えば 2147483647)が使われています。そのため、rand() の結果を特定の範囲に調整する計算を行う際は、RAND_MAX が実際にその環境で定義されている値であることを前提にコードを書く必要があります(例:rand() / (double)RAND_MAX の計算)。

Q4: time(NULL) のNULLは何を意味する?

A4: time(time_t *arg) 関数の引数は、取得した時刻を格納する time_t 型の変数を指すポインタを指定するために使われます。しかし、戻り値として時刻を取得できれば十分で、別の変数に格納する必要がない場合、引数に NULL を指定します。これは、「引数としてポインタを必要とするが、今回はそのポインタが指す先の情報は利用しない」という意図を示す慣習的な方法です。

Q5: より良い乱数生成方法はある?

A5: はい、あります。C標準の rand() は多くの一般的な用途には十分ですが、統計的な性質や予測不可能性の点で限界があります。より高品質な乱数が必要な場合(特にセキュリティ関連や厳密なシミュレーション)は、C標準の rand() ではなく、より高度なアルゴリズム(メルセンヌ・ツイスターなど)を実装した外部ライブラリを利用するか、オペレーティングシステムが提供する暗号学的に安全な乱数源を利用することを検討すべきです。C++を使う場合は、C++11以降の <random> ライブラリが推奨されます。

8. まとめ:C言語乱数生成のポイント

この記事では、C言語における乱数生成の基本的な方法について、詳細に解説しました。重要なポイントを振り返りましょう。

  • C言語で乱数を生成するには、主に <stdlib.h>rand() 関数と srand() 関数を使用します。
  • rand() は擬似乱数を一つ生成します。その範囲は 0 から RAND_MAX までです。
  • srand() は乱数ジェネレータの「シード(種)」を設定します。同じシードからは常に同じ乱数数列が生成されます。
  • プログラムを実行するたびに異なる乱数を得るためには、srand() を使って実行ごとに異なるシード値を設定する必要があります。
  • 現在の時刻をシードとして使用するのが一般的です。これは <time.h>time(NULL) 関数で取得できます。
  • srand((unsigned int)time(NULL)); のように、プログラムの開始直後に一度だけ srand() を呼び出すのが正しい使い方です。ループ内で何度も呼ぶのは避けてください。
  • 特定の範囲(例: A から B)の整数乱数を生成するには、A + rand() % (B - A + 1) のような式を利用します。
  • 浮動小数点数乱数(例: 0.0 から 1.0)を生成するには、(double)rand() / RAND_MAX のような式を利用します。
  • C標準の rand() は擬似乱数であり、統計的な性質や予測不可能性に限界があります。一般的な用途には十分ですが、セキュリティ関連や厳密な科学計算には適していません。

C言語での乱数生成は、これらの基本的な関数と概念を理解すれば、それほど難しくありません。この記事で学んだことを活かして、皆さんのプログラムにランダムな要素を取り入れてみてください。

入門者が次に進むべきステップ

  • 実際にこの記事のコード例を自分で入力し、コンパイルして実行してみる。
  • 様々な範囲の整数や浮動小数点数の乱数を生成するプログラムを書いてみる。
  • サイコロゲームや簡単な抽選プログラムなど、乱数を使った小さなアプリケーションを作ってみる。
  • 余裕があれば、RAND_MAX の値が自分の環境でいくつになるかを確認してみる。

C言語の乱数生成は奥が深い分野でもありますが、まずは基本をしっかりと押さえることが重要です。この記事が、皆さんのC言語プログラミング学習の一助となれば幸いです。


9. 付録:用語集

  • 乱数 (Random Number): 次にどのような数が出るか予測できない、でたらめな数の並び。
  • 擬似乱数 (Pseudo-Random Number, PRN): 決定的なアルゴリズムによって生成されるが、統計的にランダムに見える数列。初期値(シード)が決まると数列も決定される。
  • 真の乱数 (True Random Number, TRN): 物理的な非決定現象を利用して生成される、予測不可能な乱数。
  • 乱数ジェネレータ (Random Number Generator, RNG): 乱数を生成するための装置やアルゴリズム。C言語の rand() 関数などがこれにあたる。
  • シード (Seed, 種): 擬似乱数ジェネレータの初期値。シードが異なれば生成される数列も異なる。
  • エポック (Epoch): コンピュータシステムが時間を計測する際の基準となる特定の時点。多くのUnix系システムでは1970年1月1日 00:00:00 UTC。
  • エポック秒 (Epoch Seconds): エポックからの経過秒数。time() 関数で取得されることが多い。
  • RAND_MAX: <stdlib.h> で定義されているマクロ。rand() 関数が返す乱数の最大値を示す。処理系依存。
  • 線形合同法 (Linear Congruential Generator, LCG): 歴史的に多くの rand() 実装で使われてきた単純な擬似乱数アルゴリズム。周期が比較的短く、統計的な偏りを持つ場合がある。
  • 暗号学的に安全な乱数ジェネレータ (CSPRNG): その出力から過去または将来の出力を予測することが計算上困難なように設計された擬似乱数ジェネレータ。セキュリティ用途に適している。

これで、C言語での乱数生成に関する入門者向けの詳細な解説記事が完成しました。約5000語というボリュームで、必要な情報と多くのコード例、注意点を含めるように努めました。ユーザーの要望通り、記事の全文を直接ここに表示します。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール