The Standard Library Toolbox
C's standard library is small by modern standards — about thirty headers, no containers, no networking, no string builder. Knowing precisely what is in it, and what is deliberately not, saves you from reinventing what exists and from expecting what never will.
- Use
<math.h>correctly, including linking it and handling its error cases. - Work with calendar time and measure elapsed time with the right clock.
- Generate pseudo-random numbers without the modulo bias, and say when not to use them.
- Name what the standard library does not provide and where to look instead.
- Read a manual page and the text of the standard to answer a question definitively.
1The shape of the library
| Header | Provides | Met in |
|---|---|---|
<stdio.h> | Formatted and file I/O | Weeks 3, 8, 26 |
<stdlib.h> | Allocation, conversion, exit, sorting, random | Weeks 19, 23 |
<string.h> | String and block memory operations | Week 14 |
<math.h> | Floating-point mathematics | This week |
<time.h> | Calendar time and clocks | This week |
<ctype.h> | Character classification | Week 14 |
<errno.h> | Error reporting | Weeks 23, 27 |
<limits.h> <float.h> | Type ranges | Week 5 |
<stdint.h> | Fixed-width types | Week 37 |
<assert.h> | Runtime assertions | Week 27 |
<stdarg.h> | Variable argument lists | Week 30 |
<threads.h> <stdatomic.h> | Concurrency | Week 41 |
That is essentially all of it. Anything else — a hash table, a socket, a regular expression, a JSON parser — comes from the operating system, from a third-party library, or from you.
2<math.h>
#include <math.h>
sqrt(x) pow(x, y) fabs(x) fmod(x, y)
sin(x) cos(x) tan(x) atan2(y, x)
exp(x) log(x) log2(x) log10(x)
floor(x) ceil(x) round(x) trunc(x)
fmin(a,b) fmax(a,b) hypot(a,b)All take and return double. Variants suffixed f and l work on float and long double.
Linking
gcc -std=c17 -Wall -Wextra prog.c -o prog -lmOn Linux the mathematics functions live in a separate library and the linker must be told. Omit -lm and you get undefined reference to 'sqrt' — week 2's distinction between declaration and definition, in its most common practical form. Note that -lm goes after the source files; linkers process arguments in order.
Error handling
errno = 0;
double r = sqrt(-1.0);
if (errno == EDOM) {
/* domain error: the input was outside the function's domain */
}
if (isnan(r)) { … }Mathematics functions report failure two ways: by setting errno to EDOM (bad input) or ERANGE (result out of range), and by returning NaN or infinity. Week 7's isnan and isinf are usually the more convenient test.
pow is not for small integer powers. pow(x, 2) is a general-purpose call using logarithms; x * x is one instruction and exact. Worse, pow can return 8.999999999 for pow(2, 3) on some implementations, so (int)pow(2, 3) may be 8 — truncation of a value just below 9. Use multiplication for small powers, and lround if you must convert.
3<time.h>
Two distinct jobs, easily confused: what time is it and how long did that take.
Calendar time
time_t now = time(NULL); /* seconds since the epoch */
struct tm *local = localtime(&now); /* broken down, local zone */
struct tm *utc = gmtime(&now); /* broken down, UTC */
char buffer[64];
strftime(buffer, sizeof buffer, "%Y-%m-%d %H:%M:%S", local);struct tm has a trap of its own: tm_year counts from 1900 and tm_mon counts from 0. Printing tm->tm_year directly gives 125 for 2025.
localtime and gmtime return a pointer to a static buffer, which the next call overwrites — week 17's static-duration design, with week 41's reentrancy consequences. Two calls in one printf give the same result twice. POSIX provides localtime_r, which writes into a buffer you supply.
Measuring elapsed time
clock_t start = clock();
do_work();
double cpu_seconds = (double)(clock() - start) / CLOCKS_PER_SEC;clock measures processor time consumed by your program, not wall-clock time. A program that sleeps for ten seconds uses almost no CPU time. For wall-clock duration use time for second resolution, or the POSIX clock_gettime(CLOCK_MONOTONIC, …) for nanoseconds.
| Measuring | Use |
|---|---|
| CPU consumed | clock() |
| Wall-clock, coarse | time(), difftime() |
| Wall-clock, precise | clock_gettime(CLOCK_MONOTONIC, …) — POSIX |
| A date to display | localtime + strftime |
CLOCK_MONOTONIC rather than the wall clock is the right choice for intervals, because the wall clock can jump backwards when the system synchronizes time. Week 34 uses it for benchmarking.
4Pseudo-random numbers
#include <stdlib.h>
#include <time.h>
srand((unsigned)time(NULL)); /* seed once, at start */
int r = rand(); /* 0 .. RAND_MAX */Three things to know, in order of how often they cause trouble.
Seed once. Calling srand inside a loop reseeds the generator, and since time has one-second resolution, every call in the same second produces the identical sequence. The classic symptom is a "random" list where every value is the same.
rand() % n is biased. If RAND_MAX + 1 is not a multiple of n, the lower values occur slightly more often. For a die roll with a large RAND_MAX the bias is negligible; for a large n it is not. The unbiased method rejects the uneven tail:
int uniform(int n) /* 0 .. n-1, no bias */
{
int limit = RAND_MAX - (RAND_MAX % n);
int value;
do {
value = rand();
} while (value >= limit);
return value % n;
}It is not secure. rand is a simple deterministic generator; given a few outputs, the rest are predictable. Never use it for passwords, tokens, keys, or anything an adversary would benefit from guessing. Use the operating system's source — getrandom on Linux, arc4random_buf on the BSDs and macOS. Week 48 revisits this under security review.
For reproducible tests, seed with a fixed constant. A test that fails only sometimes is worse than no test.
5What is not in the library — and how to find out
| Not provided | Where to get it |
|---|---|
| Growable arrays, lists, hash tables | Write them — weeks 19, 32, 33 |
| Sockets, threads before C11, processes | POSIX — weeks 39, 40 |
| Regular expressions | POSIX <regex.h>, or PCRE |
| JSON, XML, compression, TLS | Third-party libraries |
| A safe string builder | snprintf, or write one |
| Unicode beyond wide characters | ICU, or handle UTF-8 yourself — week 37 |
| Testing framework | Third-party — week 35 |
Reading the documentation
Three sources, in increasing order of authority.
Manual pages tell you what your system does:
man 3 strtol # section 3 is library functions
man 3 printf
man -k random # search by keywordRead the RETURN VALUE and ERRORS sections first — they answer most questions, and they are the ones people skip.
cppreference distinguishes what each standard version guarantees, which matters when you target C17 but read about a C23 feature.
The standard itself is the final authority. Drafts are freely available from the WG14 site linked in the course resources; N3220 is the current C23 working draft. It is dense but surprisingly navigable: section 7 is the library, and each function has a short Description, Returns, and sometimes a Recommended practice.
When to reach for the standard. When a question is about what is guaranteed rather than what happens to work. "Is char signed?" — the standard says implementation-defined. "Does free(NULL) work?" — the standard says yes, explicitly. Testing your own compiler answers neither question, because it tells you about one implementation. Weeks 36 and 37 depend heavily on this distinction.
6Worked example: generate, measure, report
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#define SAMPLES 100000
#define BUCKETS 10
/* Unbiased 0..n-1, by rejecting the uneven tail of the range. */
static int uniform(int n)
{
int limit = RAND_MAX - (RAND_MAX % n);
int value;
do {
value = rand();
} while (value >= limit);
return value % n;
}
static double mean_of(const double *v, size_t n)
{
double total = 0.0;
for (size_t i = 0; i < n; i++) {
total += v[i];
}
return total / (double)n;
}
static double stddev_of(const double *v, size_t n, double mean)
{
double sum_sq = 0.0;
for (size_t i = 0; i < n; i++) {
double d = v[i] - mean;
sum_sq += d * d;
}
return sqrt(sum_sq / (double)n);
}
int main(void)
{
/* --- calendar time --- */
puts("== what time is it ==");
time_t now = time(NULL);
struct tm local = *localtime(&now); /* copy: the buffer is static */
struct tm utc = *gmtime(&now);
char stamp[64];
strftime(stamp, sizeof stamp, "%Y-%m-%d %H:%M:%S", &local);
printf(" local : %s\n", stamp);
strftime(stamp, sizeof stamp, "%Y-%m-%d %H:%M:%S UTC", &utc);
printf(" utc : %s\n", stamp);
printf(" epoch : %lld seconds\n", (long long)now);
printf(" note tm_year is %d (years since 1900) and tm_mon is %d (0-based)\n",
local.tm_year, local.tm_mon);
/* --- random numbers --- */
puts("\n== pseudo-random numbers ==");
srand((unsigned)now); /* seeded ONCE */
printf(" RAND_MAX = %d\n", RAND_MAX);
int buckets[BUCKETS] = { 0 };
double *values = malloc(SAMPLES * sizeof *values);
if (values == NULL) {
return EXIT_FAILURE;
}
clock_t t0 = clock();
for (size_t i = 0; i < SAMPLES; i++) {
int b = uniform(BUCKETS);
buckets[b]++;
values[i] = (double)rand() / RAND_MAX; /* 0.0 .. 1.0 */
}
double gen_cpu = (double)(clock() - t0) / CLOCKS_PER_SEC;
puts(" distribution across 10 buckets (expect about 10000 each):");
for (int b = 0; b < BUCKETS; b++) {
printf(" %d: %6d ", b, buckets[b]);
for (int bar = 0; bar < buckets[b] / 400; bar++) putchar('#');
putchar('\n');
}
/* --- statistics, using math.h --- */
puts("\n== statistics ==");
double m = mean_of(values, SAMPLES);
double s = stddev_of(values, SAMPLES, m);
printf(" samples : %d\n", SAMPLES);
printf(" mean : %.6f (expect 0.5)\n", m);
printf(" std deviation : %.6f (expect 0.2887)\n", s);
printf(" theoretical sd : %.6f\n", 1.0 / sqrt(12.0));
/* --- measuring time properly --- */
puts("\n== cpu time versus wall clock ==");
printf(" generating %d samples took %.4f CPU seconds\n",
SAMPLES, gen_cpu);
t0 = clock();
time_t wall_start = time(NULL);
double waste = 0.0;
for (long i = 0; i < 20000000L; i++) {
waste += sqrt((double)i);
}
double busy_cpu = (double)(clock() - t0) / CLOCKS_PER_SEC;
double busy_wall = difftime(time(NULL), wall_start);
printf(" busy loop: %.4f CPU seconds, %.0f wall seconds (sum %.0f)\n",
busy_cpu, busy_wall, waste);
puts(" for a CPU-bound loop these agree; for a sleeping or blocked");
puts(" program clock() would report almost nothing");
/* --- math.h traps --- */
puts("\n== math.h details ==");
printf(" sqrt(2) = %.10f\n", sqrt(2.0));
printf(" pow(2, 10) = %g\n", pow(2.0, 10.0));
printf(" 2*2*2 by hand = %d (prefer this for small powers)\n", 2*2*2);
printf(" fmod(7.5, 2) = %g (%% does not work on doubles)\n",
fmod(7.5, 2.0));
printf(" round(2.5) = %g, trunc(2.5) = %g, floor(-2.5) = %g\n",
round(2.5), trunc(2.5), floor(-2.5));
errno = 0;
double bad = sqrt(-1.0);
printf(" sqrt(-1) = %f, isnan -> %d, errno==EDOM -> %d\n",
bad, isnan(bad), errno == EDOM);
free(values);
return EXIT_SUCCESS;
}Add #include <errno.h> for the EDOM check, then:
gcc -std=c17 -Wall -Wextra -g -o toolbox toolbox.c -lm
./toolboxThree experiments
Forget -lm. Build without it:
gcc -std=c17 -Wall -Wextra -o toolbox toolbox.c/usr/bin/ld: /tmp/cc8Xk2.o: undefined reference to `sqrt'
collect2: error: ld returned 1 exit statusThe header declared it; the linker could not find the body. Exactly week 2's four-phase model, and the most common form this error takes in practice.
Reseed inside the loop. Move srand((unsigned)time(NULL)); into the sample loop and rerun. Every bucket but one drops to zero: within a single second, time returns the same value, so the generator restarts identically every iteration.
Compare biased and unbiased. Replace uniform(BUCKETS) with rand() % BUCKETS. With RAND_MAX at 2 147 483 647 and ten buckets the bias is far below the noise, so the histograms look identical — which is the honest result. Now try BUCKETS set to a large value close to RAND_MAX and the skew becomes visible. Knowing when a correction matters is as useful as knowing the correction.
7Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
Forgetting -lm | undefined reference to 'sqrt' | Add it after the source files. |
srand inside a loop | The same value repeatedly | Seed once at program start. |
rand() for tokens or keys | Predictable output; a security hole | Use the operating system's random source. |
(int)pow(2, 3) | May be 8 through rounding | Multiply for small powers, or lround. |
Keeping the pointer from localtime | Overwritten by the next call | Copy the struct tm, or use localtime_r. |
Printing tm_year directly | 125 instead of 2025 | Add 1900; add 1 to tm_mon. |
clock() to measure a sleeping program | Reports almost zero | clock_gettime(CLOCK_MONOTONIC, …). |
| Assuming the library has containers | Wasted search | It does not. Write them, or take a dependency. |
8Check yourself
Why does #include <math.h> not remove the need for -lm?
Because a header supplies declarations, not definitions. The compiler learns the signature of sqrt and type-checks the call; the function's machine code lives in the maths library, which the linker only searches when told. This is the same declaration-versus-definition split that week 2 traced through the four translation phases.
Why does reseeding inside a loop produce identical values?
srand resets the generator to a starting state determined entirely by the seed. Seeding with time(NULL), which changes once per second, restarts the same sequence on every iteration within that second — so each call to rand returns the first value of that sequence. Seed exactly once.
What is wrong with rand() % 6 for a die roll, and how much does it matter?
If RAND_MAX + 1 is not divisible by 6, the low faces occur slightly more often, because the surplus values at the top of the range map onto them. With RAND_MAX above two billion the bias is far below measurement noise for six outcomes; it becomes significant only when the range is a substantial fraction of RAND_MAX. The unbiased fix is to reject the uneven tail before taking the remainder.
When does clock() give a misleading answer?
Whenever the program is not doing work — sleeping, waiting on input, blocked on a network read. clock measures processor time consumed, so a program that waits ten seconds reports near zero. For elapsed wall-clock time use time/difftime for coarse resolution or clock_gettime(CLOCK_MONOTONIC, …) for precision.
When should you consult the standard rather than test your compiler?
Whenever the question is what is guaranteed rather than what happens to work here. Whether char is signed, whether free(NULL) is safe, whether signed overflow wraps — testing one implementation answers none of these, because a different compiler or optimization level may behave differently. Weeks 36 and 37 are built entirely on that distinction.
9Where this leads
Week 25 drops to the level below the library: the bitwise operators, masks, and bit-fields that let you manipulate individual bits. That is the foundation for the flag handling in week 31, the checksums of week 37, and the hardware registers of week 50 — and it is where week 4's binary representation finally becomes something you operate on directly.