Not every practical C topic fits neatly into "macros" or "pointers" - this
guide is the catch-all for the things every real command-line program
ends up needing: parsing argc/argv properly, reading environment
variables, choosing sensible exit codes, cleaning up on shutdown, timing
and randomness, and the build/debug tooling around all of it. These are
the topics that turn a program that "runs" into one that behaves like a
real, well-mannered piece of software.
0. Why this stuff is needed, why industry treats it as non-optional, and why your syllabus never got here
Why it's needed at all
A homework assignment usually just runs, reads a fixed input, and prints a result. A real program has to be launched - with arguments, flags, an environment, a working directory - and has to end in a way other software can react to (an exit code), and often has to survive being interrupted, respond to timing, or generate believable randomness. None of this comes up in a single self-contained "compute the answer" program, which is exactly why a course built around solving individual problems rarely reaches it.
Why real projects treat it as non-optional
- Every CLI tool is launched with arguments and flags.
argc/argvparsing (sections 1–4) isn't an advanced topic in practice - it's the very first thing almost every real C program does. - Exit codes are a contract with the rest of the system. Shell
scripts, CI pipelines, and other programs all branch on whether your
program exited
0(success) or non-zero (failure) - getting this wrong silently breaks automation built around your tool. - Signals are how the OS and other processes talk to a running
program. Ctrl+C,
kill, and graceful shutdown requests all arrive as signals - a server or long-running tool that doesn't handle them can leave files corrupted or resources leaked on exit. - Timing and randomness show up constantly - logging timestamps,
measuring performance, generating IDs, simple simulations and games -
and each has real, well-known pitfalls (modulo bias in
rand(), reseeding accidentally, confusing wall-clock time with CPU time). - Real projects are multi-file, and are built with warnings enabled on
purpose.
-Wall -Wextracatches entire classes of the bugs from every other guide in this series before they run - treating compiler warnings as free, automated review is standard practice.
Why your syllabus (probably) never got here
- A typical assignment is compiled and run exactly one way, by the
student or the grader, with no flags, no piping, no interruption - so
there's never a forcing function to teach
argvparsing beyondargv[1], let alone signals or exit codes. - These topics are "the glue between the program and the OS," which is inherently platform-flavored (POSIX vs. Windows) - a portable "intro to C" course has a legitimate reason to avoid teaching it in depth, even though almost all real C development happens on one specific platform where these tools are simply expected knowledge.
- Build tooling (Makefiles, compiler flags) is often treated as
"environment setup," not "the language" - even though enabling
-Wall -Wextrais arguably one of the highest-leverage things you can do to catch real bugs, it's rarely taught as part of the curriculum itself.
This is the guide of things nobody assigned you to learn, but that every working C programmer just knows - because at some point their program had to actually ship, get piped into another tool, get interrupted mid-run, or get built alongside twenty other files.
1. argc and argv - the real basics
Every C program can receive command-line arguments through main's
parameters: argc (argument count) and argv (an array of C strings).
argv[0] is always the program's own invocation name - the real
arguments start at index 1.
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Program name (argv[0]): %s\n", argv[0]);
printf("Number of REAL arguments: %d\n", argc - 1);
for (int i = 1; i < argc; i++) {
printf(" argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
Running ./myprogram hello world gives argc == 3, with
argv[1] == "hello" and argv[2] == "world" - argv[argc] is always
guaranteed to be NULL, a useful sentinel if you ever iterate argv
without relying on argc directly.
2. Manual flag and option parsing
For a handful of simple flags, many real tools just loop over argv
manually with strcmp - no library needed for something this small.
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
int main(int argc, char *argv[]) {
bool verbose = false;
const char *outputFile = NULL;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
verbose = true;
} else if (strcmp(argv[i], "-o") == 0) {
if (i + 1 >= argc) { // always check there's actually a NEXT argument
fprintf(stderr, "Error: -o requires a filename\n");
return 1;
}
outputFile = argv[++i]; // consume the NEXT argument as this flag's value
} else if (argv[i][0] == '-') {
fprintf(stderr, "Unknown option: %s\n", argv[i]);
return 1;
} else {
printf("Positional argument: %s\n", argv[i]);
}
}
printf("verbose = %d, outputFile = %s\n", verbose, outputFile ? outputFile : "(none)");
return 0;
}
This scales fine up to maybe half a dozen flags; beyond that, real
projects reach for getopt (section 3) instead, since manual parsing
gets error-prone fast (missing value checks, ambiguous combined flags,
inconsistent error messages).
3. getopt - the standard POSIX way to parse short options
getopt (from <unistd.h>, POSIX - not standard C, but nearly universal
on Unix-like systems) handles single-character flags, flags with
required values, and produces consistent error messages automatically.
#include <stdio.h>
#include <unistd.h> // POSIX
int main(int argc, char *argv[]) {
int opt;
int verbose = 0;
const char *outputFile = NULL;
// "vo:" means: -v takes no value, -o REQUIRES a value (the trailing ':')
while ((opt = getopt(argc, argv, "vo:")) != -1) {
switch (opt) {
case 'v':
verbose = 1;
break;
case 'o':
outputFile = optarg; // getopt sets this automatically for "o:" options
break;
case '?': // getopt itself reports unknown options / missing values
fprintf(stderr, "Usage: %s [-v] [-o file] [args...]\n", argv[0]);
return 1;
default:
break;
}
}
printf("verbose = %d, outputFile = %s\n", verbose, outputFile ? outputFile : "(none)");
// optind is the index of the first NON-option argument, set by getopt
for (int i = optind; i < argc; i++) {
printf("positional: %s\n", argv[i]);
}
return 0;
}
getopt also transparently handles combined short flags (-vo file is
equivalent to -v -o file), which manual parsing would need extra code
to support correctly.
4. getopt_long - long-form --option flags
getopt_long (a GNU extension, widely available on Linux, also on macOS
and via common libraries elsewhere) adds support for --verbose-style
long options alongside the short ones, exactly matching what most modern
CLI tools support.
#include <stdio.h>
#include <getopt.h> // GNU extension
int main(int argc, char *argv[]) {
int verbose = 0;
const char *outputFile = NULL;
static struct option longOptions[] = {
{"verbose", no_argument, 0, 'v'},
{"output", required_argument, 0, 'o'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0} // terminator -- required
};
int opt;
while ((opt = getopt_long(argc, argv, "vo:h", longOptions, NULL)) != -1) {
switch (opt) {
case 'v': verbose = 1; break;
case 'o': outputFile = optarg; break;
case 'h':
printf("Usage: %s [-v|--verbose] [-o|--output file]\n", argv[0]);
return 0;
case '?':
return 1; // getopt_long already printed an error message
}
}
printf("verbose = %d, outputFile = %s\n", verbose, outputFile ? outputFile : "(none)");
return 0;
}
This is exactly the pattern behind real tools' --help, --verbose, and
--output=file style flags - getopt_long even supports the
--output=file (single-token, equals-sign) form automatically.
5. Environment variables: getenv, setenv, and envp
A program can read (and, on POSIX systems, set) environment variables - key/value pairs inherited from the shell or parent process, commonly used for configuration that shouldn't be hardcoded or passed as a visible command-line argument.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
const char *home = getenv("HOME"); // returns NULL if the variable isn't set
if (home != NULL) {
printf("HOME = %s\n", home);
} else {
printf("HOME is not set\n");
}
const char *apiKey = getenv("API_KEY");
if (apiKey == NULL) {
fprintf(stderr, "Error: API_KEY environment variable is required\n");
return 1;
}
printf("Using API key: %s\n", apiKey);
// setenv is POSIX (not standard C) -- sets/overwrites a variable for
// THIS process and any children it spawns, not the parent shell
setenv("MY_VAR", "hello", 1); // 1 = overwrite if it already exists
printf("MY_VAR = %s\n", getenv("MY_VAR"));
return 0;
}
A lesser-known third parameter to main - int main(int argc, char *argv[], char *envp[]) - gives direct array access to every environment
variable at once, but getenv is almost always preferred since it's
simpler and doesn't depend on this non-standard (though widely supported)
third parameter.
6. Exit codes: the contract with the rest of the system
A program's return value from main (or the argument to exit())
becomes its exit status - by convention, 0 means success and any
non-zero value means some kind of failure, and other programs/scripts
routinely branch on this.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1; // non-zero: signals failure to the calling shell/script
}
FILE *fp = fopen(argv[1], "r");
if (fp == NULL) {
perror("fopen");
return 2; // a DIFFERENT non-zero code -- some tools use distinct
// codes to distinguish different failure REASONS
}
fclose(fp);
return EXIT_SUCCESS; // == 0, from <stdlib.h> -- more descriptive than a bare 0
// EXIT_FAILURE (also from <stdlib.h>) == 1 on virtually every real platform
}
In a shell, $? (or %errorlevel% on Windows) holds the last program's
exit code - this is exactly what shell scripts and CI pipelines check
with if ./myprogram; then ... to decide whether to proceed or abort.
7. atexit() - registering cleanup to run automatically
atexit() registers a function to run automatically when the program
exits normally (via return from main or a call to exit()) - useful
for guaranteed cleanup regardless of where in the program execution
finally ends.
#include <stdio.h>
#include <stdlib.h>
void cleanup1(void) {
printf("cleanup1 running\n");
}
void cleanup2(void) {
printf("cleanup2 running\n");
}
int main(void) {
atexit(cleanup1);
atexit(cleanup2);
printf("main doing work...\n");
return 0; // cleanup2 runs FIRST, then cleanup1 -- registered functions
// run in REVERSE order of registration, like a stack
}
atexit-registered functions do not run if the program terminates
abnormally (a crash, or abort()) - for cleanup that must happen even in
those cases, you generally need OS-level mechanisms (signal handlers,
section 10) instead.
8. assert() and NDEBUG
assert(condition) (from <assert.h>) checks a condition during
development and aborts with a clear message if it's false - a fast way to
catch "this should never happen" bugs early, that's automatically
compiled out entirely in release builds.
#include <stdio.h>
#include <assert.h>
int divide(int a, int b) {
assert(b != 0); // documents AND enforces a precondition during development
return a / b;
}
int main(void) {
printf("%d\n", divide(10, 2)); // 5, fine
// printf("%d\n", divide(10, 0)); // aborts immediately with a message like:
// "Assertion failed: b != 0, file ..., line ..."
// rather than silently doing undefined division
return 0;
}
Defining the macro NDEBUG (typically via a compiler flag, e.g. gcc -DNDEBUG ...) makes every assert() in the program compile to nothing - the
standard reason release/production builds disable asserts (for
performance), while debug builds keep them enabled to catch bugs early.
Because of this, never put code with real side effects inside an
assert - assert(doImportantWork()) silently stops doing that work
entirely in an NDEBUG build.
9. errno - a quick real-world reference
errno (covered in depth in the standard I/O and file handling guides)
is worth a quick standalone reminder here since it comes up constantly
across the whole standard library, not just file functions.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(void) {
errno = 0; // reset first -- errno isn't automatically cleared before a call
long value = strtol("not a number", NULL, 10);
if (errno != 0) {
printf("strtol reported an error: %s\n", strerror(errno));
}
FILE *fp = fopen("/nonexistent/path.txt", "r");
if (fp == NULL) {
printf("fopen failed: %s (errno %d)\n", strerror(errno), errno);
}
return 0;
}
The pattern is always the same across the standard library: reset
errno to 0 right before a call whose failure you care about
distinguishing, make the call, then check errno immediately afterward
(before any other call has a chance to overwrite it).
10. Signal handling - responding to Ctrl+C and shutdown requests
Signals are how the OS (or another process, via kill) asynchronously
notifies a running program of events like an interrupt request (Ctrl+C
sends SIGINT) or a termination request. signal() lets you register a
handler instead of accepting the default behavior (which is usually
"terminate immediately").
#include <stdio.h>
#include <signal.h>
#include <stdbool.h>
#include <unistd.h>
// volatile sig_atomic_t is the standard, safe type for a flag shared
// between a signal handler and the main program flow
volatile sig_atomic_t shutdownRequested = 0;
void handleSigint(int sig) {
(void)sig; // unused parameter -- required by signal()'s handler signature
shutdownRequested = 1; // ONLY do minimal, signal-safe work inside a handler --
// no printf, no malloc, nothing that isn't guaranteed
// async-signal-safe
}
int main(void) {
signal(SIGINT, handleSigint); // register our handler for Ctrl+C
printf("Running... press Ctrl+C to request a graceful shutdown.\n");
int iterations = 0;
while (!shutdownRequested) {
printf("working... (%d)\n", iterations++);
sleep(1);
if (iterations > 100) break; // safety limit for this example
}
printf("Shutdown requested -- cleaning up gracefully.\n");
return 0;
}
The volatile sig_atomic_t flag pattern (set a flag in the handler,
check it in the main loop) is the standard, safe way to respond to
signals - doing real work (printing, allocating memory, file I/O)
directly inside a signal handler is unsafe, since a signal can arrive
in the middle of another, unrelated operation.
11. Time and date: time(), difftime(), strftime()
<time.h> provides wall-clock time (for timestamps and dates) - distinct
from clock() (section 11 continued below), which measures CPU time
instead and is meant for performance measurement, not real-world dates.
#include <stdio.h>
#include <time.h>
int main(void) {
time_t now = time(NULL); // current time, as seconds since the Unix epoch
struct tm *localTime = localtime(&now); // break it into year/month/day/etc.
char buffer[64];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", localTime);
printf("Current time: %s\n", buffer);
time_t later = now + 3600; // one hour later, in raw seconds
double secondsElapsed = difftime(later, now);
printf("Difference: %.0f seconds\n", secondsElapsed);
return 0;
}
For measuring how long code takes to run (performance timing, not
wall-clock dates), use clock() instead, which measures CPU time
consumed by the program:
#include <stdio.h>
#include <time.h>
int main(void) {
clock_t start = clock();
long sum = 0;
for (long i = 0; i < 100000000; i++) sum += i; // some work to time
clock_t end = clock();
double elapsedSeconds = (double)(end - start) / CLOCKS_PER_SEC;
printf("sum = %ld, took %f seconds of CPU time\n", sum, elapsedSeconds);
return 0;
}
time() and clock() measure genuinely different things - time() is
real-world wall-clock time (affected by system clock changes, useful for
timestamps); clock() is CPU time consumed (unaffected by the process
being paused/descheduled, useful for benchmarking).
12. Random numbers: rand(), srand(), and avoiding modulo bias
rand() generates pseudo-random numbers - but without seeding it with
srand() first, it produces the exact same sequence every single run,
and the common rand() % n pattern has a subtle statistical bias worth
knowing about.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void) {
srand((unsigned int)time(NULL)); // seed ONCE, typically with the current time,
// so each run produces a different sequence --
// NEVER call srand() repeatedly in a loop
int diceRoll = rand() % 6 + 1; // simple, common, but has a SLIGHT statistical bias:
// if RAND_MAX+1 isn't evenly divisible by 6, low
// values become very slightly more likely than high ones
printf("dice roll: %d\n", diceRoll);
// A more statistically correct approach for ranges where the bias
// actually matters (e.g. cryptography-adjacent code, though rand()
// itself is NEVER appropriate for real cryptographic randomness):
int range = 6;
int limit = RAND_MAX - (RAND_MAX % range);
int r;
do {
r = rand();
} while (r >= limit); // reject values that would introduce bias
int fairDiceRoll = r % range + 1;
printf("fair dice roll: %d\n", fairDiceRoll);
return 0;
}
For anything security-sensitive - tokens, passwords, cryptographic keys -
rand() is never appropriate, seeded or not; it's not
cryptographically secure. Real security-sensitive code uses a CSPRNG:
platform-specific APIs like /dev/urandom (POSIX) or dedicated
cryptographic libraries, never <stdlib.h>'s rand().
13. qsort and bsearch from <stdlib.h>
qsort (introduced in the functions guide as the canonical callback
example) sorts any array given a comparator; bsearch performs a binary
search on an already-sorted array using the same comparator convention -
together they cover most of what you'd otherwise hand-write a sort/search
algorithm for.
#include <stdio.h>
#include <stdlib.h>
int compareInts(const void *a, const void *b) {
int intA = *(const int *)a;
int intB = *(const int *)b;
return intA - intB;
}
int main(void) {
int numbers[] = {5, 2, 8, 1, 9, 3};
int count = 6;
qsort(numbers, count, sizeof(int), compareInts);
printf("sorted: ");
for (int i = 0; i < count; i++) printf("%d ", numbers[i]);
printf("\n");
int target = 8;
// bsearch REQUIRES the array to already be sorted (as it now is) --
// it returns a pointer to a matching element, or NULL if not found
int *found = bsearch(&target, numbers, count, sizeof(int), compareInts);
if (found != NULL) {
printf("found %d at index %ld\n", *found, found - numbers);
} else {
printf("not found\n");
}
return 0;
}
Both functions take the same "comparator" convention (return negative,
zero, or positive), which is why the same compareInts function works
for both - a pattern worth reusing anywhere else you need consistent
ordering logic in one place.
14. Useful utility macros: MIN/MAX, ARRAY_SIZE
A small set of utility macros shows up in an enormous fraction of real-world C codebases - worth knowing both for use and for recognizing them when reading other people's code.
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#define CLAMP(value, low, high) (MAX((low), MIN((value), (high))))
int main(void) {
printf("%d\n", MAX(3, 7)); // 7
printf("%d\n", MIN(3, 7)); // 3
printf("%d\n", CLAMP(15, 0, 10)); // 10 -- clamped to the upper bound
int numbers[] = {1, 2, 3, 4, 5};
printf("%zu\n", ARRAY_SIZE(numbers)); // 5
// Remember the macro pitfalls from the macros guide: these all
// duplicate their arguments, so MAX(i++, j) would increment i TWICE.
// Prefer 'static inline' functions over these where the language
// allows it (they can't be as generic across types, but avoid the
// duplication problem entirely) -- these macros are still extremely
// common in real C because of their type-generic convenience.
return 0;
}
ARRAY_SIZE only works correctly on a real, in-scope array - exactly
like the raw sizeof(arr) / sizeof(arr[0]) idiom from the pointers guide,
it silently computes the wrong thing if arr has already decayed to a
pointer (e.g. inside a function that received it as a parameter).
15. Compiling multi-file projects, and a minimal Makefile
Real C projects split code across multiple .c/.h files (as covered in
the functions guide, section 19) and are almost never compiled by typing
the full gcc command by hand every time - a Makefile automates it.
/* main.c */
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}
/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
#endif
/* math_utils.c */
#include "math_utils.h"
int add(int a, int b) { return a + b; }
# Makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c11
myprogram: main.o math_utils.o
$(CC) $(CFLAGS) -o myprogram main.o math_utils.o
main.o: main.c math_utils.h
$(CC) $(CFLAGS) -c main.c
math_utils.o: math_utils.c math_utils.h
$(CC) $(CFLAGS) -c math_utils.c
clean:
rm -f *.o myprogram
Running make builds only what's changed since the last build (based on
file timestamps), and make clean removes generated files - this
incremental-rebuild behavior is the whole reason Makefiles (or modern
equivalents like CMake) exist instead of just re-running one big gcc
command every time.
16. Compiler warnings: -Wall -Wextra as free bug-catching
Enabling warning flags costs nothing and catches a surprising fraction of the bugs covered across this entire guide series - an uninitialized variable, a signed/unsigned comparison, a format string mismatch - before the program ever runs.
#include <stdio.h>
int main(void) {
int x; // uninitialized
printf("%d\n", x); // -Wall would warn: "'x' is used uninitialized"
int a = 5;
unsigned int b = 3;
if (a < b) { } // -Wextra would warn: signed/unsigned comparison
printf("%d\n", 3.14); // -Wall would warn: format '%d' expects 'int', got 'double'
return 0;
}
Compiling this with gcc -Wall -Wextra example.c surfaces all three
issues before the program even runs - compare that to compiling with no
flags at all, where every one of these compiles silently and becomes a
runtime bug waiting to happen. Real-world C projects near-universally
build with at least -Wall -Wextra enabled, and many add -Werror to
turn every warning into a hard compile failure, forcing issues to be
fixed before code can even be committed.
17. Debugging tools: a quick map of what's used for what
Beyond printf-debugging (inserting print statements to trace program
state), real-world C development leans on a small set of standard tools -
worth knowing what each one is actually for, even without covering
their full usage here.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof(int) * 5);
p[5] = 100; // BUG: writes one element PAST the end of the allocated array
// (a classic off-by-one buffer overflow)
free(p);
// p[0] = 1; // would ALSO be a bug: use-after-free
return 0;
}
gdb(GNU Debugger) - run a program undergdb, set breakpoints, step line-by-line, and inspect variables live; the standard tool for "why did this crash, and what was the state right before it did."- Valgrind (
valgrind ./myprogram) - runs your program in an instrumented environment and reports memory leaks, use-after-free, buffer overflows (like the one above), and uninitialized-value reads, all without needing to modify or recompile your code. - AddressSanitizer (compile with
gcc -fsanitize=address) - similar goal to Valgrind, but built into the compiler and generally faster; catches the same class of memory bugs, often with clearer error output. - Static analyzers (
clang --analyze,cppcheck, or-fanalyzerwith recent GCC) - find potential bugs by examining the source code without ever running it, catching some issues before you even test the program.
The bug above (p[5] = 100; on a 5-element array) is exactly the kind of
thing that might not crash immediately, or ever, in casual testing - and
exactly the kind of thing Valgrind or AddressSanitizer catches instantly
and precisely, which is why real projects run these tools routinely
rather than relying on bugs to announce themselves.
18. Common pitfalls checklist
Forgetting
argv[0]is the program name, not the first real argument - real arguments start at index 1 (section 1).Not checking that a flag requiring a value actually has a following argument (
-owith nothing after it) - readingargv[i+1]without bounds-checkingi+1 < argcfirst (section 2).Returning
0frommainregardless of what happened - silently breaks any shell script or CI pipeline that checks the exit code to decide success/failure (section 6).Putting code with real side effects inside
assert()- vanishes entirely inNDEBUGrelease builds, silently skipping that work (section 8).Doing unsafe work (printing, allocating memory, file I/O) directly inside a signal handler - signals can interrupt other operations mid-flight; use a
volatile sig_atomic_tflag and handle the real work in the main loop instead (section 10).Confusing
time()(wall-clock time) withclock()(CPU time) for performance measurement - using the wrong one gives misleading benchmark results (section 11).Forgetting to seed
rand()withsrand(), or seeding it more than once per program run - both produce a non-random or repeated sequence (section 12).Using
rand()for anything security-sensitive - it's not cryptographically secure regardless of seeding (section 12).Compiling without
-Wall -Wextra- silently accepts an enormous range of real, catchable bugs that the compiler is fully capable of warning about for free (section 16).
Quick reference
| Task | Function(s) / Tool | Purpose | Section |
|---|---|---|---|
| Command-line arguments | argc, argv[] | Access arguments passed at program launch | 1 |
| Manual flag parsing | strcmp loop over argv | Simple, no-dependency option parsing | 2 |
| POSIX short options | getopt (<unistd.h>) | Standard -v, -o value style parsing | 3 |
| Long options | getopt_long (<getopt.h>) | --verbose, --output=file style parsing | 4 |
| Environment variables | getenv, setenv | Read/set configuration outside the command line | 5 |
| Exit status | return/exit() value, EXIT_SUCCESS/EXIT_FAILURE | Report success/failure to the calling process | 6 |
| Cleanup on exit | atexit(fn) | Register a function to run automatically at normal exit | 7 |
| Development-time checks | assert(cond), NDEBUG | Catch broken invariants early; compiled out in release builds | 8 |
| Error diagnosis | errno, strerror(errno) | Discover why a standard library call failed | 9 |
| Graceful shutdown | signal(SIGINT, handler) | Respond to Ctrl+C / termination requests safely | 10 |
| Wall-clock time | time, localtime, strftime | Timestamps and human-readable dates | 11 |
| CPU timing | clock(), CLOCKS_PER_SEC | Measure how much CPU time code consumed | 11 |
| Pseudo-random numbers | rand, srand | Non-cryptographic randomness | 12 |
| Sorting/searching | qsort, bsearch | Generic sort and binary search via a comparator | 13 |
| Utility macros | MAX, MIN, ARRAY_SIZE, CLAMP | Common small, type-generic helper patterns | 14 |
| Multi-file builds | Makefile, make | Automated, incremental compilation | 15 |
| Bug-catching at compile time | -Wall -Wextra -Werror | Free, automatic detection of common bug classes | 16 |
| Interactive debugging | gdb | Step through a running program, inspect state | 17 |
| Memory bug detection | Valgrind, AddressSanitizer | Find leaks, overflows, use-after-free at runtime | 17 |
| Static bug detection | clang --analyze, cppcheck | Find potential bugs without running the program | 17 |
Coverage note
This guide rounds out the Realworld C series with the practical,
OS-facing glue that real command-line programs depend on: proper
argc/argv and environment handling, exit codes and cleanup, signals,
timing, randomness, and the build/debugging tooling every real project is
built and verified with. Combined with the other guides in this
folder - macros, structs/unions, functions, pointers, file handling, and
standard I/O - this collection covers the practical foundation for
reading, writing, and shipping real-world C code.
The Kid Who Discovered Machine Learning
File Handling in C