C / STANDARD LIBRARY TOUR
Random numbers and seeding without repeating sequences
Seed rand() correctly once per run, replay any run from a saved seed, and cut a range down to 1..6 without modulo bias.
What you will learn
- Call srand exactly once per run; each extra call restarts the same stream.
- Recognise that skipping srand behaves like srand(1) and repeats every run.
- Reduce a range by rejecting the tail instead of writing a bare rand() % n.
- Print the seed you used so a bad run can be replayed exactly.
Understanding Random numbers and seeding without repeating sequences
rand() is not a source of randomness; it is a pure function of a hidden state that the library keeps between calls. srand(s) writes that state, and from then on every rand() result is fixed by s alone, which is why the program below gets the same ten dice out of seed 12345 however often it asks. A program that never calls srand starts as though srand(1) had run, so its "random" output is the identical stream on every launch.
Not repeating is therefore a question about the seed, not about rand(). srand((unsigned)time(NULL)) at the top of main works because the clock has moved since the last run, but two consequences follow from time() ticking only once per second: processes started in the same second share a stream, and reseeding inside a drawing loop pins the state so every draw returns that seed's first value again. The mental model: the seed chooses which page of a fixed book you open, rand() reads the next word, and srand always sends you back to the top of a page.
Squeezing rand()'s RAND_MAX+1 possible results into n buckets is the other half of the job. rand() % n hands some buckets one extra chance whenever n does not divide RAND_MAX+1 evenly, which is invisible for n = 6 with RAND_MAX = 2147483647 and glaring once n is a large fraction of RAND_MAX. Discarding every draw at or above the largest multiple of n removes the bias for the price of a loop that almost never iterates twice. None of this makes rand() fit for keys or tokens; for those, read from the operating system generator.
<stdio.h>
<stdlib.h>
<time.h>
/* Uniform value in 0..bound-1: reject the ragged tail, then take the remainder. */
static int uniform(int bound)
{
unsigned long span = (unsigned long)RAND_MAX + 1UL;
unsigned long limit = span - span % (unsigned long)bound;
unsigned long r;
do {
r = (unsigned long)rand();
} while (r >= limit);
return (int)(r % (unsigned long)bound);
}
static void roll_ten(int *out)
{
int i;
for (i = 0; i < 10; i++)
out[i] = uniform(6) + 1;
}
static int identical(const int *a, const int *b)
{
int i;
for (i = 0; i < 10; i++)
if (a[i] != b[i])
return 0;
return 1;
}
int main(void)
{
int a[10], b[10], c[10];
int i, t, ok = 1;
srand(12345u);
roll_ten(a);
srand(12345u); /* same seed, same ten rolls, every time */
roll_ten(b);
srand(54321u); /* a different seed, a different stream */
roll_ten(c);
for (i = 0; i < 10; i++)
if (a[i] < 1 || a[i] > 6 || c[i] < 1 || c[i] > 6)
ok = 0;
printf("same seed (12345, 12345) -> identical sequence: %s\n",
identical(a, b) ? "yes" : "no");
printf("other seed (12345, 54321) -> identical sequence: %s\n",
identical(a, c) ? "yes" : "no");
printf("all rolls inside 1..6: %s\n", ok ? "yes" : "no");
srand((unsigned)time(NULL)); /* one seed per run; the roll itself varies */
t = uniform(6) + 1;
printf("time-seeded roll inside 1..6: %s\n", (t >= 1 && t <= 6) ? "yes" : "no");
return 0;
}
The seed fully determines the sequence, so unrepeated output comes from seeding once with something that changes, and fairness comes from how you reduce the range.
Worked examples
Counting the modulo bias exactly
Enumerates every possible rand() result to show how rand() % 10000 favours small remainders when RAND_MAX is 32767.
<stdio.h>
/* How often does rand() % 10000 land on 0 versus 9999, if RAND_MAX is 32767? */
int main(void)
{
unsigned long span = 32768; /* RAND_MAX + 1 on a minimal implementation */
unsigned long bound = 10000;
unsigned long hits0 = 0, hits9999 = 0, v;
for (v = 0; v < span; v++) {
if (v % bound == 0)
hits0++;
if (v % bound == 9999)
hits9999++;
}
printf("draws that produce 0 : %lu\n", hits0);
printf("draws that produce 9999 : %lu\n", hits9999);
printf("0 is %.1f%% more likely\n", 100.0 * hits0 / hits9999 - 100.0);
return 0;
}
Example explained
Line 1The loop walks every value rand() could return, so these counts are exact rather than sampled.
Line 20 is reachable from 0, 10000, 20000 and 30000, while 9999 is only reachable from 9999, 19999 and 29999.
Line 3The culprit is the short tail 30000..32767, which covers remainders 0..2767 and nothing above them.
Line 4That tail is precisely what the do/while in the main example throws away.
Reseeding inside the loop
Shows that resetting the state before every draw reproduces one value, while seeding once lets the sequence advance.
<stdio.h>
<stdlib.h>
int main(void)
{
int first, i, allsame;
srand(4242);
first = rand(); /* the first value seed 4242 produces */
allsame = 1;
for (i = 0; i < 5; i++) {
srand(4242); /* the bug: state reset before every draw */
if (rand() != first)
allsame = 0;
}
printf("reseeded before every draw -> all five equal: %s\n",
allsame ? "yes" : "no");
allsame = 1;
srand(4242); /* the fix: seed once, then keep drawing */
for (i = 0; i < 5; i++)
if (rand() != first)
allsame = 0;
printf("seeded once, then five draws -> all five equal: %s\n",
allsame ? "yes" : "no");
return 0;
}
Example explained
Line 1srand(4242) followed by rand() has exactly one answer, so the first loop recomputes that answer five times.
Line 2The second loop never touches the state again, so rand() walks forward and only the first draw matches first.
Line 3Swapping 4242 for time(NULL) inside the loop changes nothing: time() returns the same second on all five passes.
Line 4Nothing is illegal here — srand may be called repeatedly — which is why the compiler cannot warn you.
Scaling by division instead of remainder
Measures the bucket sizes of rand() / (RAND_MAX / 6 + 1), the historical alternative to % 6.
<stdio.h>
/* Bucket sizes of rand() / (RAND_MAX / 6 + 1) when RAND_MAX is 32767. */
int main(void)
{
unsigned long span = 32768;
unsigned long divisor = (span - 1) / 6 + 1;
unsigned long counts[6] = {0};
unsigned long v;
int i;
for (v = 0; v < span; v++)
counts[v / divisor]++;
printf("divisor = %lu\n", divisor);
for (i = 0; i < 6; i++)
printf("face %d comes from %lu draws\n", i + 1, counts[i]);
return 0;
}
Example explained
Line 1Division groups consecutive draws into blocks, so it reads the high bits of rand() — the reason old code preferred it when low bits were weak.
Line 2Six full blocks of 5462 would need 32772 draws but only 32768 exist, so the last face is short by four.
Line 3The divisor is RAND_MAX / 6 + 1, not RAND_MAX / 6, or v / divisor would reach 6 and write past counts[5].
Line 4The bias is far smaller than the % 10000 case, yet still nonzero; only rejection makes the six faces exactly equal.
Important notes
rand() and srand() share one global state, so they are not thread-safe, and they must never produce passwords, session tokens or keys — use getrandom, arc4random_buf or BCryptGenRandom for that.
Which numbers a given seed produces is implementation-defined: seed 12345 on glibc and on MSVC do not match, so ship your own generator if a test fixture must be identical across platforms.
Common mistakes
Calling srand(time(NULL)) immediately before each rand(): time() advances once a second, so a fast loop reseeds identically and prints the same number over and over.
Leaving srand out and concluding rand() is broken — the starting state equals srand(1), so the dice or the shuffled deck come out in the same order on every launch.
Writing lo + rand() % hi for an inclusive range: 10 + rand() % 20 yields 10..29, not 10..20, so values silently escape the intended bounds; the correct form is lo + rand() % (hi - lo + 1).
Try it yourself
Change, predict, then run
Fill an array with 0..51, shuffle it with Fisher-Yates using a bias-free uniform(i + 1) helper, and print the first five cards. Run it with srand(1), then with srand((unsigned)time(NULL)), and note which version prints the same five cards every time you press run.
Open the C workspaceCheck your understanding
A program calls srand((unsigned)time(NULL)) at the top of a loop that draws one number per iteration and finishes in a few microseconds. What does it print?
- The same number on every iteration, because time(NULL) returns the same second so the state is reset identically each pass
- Better-quality random numbers, because the generator receives fresh entropy on every pass
- Repeated numbers only if the loop runs more than RAND_MAX times
- Correct random numbers, because srand after the first call is ignored by the library
Show answer
time() has one-second resolution, so all iterations pass the same seed, and each srand rewinds the generator to that seed's first value. Option 2 is the tempting one: reseeding cannot add entropy, it discards the progress the generator has made and restarts from a known state. Option 4 is also wrong — srand may legally be called any number of times, and every call resets the state, which is exactly why the bug is silent.