【C++】乱数が毎回同じになる理由とは?|srandとmt19937の使い方を解説

C++

今回は、C++での「乱数が毎回同じになる理由」と「正しい乱数の使い方」について解説していきます。

「rand()を使ったのに、実行するたびに同じ数が出てくる…」
「srandって何?書かないといけないの?」
「ゲームのランダム処理がぜんぜんランダムじゃない」

こんな疑問はありませんか?

C++の乱数はきちんと理解して使わないと、毎回まったく同じ数列が出続けてしまいます。
原因を知ればすぐ直せるので、仕組みからしっかり押さえておきましょう。

この記事を読み終えると、乱数が毎回同じになる原因と正しい使い方がわかるようになると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

そもそもrand()とは?

rand() とは、

  • 内部の計算式を使って、疑似的な乱数を返す」関数

です。

ここで大事なのが「疑似乱数」という言葉です。
rand() はサイコロのように本当にランダムな値を出すのではなく、シード(種)と呼ばれる初期値をもとに計算で値を生成しています。

そのため、シードが同じなら、毎回まったく同じ数列が出てきます。

毎回同じになる理由

まずはNG例を見てみましょう。

▼main.cpp


#include <iostream>
#include <cstdlib>

int main() {
    // srand()を呼ばずにrand()だけ使う
    for (int i = 0; i < 5; i++) {
        std::cout << rand() << "\n";
    }
    return 0;
}

実行結果(何度実行しても同じ)

41
18467
6334
26500
19169

このように、srand()を呼ばない場合はシードが自動的に 1 固定になります。
シードが同じなので、当然出てくる数列も毎回まったく同じです。

基本の解決策:srand(time(NULL))

srand()は、

  • rand()のシード(種)を設定する」関数

です。

よく使われるのが srand(time(NULL)) です。
time(NULL) は現在時刻を秒単位で返すため、プログラムを実行するたびに違うシードが設定されて、毎回違う乱数が出るようになります。

▼main.cpp


#include <iostream>
#include <cstdlib>
#include <ctime>

int main() {
    srand(static_cast<unsigned int>(time(NULL))); // シードを時刻で設定

    for (int i = 0; i < 5; i++) {
        std::cout << rand() << "\n";
    }
    return 0;
}

実行結果(実行するたびに変わる)

1番目の実行:
23057
4128
19801
7643
31204

2番目の実行:
8742
27341
516
13899
20118

毎回違う数が出るようになりました。
ポイントは、srand()はプログラムの最初に1回だけ呼ぶことです。

モダンな書き方:mt19937 + random_device

rand()srand() の組み合わせは古い書き方で、乱数の品質もあまり高くありません。
C++11以降では、mt19937random_device を使うのが現代的な書き方として推奨されています。

▼main.cpp


#include <iostream>
#include <random>

int main() {
    std::random_device rd;           // ハードウェア由来の真の乱数でシードを作る
    std::mt19937 mt(rd());           // mt19937にシードを渡して初期化

    // 1〜100の整数を一様分布で取得
    std::uniform_int_distribution<int> dist(1, 100);

    for (int i = 0; i < 5; i++) {
        std::cout << dist(mt) << "\n";
    }
    return 0;
}

実行結果

73
12
88
45
31
(実行のたびに変わる)

この書き方の利点は3つです。

  • random_device:OSやハードウェアの情報から真の乱数でシードを作るため、予測されにくい
  • mt19937:乱数の品質が高く、周期も非常に長い(rand()より信頼性が高い)
  • uniform_int_distribution:指定した範囲の乱数を偏りなく取れる

なお、この書き方を関数に分けて使う場合は、プロトタイプ宣言の記事もあわせて読んでみてください。

【重要】私が実際にゲーム制作でハマった体験談

自主制作のゲームにドロップアイテムのシステムを実装したときに、まんまとこの問題にハマりました。

敵を倒したときに「剣」「盾」「回復薬」のどれかがランダムで出るようにしたかったのですが、何度テストしても毎回まったく同じアイテムが出続けます。コードを何度見直しても間違っているように見えない…。

30分ほど悩んだ末、原因はシンプルでした。srand()をどこにも書いていなかったのです。
シードが常に 1 固定だったため、毎回まったく同じ数列が生成されていました。

srand(time(NULL))main() の先頭に1行追加しただけで、ちゃんとランダムなドロップが出るようになりました。

「ランダムに見えるのに毎回同じ結果」という現象が起きたら、まず srand() を呼んでいるか確認するのが最初の一手です。

よくある失敗例と対処法

  • srand()を書き忘れている
    → シードがデフォルトの 1 固定になり、毎回同じ数列になります。main()の先頭に1回だけ srand(time(NULL)) を書きましょう。

  • ループの中でsrand()を呼んでいる
    → ループのたびにシードがリセットされ、同じ値が連続して出やすくなります。srand()はループの外で1回だけ呼ぶのが鉄則です

  • srand(time(NULL))を短時間に何度も呼んでいる
    time(NULL) は秒単位の値を返します。1秒以内に複数回呼ぶと同じシードになってしまいます。関数の中でsrand()を呼ぶ設計は避けましょう。

乱数の結果によって処理を分岐する場面では、if文とswitch文の使い分けの記事も参考にしてみてください。

rand()とmt19937の使い分け

  • rand() + srand():手軽に書けるが品質は低め。簡単なテストや学習向け
  • mt19937 + random_device:品質が高く現代的。ゲーム制作や実用コードではこちらを使うのがおすすめ

乱数を保持する変数の扱い方が気になる方は、staticの使い方の記事もあわせて読むと、「乱数生成器を関数内で使い回す」設計が理解しやすくなります。

まとめ

  • rand()シード(種)をもとに計算で値を作る疑似乱数
  • srand()を呼ばないとシードがデフォルトの 1 固定になり、毎回同じ数列になる
  • srand(time(NULL))main()の先頭に1回だけ書くと毎回違う乱数になる
  • srand()はループの外で1回だけ呼ぶのが鉄則
  • 品質重視なら mt19937 + random_device + uniform_int_distribution がモダンな書き方

これで乱数が毎回同じになる問題はバッチリですね!

ここまで読んでくださり、ありがとうございました。
この記事が皆様の学習の役に立てば幸いです。

関連記事