I numeri casuali del C arrivano da due funzioni di <stdlib.h>: rand(), che produce il valore successivo, e srand(), che fissa il punto di partenza. Non sono davvero casuali: sono una sequenza pseudocasuale, calcolata in modo deterministico a partire da un seme. Per la crittografia è un limite, per i test è un vantaggio.
rand() e RAND_MAX
rand() restituisce un int compreso tra 0 e RAND_MAX, estremi inclusi. RAND_MAX è una macro che vale almeno 32767; su Linux e macOS è 2147483647.
Eseguilo due volte. I numeri sono identici entrambe le volte, e non è un bug.
Inizializzare il seme con srand
Senza una chiamata a srand, la sequenza si comporta come se avessi chiamato srand(1). Stesso seme, stessa sequenza, a ogni esecuzione. Per ottenere numeri diversi a ogni esecuzione, usa come seme qualcosa che cambia, per convenzione l'ora corrente:
time(NULL) di <time.h> restituisce i secondi trascorsi dall'inizio del 1970, quindi ogni esecuzione riceve un seme diverso. Il cast a unsigned int zittisce un avviso sul restringimento di time_t.
Tre regole sul seme, e le si sbaglia tutte e tre:
Inizializza il seme una sola volta, all'inizio di main. Chiamare srand prima di ogni rand() è il classico anti-pattern: dentro un ciclo che finisce in meno di un secondo, time(NULL) restituisce lo stesso valore a ogni iterazione, quindi reinizializzi con lo stesso numero e rand() restituisce ogni volta lo stesso primo valore. Il risultato è una colonna di numeri "casuali" tutti uguali.
Non reinizializzare per "migliorare" la casualità. La qualità del generatore viene dall'avanzamento del suo stato interno; azzerare quello stato butta via la sequenza.
time(NULL) ha una risoluzione di un secondo. Due programmi avviati nello stesso secondo ottengono la stessa sequenza. Per un gioco va bene, per qualsiasi cosa in cui conta l'indipendenza no.
Un numero in un intervallo
L'idioma standard usa l'operatore resto:
rand() % n /* da 0 a n-1 */
rand() % n + min /* da min a min+n-1 */
Per ottenere da min a max inclusi, il numero di valori possibili è max - min + 1:
Il + 1 è dove si annidano gli errori off-by-one. rand() % 6 dà da 0 a 5, quindi un lancio di dado è rand() % 6 + 1. Scrivere rand() % 7 + 1 per "includere il 6" ti dà un dado a sette facce.
Una nota onesta sul bias del modulo
rand() % n non è perfettamente uniforme a meno che n non divida esattamente RAND_MAX + 1.
Pensaci con numeri piccoli. Se RAND_MAX fosse 9, cioè rand() restituisse da 0 a 9, dieci valori ugualmente probabili, allora rand() % 3 porterebbe 0,3,6,9 su 0; 1,4,7 su 1; e 2,5,8 su 2. Il risultato 0 esce in quattro modi su dieci, i risultati 1 e 2 in tre modi ciascuno. Lo zero è più probabile del 33%.
Lo stesso sbilanciamento esiste con il vero RAND_MAX, solo molto più piccolo: i valori in eccesso sono i primi (RAND_MAX + 1) % n risultati, e ognuno guadagna una possibilità in più su circa 2,1 miliardi. Per un lancio di dado, un mazzo mescolato o una simulazione è impossibile da misurare: usa % e vai avanti.
Quando invece conta, per lavori statistici o qualsiasi cosa legata alla sicurezza, scarta i valori in eccesso invece di ripiegarli nell'intervallo:
Il ciclo scarta il piccolo intervallo di valori che causerebbe lo sbilanciamento ed estrae di nuovo. Termina in fretta: la fetta scartata è una frazione minuscola del totale.
Per una casualità davvero sensibile alla sicurezza, rand() è lo strumento sbagliato a prescindere dalla cura che ci metti: usa arc4random_buf su macOS e BSD, getrandom() su Linux o BCryptGenRandom su Windows.
Double casuali
Dividi per RAND_MAX per finire in [0.0, 1.0], poi scala:
Il cast in (double) rand() è essenziale. Senza, rand() / RAND_MAX è una divisione intera e vale 0 quasi sempre e 1 nell'unica possibilità su due miliardi di colpire il massimo: un bug che si presenta come "i miei double casuali sono tutti zero". Leggi il type casting per capire perché.
Sequenze riproducibili
Un seme fisso dà la stessa identica sequenza a ogni esecuzione, ed è proprio ciò che vuoi per un test, una sessione di debug o un gioco con codici di livello da condividere:
Il seme 42 produce gli stessi cinque numeri ogni volta che viene usato, in questa esecuzione e in qualsiasi altra con la stessa libreria. Questa riproducibilità è il motivo per cui una simulazione dovrebbe permettere di scegliere il seme: normalmente la esegui con l'orologio, e passi un seme fisso quando devi riprodurre un bug.
Un'avvertenza: la sequenza per un dato seme non è portabile. Librerie C diverse usano generatori diversi, quindi il seme 42 su glibc e il seme 42 su Windows danno numeri diversi. Riproducibile su una macchina, non tra macchine diverse.
Un gioco di dadi
Tutto insieme: seme inizializzato una volta, una funzione di supporto per l'intervallo e un array che conta i risultati:
L'istogramma dovrebbe avere il picco sul 7 e scendere verso il 2 e il 12: ci sono sei modi di fare 7 e un solo modo ciascuno per fare 2 o 12. Un generatore che qui producesse una distribuzione piatta sarebbe difettoso.
Due pagine collegate: la libreria standard mappa il resto di <stdlib.h>, e le funzioni matematiche trattano <math.h>, che ti servirà appena i valori casuali entrano in calcoli veri.
Domande frequenti
Come si genera un numero casuale in C?
Includi <stdlib.h>, inizializza il seme una volta all'inizio di main con srand((unsigned) time(NULL)) (che richiede <time.h>) e poi chiama rand() per ogni valore. rand() restituisce un int compreso tra 0 e RAND_MAX, estremi inclusi.
Come si ottiene un numero casuale tra due valori in C?
Usa rand() % (max - min + 1) + min. Per un lancio di dado tra 1 e 6 diventa rand() % 6 + 1. Il % n porta il risultato in 0..n-1 e sommare min sposta la finestra: assicurati solo che il conteggio includa entrambi gli estremi, ed è quello che fa il + 1.
Perché il mio programma C stampa sempre gli stessi numeri casuali?
Perché non hai mai chiamato srand. Senza seme, rand() si comporta come se fosse inizializzato con 1, quindi ogni esecuzione produce la stessa identica sequenza. Chiama srand((unsigned) time(NULL)) una volta all'avvio del programma: una volta, non prima di ogni chiamata a rand(), che peggiorerebbe le cose.
Cos'è il bias del modulo nella generazione di numeri casuali?
rand() % n è perfettamente uniforme solo quando n divide esattamente RAND_MAX + 1. Altrimenti i primi valori compaiono una volta in più sull'intero intervallo e diventano leggermente più probabili. Con RAND_MAX pari a 2147483647 e un n piccolo lo scarto è ben al di sotto di ciò che un gioco o una simulazione possa notare, ma per crittografia o statistica usa un ciclo di rifiuto o un generatore adeguato.