閉じる

【C言語】srand使い方まとめ|初心者がやりがちなNG例と正しい初期化方法

C言語で乱数を扱うときに登場するsrandですが、実は使い方を間違えると、乱数なのに「毎回同じ結果になってしまう」「偏った値しか出ない」などの問題が起きます。

本記事では、初心者がつまずきやすいポイントを具体的なコード例とともに丁寧に解説し、正しい初期化方法とNG例を整理していきます。

C言語の乱数とsrandの基本

乱数を出す代表的な関数はrand

C言語で乱数を生成するときは、標準ライブラリstdlib.hに含まれるrand関数を使います。

これは擬似乱数生成器で、0以上RAND_MAX以下の整数を返します。

rand関数の基本仕様

randは、次のような性質を持っています。

  • 戻り値はint型の非負整数
  • 取り得る範囲は0RAND_MAX(環境依存、よくある値は32767など)
  • 乱数といっても擬似乱数であり、内部状態(シード)が同じなら同じ列を生成

このRAND_MAXstdlib.h内で定義されている定数です。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    printf("RAND_MAX = %d\n", RAND_MAX);  // 環境ごとのRAND_MAXを表示
    return 0;
}
実行結果
RAND_MAX = 32767

※出力値は実行環境によって異なります。

擬似乱数とシード(種)という考え方

擬似乱数は、真のランダムではなく、決まったアルゴリズムに従って値が生成される列です。

同じ初期状態(シード)から開始すると、常に同じ乱数列が得られます。

シード(seed)とは、この擬似乱数生成器に与える初期値のことです。

C言語ではsrand関数でシードを設定します。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // シードを設定
    srand(123);

    // 乱数を3つ表示
    printf("%d\n", rand());
    printf("%d\n", rand());
    printf("%d\n", rand());

    return 0;
}
実行結果
10322
4897
23489

同じプログラムを何度実行しても、必ず同じ3つの値が出力されます。

これはrandが「シード123から始まる決まった乱数列」を生成しているからです。

srandとは何か

srandの役割と基本的な使い方

srandrandが生成する擬似乱数列の初期値(シード)を設定する関数です。

ヘッダはstdlib.hに定義されています。

C言語
#include <stdlib.h>

void srand(unsigned int seed);

srandの基本的な役割

srandを呼び出さない場合、標準では実装依存ですが、一般的にはシードが1などの固定値に初期化されており、プログラムを起動するたびに同じ乱数列になってしまいます。

逆にsrandを適切な値で1回だけ呼び出すことで、実行ごとに異なる乱数列を得ることができます。

典型的な初期化パターンsrand(time(NULL))

実行のたびに異なるシード値を与えるために、現在時刻を使うのが最も一般的です。

時刻はtime関数で取得できます。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>  // timeを使うために必要

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

    // 乱数を3つ表示
    printf("%d\n", rand());
    printf("%d\n", rand());
    printf("%d\n", rand());

    return 0;
}
実行結果
10653
2529
20687

このプログラムを何度も実行すると、実行した時刻が異なる限り、異なる3つの値が出力されます。

ポイントとして、time(NULL)time_t型を返し、秒単位の値であることが多いです。

そのため、同じ秒の間に起動されたプログラムは同じシードになってしまう可能性があります。

この点については後ほど詳しく説明します。

srandを使うときの正しい書き方

プログラム内での配置位置

srandは通常、プログラム開始時に1回だけ呼び出します

よくあるパターンはmain関数の最初で呼び出す方法です。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    // 乱数の初期化(1回だけ)
    srand((unsigned int)time(NULL));

    // ここから先でrandを自由に使う
    for (int i = 0; i < 5; i++) {
        int r = rand();  // 初期化済みの乱数生成器を使用
        printf("%d\n", r);
    }

    return 0;
}
実行結果
15779
10068
8373
11812
16189

重要な点は、srandをループ内や頻繁に呼ばれる関数の中に書かないことです。

これをやってしまうと、かえって乱数の質が悪くなる典型的なNG例になります。

乱数の範囲を指定する方法

rand0RAND_MAXの範囲で値を返しますが、実用上は「0〜9」「1〜6」のような任意の範囲で乱数を使いたい場面が多いです。

代表的な変換方法は、剰余演算子%を使うやり方です。

例: 0〜9の乱数を生成する

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    srand((unsigned int)time(NULL));

    for (int i = 0; i < 10; i++) {
        int r = rand() % 10;  // 0〜9の値
        printf("%d\n", r);
    }

    return 0;
}
実行結果
3
7
0
1
9
5
8
4
2
6

例: 1〜6のサイコロの目を生成する

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    srand((unsigned int)time(NULL));

    for (int i = 0; i < 10; i++) {
        int dice = rand() % 6 + 1;  // 1〜6
        printf("%d\n", dice);
    }

    return 0;
}
実行結果
4
1
6
2
5
3
3
6
1
4

剰余演算子を使うと理論上の偏りが生じる点はありますが、C言語の標準乱数と組み合わせて簡易的な用途であれば、初心者のうちはこの方法で問題ないことが多いです。

初心者がやりがちなNG例

ここからは「こう書くとまずい」というパターンを具体的に紹介し、なぜ問題なのかを解説します。

NG例1: srandをループの中で何度も呼ぶ

最も多い誤りがループの中で毎回srandを呼び出してしまうパターンです。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    for (int i = 0; i < 10; i++) {
        // 【NG】ループのたびにシードを設定してしまう
        srand((unsigned int)time(NULL));

        int r = rand();
        printf("%d\n", r);
    }

    return 0;
}
実行結果
12489
12489
12489
12489
12489
12489
12489
12489
12489
12489

※実際には完全に同じにならない場合もありますが、かなり似たような結果になります。

このコードでは、time(NULL)秒単位であるため、ループが高速に回ると同じ秒の間は常に同じシードが設定されます。

その結果、毎回rand同じ乱数列の先頭から開始してしまい、乱数らしさがほとんど失われてしまいます。

正しい書き方

srandはループの外、つまりプログラム開始時に1回だけ呼び出すようにします。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    // ここで1回だけ初期化
    srand((unsigned int)time(NULL));

    for (int i = 0; i < 10; i++) {
        int r = rand();
        printf("%d\n", r);
    }

    return 0;
}

NG例2: srandを複数回呼んで「よりランダム」にしようとする

初心者の中には、たくさんsrandを呼べば呼ぶほどランダムになると誤解する方もいますが、これは逆効果です。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    // 【NG】何度もシードを設定しても意味がない
    srand((unsigned int)time(NULL));
    srand((unsigned int)time(NULL) + 1);
    srand((unsigned int)time(NULL) + 2);

    // 実際に有効なのは最後のsrandだけ
    printf("%d\n", rand());

    return 0;
}
実行結果
10435

このように、最後のsrandだけが有効で、それ以前の呼び出しはすべて無駄になります。

シードを頻繁に変えると、かえって乱数列の性質を理解しづらくなり、不具合の原因にもなります。

NG例3: srandを呼び忘れて毎回同じ乱数列になる

逆パターンとしてsrandを一度も呼ばないケースもよく見られます。

この場合、多くの実装ではsrand(1)などが内部で暗黙に呼ばれた状態と同じになり、毎回同じ乱数列を得ることになります。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // 【NG】srandを呼んでいない
    for (int i = 0; i < 5; i++) {
        printf("%d\n", rand());
    }

    return 0;
}
実行結果
41
18467
6334
26500
19169

このプログラムを何度実行しても、必ず同じ5つの値が出力されます。

テストには便利ですが、ゲームなどランダム性が必要なアプリケーションでは問題になります。

NG例4: time(NULL)だけを過信してしまう

time(NULL)は秒単位であることが多いため、1秒以内に連続実行したプログラムでは、同じシードになってしまいます。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    // 秒単位の時刻をシードに使用
    time_t t = time(NULL);
    printf("time(NULL) = %ld\n", (long)t);

    srand((unsigned int)t);
    printf("rand() = %d\n", rand());

    return 0;
}
実行結果
time(NULL) = 1700000000
rand() = 14329

このプログラムを1秒以内に何度も起動すると、まったく同じ出力になることがあります。

短時間で何度もプロセスを起動するような用途では、time(NULL)だけに頼るのは十分ではない可能性があります。

srandの正しい初期化パターン

基本形: srand(time(NULL)) を1回だけ呼ぶ

もっとも一般的で、初心者がまず覚えるべきパターンは「プログラム開始時にsrand(time(NULL))を1回だけ呼ぶ」形です。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void init_random(void) {
    // 乱数の初期化はこの関数で1回だけ行う
    srand((unsigned int)time(NULL));
}

int main(void) {
    init_random();  // 最初に必ず呼ぶ

    for (int i = 0; i < 10; i++) {
        int r = rand();
        printf("%d\n", r);
    }

    return 0;
}
実行結果
21357
27418
10840
19405
23693
15202
25121
10379
3166
4082

このようにinit_random関数を用意しておくと、どこで乱数を初期化しているかがコード上で明確になり、読みやすくなります。

より細かいシードが欲しい場合の工夫例

time(NULL)の解像度が秒単位であり、かつ短時間に複数プロセスを起動するような状況では、ミリ秒単位など、より細かい時間情報をシードに利用したい場合があります。

標準Cだけでミリ秒を取得する方法は用意されていませんが、clock関数などと組み合わせて、ある程度細かい値を得る工夫ができます。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

unsigned int make_seed(void) {
    // timeとclockを組み合わせてシードを生成する例
    time_t t = time(NULL);
    clock_t c = clock();

    // 時刻とCPU時間をXORして混ぜる(簡易的な例)
    unsigned int seed = (unsigned int)t ^ (unsigned int)c;
    return seed;
}

int main(void) {
    unsigned int seed = make_seed();
    printf("seed = %u\n", seed);

    srand(seed);

    for (int i = 0; i < 5; i++) {
        printf("%d\n", rand());
    }

    return 0;
}
実行結果
seed = 639240421
14770
25093
25804
12863
11676

この方法でも完全ではありませんが、秒単位の時刻だけを使うよりも、多少の変化を期待できます。

実際のアプリケーションでは、OS依存の高精度タイマーや乱数APIを利用するほうが望ましい場面も多いです。

デバッグ時に「わざと乱数列を固定する」テクニック

必ずしもsrandは毎回time(NULL)で初期化しなければならないわけではありません。

デバッグ時には、あえて乱数列を固定することで再現性のあるテストが行いやすくなります。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 0なら固定シード、1なら時刻でシード
#define USE_TIME_SEED 0

void init_random(void) {
#if USE_TIME_SEED
    // 実行ごとに乱数列を変えたいとき
    srand((unsigned int)time(NULL));
#else
    // デバッグで乱数列を固定したいとき
    srand(1234);
#endif
}

int main(void) {
    init_random();

    for (int i = 0; i < 5; i++) {
        printf("%d\n", rand());
    }

    return 0;
}
実行結果
8221
30156
29152
11196
3277

このように条件付きコンパイルや設定フラグを使うと、本番とデバッグで乱数の挙動を切り替えることが簡単になります。

よくある用途別の具体例

例1: サイコロゲーム

サイコロを振って出目を表示する簡単なゲームを通して、srandrandの使い方を整理します。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int roll_dice(void) {
    // 1〜6のサイコロの目を返す
    return rand() % 6 + 1;
}

int main(void) {
    // 乱数の初期化はプログラム開始時に1回だけ
    srand((unsigned int)time(NULL));

    printf("サイコロを3回振ります。\n");

    for (int i = 0; i < 3; i++) {
        int dice = roll_dice();
        printf("%d回目の出目: %d\n", i + 1, dice);
    }

    return 0;
}
実行結果
サイコロを3回振ります。
1回目の出目: 4
2回目の出目: 1
3回目の出目: 6

ここで重要なのは、roll_dice関数の中ではsrandを呼んでいない点です。

「サイコロを振るたびにsrandを呼ぶ」のはNGであり、シードの設定は必ずプログラム開始時に1回だけ行います。

例2: 配列をランダムシャッフルする

乱数を使う典型的な用途として、配列の中身をランダムに並べ替える処理があります。

ここではFisher–Yatesシャッフル(またはKnuthシャッフル)と呼ばれるよく知られたアルゴリズムを例にします。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 配列をランダムに並べ替える関数
void shuffle(int *array, int n) {
    for (int i = n - 1; i > 0; i--) {
        // 0〜iの範囲でランダムなインデックスを選ぶ
        int j = rand() % (i + 1);

        // array[i]とarray[j]を交換する
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

int main(void) {
    // 乱数の初期化
    srand((unsigned int)time(NULL));

    int data[] = {1, 2, 3, 4, 5};
    int n = sizeof(data) / sizeof(data[0]);

    printf("シャッフル前: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", data[i]);
    }
    printf("\n");

    shuffle(data, n);

    printf("シャッフル後: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", data[i]);
    }
    printf("\n");

    return 0;
}
実行結果
シャッフル前: 1 2 3 4 5 
シャッフル後: 3 5 1 4 2

この例でも、シードの設定はmain関数内で1回だけ行っており、shuffle関数の中ではsrandを一切呼んでいません。

これが正しいsrandの使い方です。

例3: ランダムな文字列(英数字)を生成する

もう少し実用的な例として、ランダムな英数字の文字列を生成するコードを示します。

パスワードなどの用途には不十分ですが、テスト用IDなど簡易的な場面では役立ちます。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void make_random_string(char *buf, int length) {
    const char charset[] =
        "0123456789"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "abcdefghijklmnopqrstuvwxyz";

    int charset_size = (int)(sizeof(charset) - 1);  // 終端'\0'を除く

    for (int i = 0; i < length; i++) {
        int r = rand() % charset_size;
        buf[i] = charset[r];
    }

    buf[length] = '\0';  // 文字列終端
}

int main(void) {
    srand((unsigned int)time(NULL));

    char token[17];  // 16文字 + 終端

    make_random_string(token, 16);
    printf("ランダム文字列: %s\n", token);

    return 0;
}
実行結果
ランダム文字列: H3FaZ0nqLm92TsBg

ここでもやはり、make_random_stringの中ではsrandを呼んでいないことに注目してください。

乱数を使う関数と、乱数を初期化する場所は分けるのが基本です。

srand/randに関する補足知識

srandとrandはスレッド安全か

標準Cのrandsrandは、多くの実装でスレッド安全ではありません

複数スレッドから同時にrandを呼び出すと、内部状態が競合し予測不能な挙動になる可能性があります。

マルチスレッド環境で乱数を使う場合は、次のような対策が考えられます。

  • 実装依存のrand_r(再入可能版)を利用する
  • スレッドごとに独立した乱数生成器を用意し、ロックで保護する
  • OSやライブラリが提供するスレッド安全な乱数APIを利用する

初心者の段階では「randとsrandはマルチスレッドでそのまま使うと危険」という認識だけ持っておくと良いです。

セキュリティ用途には使わない

randsrandは、セキュリティ用途の乱数(暗号鍵、トークン、ワンタイムパスワードなど)には適していません。

アルゴリズムが単純であり、出力から内部状態を推測されやすいためです。

セキュアな乱数が必要な場合は、必ず暗号論的擬似乱数生成器(CSPRNG)を使用してください。

たとえば以下のようなOS依存APIがあります。

  • Unix系: /dev/urandomgetrandomなど
  • Windows: CryptGenRandomなど

標準Cだけではカバーできない領域であるため、用途に応じてライブラリやOS APIを選ぶことが重要です。

srandのシードに0を使ってもよいか

srand(0)のようにシードに0を指定すること自体は問題ありません

C標準では、シード値の特別な制限を定めていません。

ただし、実装によっては0を内部で別の値に変換して扱うなどの挙動をする場合もあり得るため、「0は絶対にだめ」というわけではないが、「どの値でもよい」のが基本と理解しておくとよいです。

まとめ

C言語のsrandは、乱数生成器randに「種(シード)」を与える初期化関数です。

プログラム開始時に1回だけsrandを呼び、その後はrandを好きなだけ使うというのが正しい基本形になります。

よくあるNG例として、ループ内や関数内で何度もsrandを呼んでしまい、かえって乱数が不自然になるパターンがあります。

また、time(NULL)は秒単位であり、短時間に複数起動すると同じ乱数列になる点にも注意が必要です。

用途に応じて、srand(time(NULL))、固定シード、より高精度なシード生成などを使い分けながら、再現性とランダム性のバランスを取っていくことが大切です。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!