📚 The Standard Library
stdlib.h: conversions, random, qsort & exits
▶ Open the interactive lesson — free, no signupDice rolls, card shuffles, and loot drops all come from a random-number generator — and without one line of setup, a C program produces the exact same "random" results every single run. This lesson fixes that, shows you how to sort any list — scores, names, anything — with a single library call, and gives you a way to turn text like "42" into a number that actually tells you when the text was garbage.
stdlib.h is C's junk drawer of essentials: number parsing, random numbers, sorting, program termination, and reading the settings your operating system hands every program. You already know its most famous residents — malloc, calloc, realloc, free — from Part 2, so today we tour everything else.
String → number: atoi vs strtol
atoi("42") is tempting and terrible: on bad input it returns 0 — indistinguishable from a real 0 — and on overflow its behavior is undefined. The grown-up tool is strtol (and siblings strtoul, strtoll, strtod): it reports exactly where parsing stopped via an endptr, and flags overflow through errno:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
const char *inputs[] = { "42", " 99kg", "banana", "999999999999999999999" };
for (int i = 0; i < 4; i++) {
const char *s = inputs[i];
char *end;
errno = 0; /* clear BEFORE the call */
long v = strtol(s, &end, 10);
if (end == s)
printf("%-22s -> no digits at all (atoi says: %d)\n", s, atoi(s));
else if (errno == ERANGE)
printf("%-22s -> overflow! clamped to %ld\n", s, v);
else if (*end != '\0')
printf("%-22s -> got %ld, junk after: \"%s\"\n", s, v, end);
else
printf("%-22s -> clean parse: %ld\n", s, v);
}
return 0;
}$ gcc strtol.c -o strtol && ./strtol 42 -> clean parse: 42 99kg -> got 99, junk after: "kg" banana -> no digits at all (atoi says: 0) 999999999999999999999 -> overflow! clamped to 9223372036854775807
The rules: if endptr == start, nothing was parsed. If *endptr != '\0', there's trailing junk. If errno == ERANGE, the value overflowed (and you got LONG_MAX/LONG_MIN clamped). Three distinct failure modes atoi silently swallows. The third argument is the base — 0 means auto-detect 0x/0 prefixes like a C compiler would.
🧠 Checkpoint: After long v = strtol("12ab", &end, 10); what are v and *end?
- v = 0, *end = '1'
- v = 12, *end = 'a'
- v = 12, *end = '\0'
- undefined behavior
Show answer
v = 12, *end = 'a' — strtol parses the longest valid prefix (12) and points end at the first unconsumed character ('a'). That endptr is exactly what lets you detect trailing junk — atoi would just return 12 and shrug.
Random numbers: rand & srand
rand() returns a pseudo-random int in [0, RAND_MAX] (at least 32767). The sequence is 100% deterministic — it's computed from a seed, which is why every unseeded program gets the same "random" numbers. Seed once at startup with something that varies, traditionally the clock:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void) {
srand((unsigned)time(NULL)); /* seed ONCE, at startup */
for (int i = 0; i < 5; i++) {
int die = rand() % 6 + 1; /* 1..6 (tiny bias, fine here) */
printf("roll %d: %d\n", i + 1, die);
}
printf("RAND_MAX here = %d\n", RAND_MAX);
return 0;
}$ gcc dice.c -o dice && ./dice roll 1: 4 roll 2: 1 roll 3: 6 roll 4: 3 roll 5: 6 RAND_MAX here = 2147483647 # run it again — different rolls, because time() changed
Modulo bias: rand() % 6 is not perfectly uniform unless RAND_MAX+1 divides evenly by 6 — the low remainders come up slightly more often. For dice games, nobody cares. For simulations, reject-and-retry or scale via division; for anything security-related, don't use rand() at all — it's trivially predictable.
🧠 Checkpoint: A program calls rand() without ever calling srand. What happens?
- Compile error
- Truly random numbers from the OS
- The exact same sequence on every run, as if srand(1) was called
- rand returns only 0
Show answer
The exact same sequence on every run, as if srand(1) was called — The standard says an unseeded generator behaves like srand(1) — fully deterministic. Great for reproducible tests, embarrassing for a poker game. Seeding with time(NULL) varies the sequence per second.
qsort & bsearch: generic algorithms via function pointers
Remember function pointers from Part 2? Here's their killer app. qsort can sort an array of anything — it just needs the element size and a comparator you supply. The comparator receives const void * pointers to two elements and returns negative / zero / positive, exactly like strcmp:
#include <stdio.h>
#include <stdlib.h>
int cmp_int(const void *a, const void *b) {
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y); /* -1, 0 or +1 — overflow-proof */
}
int main(void) {
int v[] = { 42, 7, 99, -3, 15, 7 };
size_t n = sizeof v / sizeof v[0];
qsort(v, n, sizeof v[0], cmp_int);
for (size_t i = 0; i < n; i++) printf("%d ", v[i]);
printf("\n");
int key = 15;
int *hit = bsearch(&key, v, n, sizeof v[0], cmp_int);
if (hit) printf("found %d at index %td\n", *hit, hit - v);
return 0;
}$ gcc qsort.c -o qsort && ./qsort -3 7 7 15 42 99 found 15 at index 3
🤔 Many tutorials write the int comparator as return x - y;. Why is that subtly broken?
Think first
If x = INT_MAX and y = -1, then x - y overflows a signed int — undefined behavior (Part 3 flashbacks!). Even when it "works", the wrapped result has the wrong sign, so qsort mis-sorts. (x > y) - (x < y) costs two comparisons, can never overflow, and yields exactly −1, 0, or +1.
bsearch uses the same comparator to binary-search a sorted array — O(log n) lookups for free, as shown above.
Leaving the building: exit, atexit, abort
| function | what it does |
|---|---|
exit(status) | normal termination from anywhere: flushes stdio, runs atexit handlers, returns status to the OS |
atexit(fn) | registers fn to run at normal exit (up to 32, called in reverse order) |
abort() | abnormal termination: raises SIGABRT, no flushing, no handlers — this is what a failed assert calls |
EXIT_SUCCESS / EXIT_FAILURE | portable status codes for exit / return from main |
#include <stdio.h>
#include <stdlib.h>
void bye(void) { printf("2. handlers run in reverse\n"); }
void cleanup(void) { printf("1. cleanup ran\n"); }
int main(void) {
atexit(bye); /* registered first, runs LAST */
atexit(cleanup);
printf("0. main is done\n");
if (getenv("DEBUG"))
printf(" (DEBUG is set to: %s)\n", getenv("DEBUG"));
exit(EXIT_SUCCESS); /* same as return 0 from main */
}$ gcc atexit.c -o atexit && ./atexit 0. main is done 1. cleanup ran 2. handlers run in reverse $ DEBUG=yes ./atexit 0. main is done (DEBUG is set to: yes) 1. cleanup ran 2. handlers run in reverse
The rest of the drawer
| function | one-liner |
|---|---|
getenv("HOME") | read an environment variable (NULL if unset — don't modify the returned string) |
system("ls -l") | run a shell command — avoid it: slow, non-portable, and building the command from user input is a textbook shell-injection hole |
abs / labs / llabs | integer absolute value (int / long / long long) |
div / ldiv | quotient and remainder in one struct: div(7,2) → {.quot=3, .rem=1} |
strtod / strtof | string → double/float, same endptr protocol as strtol |
🧠 Checkpoint: Which termination path does not run functions registered with atexit?
return 0;from mainexit(1)deep inside a helperabort()- Falling off the end of main
Show answer
abort() — abort() is the emergency exit: it raises SIGABRT immediately — no atexit handlers, no stdio flushing. Everything else (return from main, exit anywhere) takes the orderly path.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
Next: the header that handles C's most famously fiddly data type — string.h and the art of the null-terminated string.