The C Path — learn C, visually

📚 The Standard Library

stdlib.h: conversions, random, qsort & exits

⏱ 14 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

Dice 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:

strtol.c
#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;
}
terminal
$ 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:

dice.c
#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;
}
terminal
$ 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:

qsort.c
#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;
}
terminal
$ 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

functionwhat 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_FAILUREportable status codes for exit / return from main
atexit.c
#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 */
}
terminal
$ 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

functionone-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 / llabsinteger absolute value (int / long / long long)
div / ldivquotient and remainder in one struct: div(7,2){.quot=3, .rem=1}
strtod / strtofstring → double/float, same endptr protocol as strtol

🧠 Checkpoint: Which termination path does not run functions registered with atexit?

  • return 0; from main
  • exit(1) deep inside a helper
  • abort()
  • 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.

▶ Practice this lesson interactively (with live gcc)