C言語のきほん|疑似乱数を生成する(srand, rand)

毎回同じではつまらない。srand と rand を使って、C言語でランダムな動きを作ってみよう

ゲームやシミュレーションのようなプログラムを作るとき、「毎回少し違う結果が出る仕組み」がほしくなることがあります。
たとえば、サイコロの目、じゃんけんの手、ランダムなパスワード、くじ引きの番号などです。こうした場面で必要になるのが乱数です。

ただし、コンピュータが作る乱数は、完全に気まぐれな「本物の偶然」ではありません。
一定の計算ルールに従って作られるため、正確には疑似乱数と呼ばれます。

C言語では、この疑似乱数を扱うために、主に次の2つの関数を使います。

  • rand
    疑似乱数を生成する関数
  • srand
    rand が使う「種」を設定する関数

この2つはセットで覚えることがとても大切です。
rand だけでも乱数らしい値は得られますが、srand を使わないと、プログラムを実行するたびに同じ並びの値が出やすくなります。
反対に、srand で現在時刻などを使って種を変えると、実行のたびに違う並びの乱数が得られるようになります。

この記事では、

  • rand が返す値の範囲
  • % 演算子を使った範囲の調整
  • 0.0〜1.0 の乱数の作り方
  • srand の役割
  • time 関数と組み合わせる理由
  • srand を呼ぶ場所の注意点

といったポイントを、やさしく順番に見ていきます。

乱数は、ゲームだけでなく、抽選、テストデータ作成、簡単なシミュレーションなど、いろいろな場面で役立ちます。
ここで基本をしっかり押さえておくと、プログラムの表現力がぐっと広がります。

疑似乱数とは

疑似乱数とは、コンピュータが計算によって作り出す「乱数らしい値」のことです。
見た目にはランダムに見えても、内部では決まった規則に従って生成されています。

そのため、同じ条件から始めれば、同じ乱数の並びを再現できます。
これは一見不思議に感じるかもしれませんが、実はとても便利です。

たとえば、

  • 毎回違う乱数を使いたいときは、毎回違う種を使う
  • 同じ乱数列を再現したいときは、同じ種を使う

という使い分けができます。

この「種」にあたるのが、srand に渡すシード値です。

rand 関数とは

rand は、疑似乱数を1つ生成する関数です。

関数宣言

#include <stdlib.h>

int rand(void);

機能

0 以上 RAND_MAX 以下の範囲の疑似乱数を返します。

返却値

疑似乱数整数を返します。

使用例

int r = rand();

この場合、r には 0〜RAND_MAX の範囲の整数値が入ります。

RAND_MAX とは

rand が返す値の最大値は、RAND_MAX というマクロで表されます。
この値は処理系によって異なりますが、少なくとも 32767 以上であることが規格で決められています。

つまり、rand が返す値の範囲は次のようになります。

最小値最大値
0RAND_MAX

たとえば、ある環境で RAND_MAX が 32767 なら、rand は 0〜32767 の範囲の整数を返します。

rand の値の範囲を調整する

rand はそのままだと 0〜RAND_MAX の範囲を返します。
でも実際には、

  • 0〜2
  • 1〜6
  • 10〜20

のように、もっと狭い範囲で使いたいことがよくあります。

そんなときによく使うのが % 演算子です。

0〜2 の乱数

int r = rand() % 3;

rand() % 3 は、3 で割った余りなので 0〜2 になります。

1〜3 の乱数

int r = rand() % 3 + 1;

rand() % 3 は 0〜2 なので、そこに 1 を足すと 1〜3 になります。

10〜20 の乱数

int r = rand() % 11 + 10;

rand() % 11 は 0〜10 なので、そこに 10 を足すと 10〜20 になります。

乱数範囲の考え方

範囲を調整するときの基本の考え方は、次の通りです。

最小値 〜 最大値
→ 個数 = 最大値 - 最小値 + 1
→ rand() % 個数 + 最小値

たとえば、10〜20 なら、

  • 個数 = 20 - 10 + 1 = 11
  • rand() % 11 + 10

になります。

この考え方を表にすると、次のようになります。

作りたい範囲書き方
0〜2rand() % 3
1〜6rand() % 6 + 1
10〜20rand() % 11 + 10
5〜9rand() % 5 + 5

0.0〜1.0 の乱数を作る

整数ではなく、小数の乱数がほしいこともあります。
そのときは、rand() を RAND_MAX で割る方法がよく使われます。

double r = (double)rand() / RAND_MAX;

これで、0.0〜1.0 以下の範囲の乱数らしい値が得られます。

ここで大切なのは、キャストが必要という点です。

(double)rand()

としておかないと、整数同士の割り算になってしまい、期待どおりの小数になりません。

rand の動きを図で理解する

この図では、rand() が返す大きな範囲の整数値から、% 演算子や割り算を使って必要な範囲の値を作る流れを表しています。
rand() はそのままだと 0〜RAND_MAX の値ですが、工夫することで小さな範囲の乱数や小数の乱数として利用できます。

rand だけでは毎回同じ並びになることがある

ここがとても大切なポイントです。

rand をそのまま使ってプログラムを何度も実行すると、毎回同じような値が表示されることがあります。
これは、乱数の「種」が固定されているためです。

多くの環境では、srand を呼ばずに rand を使うと、実質的に同じシード値から始まるため、毎回同じ乱数列が生成されます。

つまり、rand は「完全に自由な乱数」を返しているのではなく、シード値に応じた決まった並びを返しているのです。

srand 関数とは

srand は、rand が返す疑似乱数列のシード値を設定する関数です。

関数宣言

#include <stdlib.h>

void srand(unsigned int seed);

機能

rand 関数が返す疑似乱数の種を設定します。

返却値

なし

使用例

srand(10);

このように書くと、シード値 10 に対応した乱数列が生成されます。
同じシード値を使えば、同じ乱数列が再現されます。

srand の役割

srand の役割を表で整理すると、次のようになります。

使い方結果
srand を使わない毎回同じ並びになりやすい
同じ seed を使う同じ乱数列を再現できる
毎回違う seed を使う毎回違う乱数列になりやすい

この性質はとても便利です。
たとえば、デバッグ中に同じ乱数列を再現したいときは、固定の seed を使えばよいです。
一方、ゲームのように毎回違う結果がほしいときは、毎回変わる値を seed に使います。

現在時刻をシードにする理由

実行のたびに違う乱数列を得たいときは、現在時刻を使って srand を呼ぶのが一般的です。

#include <time.h>

srand((unsigned)time(NULL));

time(NULL) は現在時刻を返します。
現在時刻は毎秒変わるので、その値を seed に使えば、実行ごとに違う乱数列になりやすくなります。

なぜキャストしているのか

time(NULL) の返却値は time_t 型です。
一方、srand の引数は unsigned int 型です。

そのため、型を合わせるために

(unsigned)time(NULL)

のようにキャストしています。

srand と rand の関係を図で理解する

この図では、srand がシード値を受け取り、そのシードに応じて rand の値の並びが決まることを表しています。
固定値を使えば同じ並びを再現でき、現在時刻を使えば実行ごとに違う並びになりやすいことがわかります。

シンプルなサンプルプログラム

1〜6 の乱数を5回表示するプログラムです。

ファイル名:12_15_1.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    /* 現在時刻をシードに設定する */
    srand((unsigned)time(NULL));

    printf("サイコロを5回ふった結果を表示します。\n");

    for (int i = 0; i < 5; i++) {
        int dice = rand() % 6 + 1;
        printf("%d回目: %d\n", i + 1, dice);
    }

    return 0;
}

このプログラムのポイント

このプログラムでは、まず最初に

srand((unsigned)time(NULL));

を1回だけ呼んでいます。
これがとても重要です。

そのあと、for 文の中で

rand() % 6 + 1

を使って 1〜6 の乱数を作っています。
これはサイコロの目を表すのにぴったりです。

実行イメージ

実行結果は環境によって変わりますが、たとえば次のようになります。

サイコロを5回ふった結果を表示します。
1回目: 4
2回目: 1
3回目: 6
4回目: 2
5回目: 5

もう一度実行すると、別の並びになるはずです。

srand をループの中で呼んではいけない理由

これはとてもよくある注意点です。

次のように、ループの中で毎回 srand を呼ぶのは好ましくありません。

for (int i = 0; i < 10; i++) {
    srand((unsigned)time(NULL));
    printf("%d\n", rand() % 100 + 1);
}

なぜなら、ループが非常に速く回ると、time(NULL) の値が変わらず、毎回同じ seed になってしまうからです。
その結果、同じ乱数が繰り返されやすくなります。

正しい考え方

  • srand は通常、プログラム開始時に1回だけ
  • rand は必要な回数だけ何度でも呼ぶ

と覚えておくのが基本です。

実践問題

次の文字列配列が与えられています。
この中から、小数点を含まない文字列だけを整数に変換し、その合計を表示するプログラムを作成してください。

char *data[] = { "250", "31.5", "480", "99", "72.8", "610", "15" };

小数点の有無の確認には strchr、整数変換には atoi を使ってください。

解答例

ファイル名:12_15_2.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char *data[] = { "250", "31.5", "480", "99", "72.8", "610", "15" };
    int count = sizeof(data) / sizeof(data[0]);
    int sum = 0;

    for (int i = 0; i < count; i++) {
        if (strchr(data[i], '.') == NULL) {
            sum += atoi(data[i]);
        }
    }

    printf("整数の合計は %d です。\n", sum);

    return 0;
}

解説

この問題では、まず strchr で . があるかどうかを調べています。
小数点がなければ整数として扱えるので、atoi で変換して合計に加えています。
文字列検索と数値変換を組み合わせる練習になります。

実践問題

数字、英大文字、英小文字からなるランダムなユーザーIDを生成するプログラムを作成してください。

条件は次の通りです。

  • 文字数は 6〜10 文字の間でランダム
  • 使用する文字は
    0〜9
    A〜Z
    a〜z
  • 実行ごとに違う結果になりやすいようにする

解答例

ファイル名:12_15_3.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    char table[] =
        "0123456789"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "abcdefghijklmnopqrstuvwxyz";

    int table_size = sizeof(table) - 1;
    int length;
    
    srand((unsigned)time(NULL));

    length = rand() % 5 + 6;

    printf("生成されたユーザーID: ");

    for (int i = 0; i < length; i++) {
        char ch = table[rand() % table_size];
        putchar(ch);
    }

    putchar('\n');

    return 0;
}

解説

この問題では、使用可能な文字を1つの文字列にまとめています。
その中から rand() % table_size を使ってランダムに1文字ずつ選び、表示しています。
パスワード生成の考え方に近いですが、少し短めで取り組みやすい問題です。

実践問題

コンピュータとサイコロ勝負を行うプログラムを作成してください。

仕様は次の通りです。

  • ユーザーは 1〜6 の数字を入力する
  • コンピュータは rand を使って 1〜6 を出す
  • 大きい目を出したほうが勝ち
  • 同じなら引き分け

解答例

ファイル名:12_15_5.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    int user;
    int computer;

    srand((unsigned)time(NULL));

    printf("サイコロの目を入力してください(1〜6)> ");
    scanf("%d", &user);

    computer = rand() % 6 + 1;

    printf("あなたの目: %d\n", user);
    printf("コンピュータの目: %d\n", computer);

    if (user > computer) {
        puts("あなたの勝ちです。");
    } else if (user < computer) {
        puts("コンピュータの勝ちです。");
    } else {
        puts("引き分けです。");
    }

    return 0;
}

解説

この問題では、rand() % 6 + 1 を使って 1〜6 の乱数を作っています。
じゃんけんよりルールが簡単なので、乱数を使った対戦処理の入門として取り組みやすい問題です。

実践問題

要素数 20 の配列に、0.0〜1.0 以下の疑似乱数を格納し、その平均値を求めるプログラムを作成してください。

条件は次の通りです。

  • シード値は 10 に固定する
  • 各要素は
    (double)rand() / RAND_MAX
    で作る
  • 最後に平均値を表示する

解答例

ファイル名:12_15_6.c

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    double values[20];
    double sum = 0.0;
    double average;

    srand(10);

    for (int i = 0; i < 20; i++) {
        values[i] = (double)rand() / RAND_MAX;
        sum += values[i];
    }

    average = sum / 20.0;

    printf("平均値: %.4f\n", average);

    return 0;
}

解説

この問題では、乱数そのものを配列に保存して集計しています。
分散や標準偏差よりもずっとやさしい内容ですが、固定 seed による再現性、0.0〜1.0 の乱数生成、配列への格納、平均の計算といった重要な要素がしっかり含まれています。

確認問題

次の項目について、正しいものには ○、間違っているものには × をつけてください。

① rand 関数は 0 以上 RAND_MAX 以下の整数値を返す。

② srand 関数は rand が返す乱数のシード値を設定する。

③ srand を呼ばずに rand を使うことはできない。

④ rand() % 6 + 1 と書くと、1〜6 の範囲の整数乱数を作れる。

⑤ (double)rand() / RAND_MAX と書くと、0.0〜1.0 以下の疑似乱数を作れる。

⑥ 現在時刻を使って srand を呼ぶと、実行ごとに違う乱数列になりやすい。

⑦ srand は rand を呼ぶたびに毎回実行するのが正しい。

⑧ 固定のシード値を使うと、同じ乱数列を再現できる。

⑨ RAND_MAX の値は、どの処理系でも必ず 32767 である。

⑩ rand の値の範囲を狭める方法として、% 演算子がよく使われる。

解答と解説

① ○
rand 関数は 0 以上 RAND_MAX 以下の範囲の疑似乱数整数を返します。RAND_MAX は処理系依存ですが、少なくとも 32767 以上です。

② ○
srand は rand が使う乱数列の種、つまりシード値を設定する関数です。この seed によって、その後の乱数列が決まります。

③ ×
srand を呼ばなくても rand は使えます。ただし、その場合は固定のシード値から始まることが多く、毎回同じ乱数列になりやすいです。

④ ○
rand() % 6 は 0〜5、そこに 1 を足すので 1〜6 の範囲の整数乱数になります。サイコロの目を表すときによく使われます。

⑤ ○
rand の返り値を double に変換してから RAND_MAX で割ると、0.0〜1.0 以下の範囲の疑似乱数を作れます。整数同士の割り算にならないようにキャストが必要です。

⑥ ○
time(NULL) は現在時刻を返すため、これをシードに使うと実行のたびに違う乱数列になりやすくなります。ランダムらしさがほしいときの定番の方法です。

⑦ ×
srand は通常、プログラムの先頭で1回だけ呼びます。ループの中で毎回呼ぶと、同じ時刻がシードになってしまい、同じ乱数が繰り返されることがあります。

⑧ ○
固定の seed を使えば、その seed に対応する乱数列を再現できます。デバッグや動作確認ではこの性質がとても役立ちます。

⑨ ×
RAND_MAX の値は処理系依存です。規格では 32767 以上であることが定められていますが、必ず 32767 とは限りません。

⑩ ○
rand が返す大きな範囲の整数から、必要な範囲の整数乱数を作るために % 演算子がよく使われます。たとえば rand() % 3 なら 0〜2 になります。

rand と srand を使うときの心構え

rand と srand はとても使いやすい関数ですが、次のポイントを意識するとより確実に使えます。

ポイント内容
stdlib.h が必要rand と srand を使う準備をする
time.h が必要なことがあるtime(NULL) を使うなら必要
srand は通常1回だけループの中では呼ばない
範囲調整を考える% と加算で必要な範囲にする
再現性も大事固定 seed なら同じ並びを再現できる

疑似乱数は、ゲームや抽選だけでなく、テストデータ作成や簡単なシミュレーションでも活躍します。
rand と srand の基本を理解すると、「毎回少し違う結果を出すプログラム」を自然に作れるようになります。