前言
「隨機數」是程式設計常見需求,但 C 標準庫提供的 rand() 與 srand() 機制常被誤用,導致「每次執行序列相同」或「循環周期極短」。本文深入剖析偽隨機數原理、種子設置陷阱、正確用法與進階替代方案。
核心概念釐清
| 術語 | 定義 |
| 偽隨機數 | 確定性演算法產生,滿足統計隨機性,但可重現。真隨機需硬體熵源(熱雜訊、輻射衰變等) |
| 種子 | 演算法初始狀態。相同種子 → 相同序列。srand(seed) 設定種子,rand() 依種子產生數列 |
| RAND_MAX | rand() 最大回傳值(至少 32767,常為 2147483647) |
標準庫函式詳解
1. void srand(unsigned int seed) — 設定種子
- 程式啟動時若未呼叫
srand(),等同 srand(1)
- 種子必須「變化」才能產生不同序列
- 常見做法:
srand((unsigned)time(NULL))
2. int rand(void) — 取得偽隨機數
- 回傳 0 ~ RAND_MAX 間整數
- 內部為線性同餘法(LCG)或更佳演算法,具週期性
- 關鍵:
rand() 本身不會重新呼叫 srand(),種子只在 srand() 顯式呼叫時設定
3. time_t time(time_t *timer) — 取得秒數當種子
- 回傳 1970-01-01 00:00:00 UTC 至今的秒數
- 精度只到秒,同一秒內多次啟動種子相同
正確使用模式
#include <stdlib.h>
#include <time.h>
int main(void) {
/* 1. 程式啟動時「只呼叫一次」srand */
srand((unsigned)time(NULL));
/* 2. 後續只呼叫 rand(),不要重複 srand */
for (int i = 0; i < 10; i++) {
int r = rand() % 10; /* 0~9,但有模數偏差 */
int r_better = rand() / (RAND_MAX / 10 + 1); /* 均勻分布較佳 */
printf("%d ", r);
}
puts("");
return 0;
}
常見錯誤與修正
| 錯誤寫法 | 後果 | 修正 |
for(...) { srand(time(NULL)); r=rand(); } | 同一秒內種子相同,產生重複數列 | srand() 提到迴圈外,全程只呼叫一次 |
rand() % N 取範圍 | 當 N 不整除 RAND_MAX+1 時,分布不均(模數偏差) | 用 rand() / (RAND_MAX / N + 1) 或拒絕採樣法 |
多執行緒共用 rand() | 資料競爭,序列交錯不可預期 | 用 rand_r() (POSIX) 或 C11 thread_local 狀態,或 mt19937 |
加密用途用 rand() | 可預測,嚴重安全漏洞 | 用 /dev/urandom、CryptGenRandom、getrandom()、std::random_device |
進階替代方案
C11 <stdrandom.h>(草案階段) / C++11 <random>
/* C++11 範例,現代 C 專案常混編使用 */
#include <random>
std::mt19937 gen(std::random_device{}());
std::uniform_int_distribution<int> dist(1, 100);
int r = dist(gen); /* 高品質、無模數偏差、執行緒安全 */
POSIX random()/srandom() / drand48()
- 週期更長、統計特性優於
rand()
random_r() 提供可重入版本
密碼學級隨機數
- Linux:
getrandom(2)、/dev/urandom
- Windows:
BCryptGenRandom()、CryptGenRandom()
- 跨平台:
libsodium randombytes_buf()
總結
srand(time(NULL)) 只在 main 開頭呼叫一次
- 範圍縮放用 除法而非模數 避免偏差
- 多執行緒/高品質/加密需求,棄用
rand() 改用現代 API
- 理解 LCG 週期與統計缺陷,選擇適合場景的產生器