Números aleatorios en C: rand, rangos sin sesgo, mezclar y arc4random

Números aleatorios en C para cada caso: rand y srand para juegos y simulaciones que se pueden repetir, cómo sacar un número en un rango sin sesgo, cómo mezclar un array correctamente y qué usar cuando el número es un secreto, como un token o una contraseña.

rand: para juegos y simulaciones

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

int main(void)
{
    srand(2026);
    for (int i = 0; i < 8; i++) {
        printf("%d ", rand() % 6 + 1);
    }
    printf("\nRAND_MAX = %d\n", RAND_MAX);
    return 0;
}
salida (con glibc, siempre la misma para la semilla 2026)2 1 2 4 5 6 6 3 
RAND_MAX = 2147483647

rand() devuelve un número entre 0 y RAND_MAX, y srand fija la semilla: con la misma semilla, la misma secuencia. Eso es justo lo que se quiere en una simulación o un juego que hay que poder repetir, y por eso en el ejemplo la semilla es fija. Para que cambie en cada ejecución se suele usar srand(time(nullptr)), pero tiene un problema: dos programas que arrancan en el mismo segundo sacan los mismos números.

Dos limitaciones más. La calidad de rand depende de cada biblioteca y el estándar no exige casi nada. Y rand() % 6 tiene un pequeño sesgo: si RAND_MAX + 1 no es múltiplo de 6, algunos resultados salen un poco más que otros.

Un número en un rango sin sesgo

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

/* Entero uniforme entre min y max, ambos incluidos, a partir de rand(), descartando el sobrante. */
int aleatorio_entre(int min, int max)
{
    unsigned rango = (unsigned) (max - min) + 1;
    unsigned limite = ((unsigned) RAND_MAX + 1u) / rango * rango;
    unsigned r;
    do {
        r = (unsigned) rand();
    } while (r >= limite);
    return min + (int) (r % rango);
}

int main(void)
{
    srand(7);
    int cuenta[6] = {};
    for (int i = 0; i < 600000; i++) {
        cuenta[aleatorio_entre(1, 6) - 1]++;
    }
    for (int i = 0; i < 6; i++) {
        printf("%d: %d\n", i + 1, cuenta[i]);
    }
    return 0;
}
salida1: 99835
2: 99946
3: 99861
4: 100240
5: 100319
6: 99799

La técnica se llama muestreo por rechazo: se descartan los valores del último tramo incompleto, los que provocarían el sesgo, y se vuelve a tirar. En 600,000 tiradas, cada cara sale unas 100,000 veces. Con un RAND_MAX tan grande como el de glibc el sesgo de % sería minúsculo, pero en sistemas donde vale 32,767 se nota.

Mezclar un array: Fisher-Yates

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

static size_t entre(size_t n)
{
    return (size_t) arc4random_uniform((unsigned) n);
}

void mezclar(int *v, size_t n)
{
    for (size_t i = n - 1; i > 0; i--) {
        size_t j = entre(i + 1);
        int tmp = v[i];
        v[i] = v[j];
        v[j] = tmp;
    }
}

int main(void)
{
    int cartas[10];
    for (int i = 0; i < 10; i++) cartas[i] = i + 1;
    mezclar(cartas, 10);

    int suma = 0;
    for (int i = 0; i < 10; i++) suma += cartas[i];
    printf("Mezcladas; siguen sumando %d\n", suma);
    return 0;
}
salidaMezcladas; siguen sumando 55

El algoritmo de Fisher-Yates recorre el array de atrás hacia delante e intercambia cada posición con una elegida al azar entre ella y el principio. Así, todas las ordenaciones posibles salen con la misma probabilidad. La versión «ingenua», intercambiar cada elemento con uno cualquiera del array entero, parece equivalente pero no lo es: favorece unas ordenaciones sobre otras. (El programa no imprime el orden porque cambia cada vez; imprime la suma, que demuestra que no se perdió ni se duplicó ninguna carta.)

Para secretos: arc4random y getrandom

#include <stdio.h>
#include <stdlib.h>
#include <sys/random.h>

int main(void)
{
    unsigned char clave[16];
    arc4random_buf(clave, sizeof clave);

    unsigned char otra[16];
    ssize_t n = getrandom(otra, sizeof otra, 0);

    printf("arc4random_buf: %zu bytes, getrandom: %zd bytes\n", sizeof clave, n);
    printf("PIN de un solo uso: %06u\n", arc4random_uniform(1000000));
    return 0;
}
salida (el PIN cambia en cada ejecución)arc4random_buf: 16 bytes, getrandom: 16 bytes
PIN de un solo uso: 913859

Para tokens de sesión, contraseñas temporales, claves o códigos de verificación, rand no sirve: su estado se puede reconstruir viendo unas pocas salidas. Hay que usar el generador criptográfico del sistema. arc4random_buf y arc4random_uniform están en glibc desde la versión 2.36, en macOS y en los BSD, y no necesitan semilla; getrandom es la llamada al sistema de Linux que hay debajo. En Windows, el equivalente es BCryptGenRandom. Más en Seguridad en C.

  • ParaUsa
  • Simulaciones y juegos repetiblesrand con semilla fija, o un generador propio como xorshift o PCG
  • Un rango sin sesgomuestreo por rechazo, o arc4random_uniform
  • MezclarFisher-Yates
  • Secretosarc4random_buf, getrandom; nunca rand

Todo el código de esta página se compiló con GCC 15.3 (C23, -Wall -Wextra, sin avisos) y se ejecutó en Linux, en un contenedor limpio sin acceso a la red, antes de publicarla; las salidas están copiadas de esa ejecución.