Introduction to C: The Language Underneath Everything
Why C
Dennis Ritchie created C in 1972 at Bell Labs to rewrite Unix. Before C, operating systems were written in assembly — thousands of lines of instructions tied to a specific processor. Ritchie wanted a language that was close enough to the hardware to write an operating system, but abstract enough to be portable across machines. He succeeded. Unix was rewritten in C, and both language and operating system began to spread.
Fifty years later, C is the foundation of modern computing. The Linux kernel is C. The Windows kernel is C. macOS is C. The Python interpreter is C. The JVM is C and C++. The TCP/IP stack that connects every device on the internet is C. Your phone, your car’s engine controller, the Mars rovers, the MRI machine — all C.
“C is not a big language, and it is not well served by a big book.”
Why C Is Still Relevant
Every operating system, every database engine (PostgreSQL, SQLite, MySQL), every language runtime (CPython, Ruby MRI, PHP), every embedded system, and most network infrastructure is written in C. Learning C means learning how the machine actually works: how memory is laid out, how pointers work, why buffer overflows are dangerous, and what your higher-level language is doing under the hood.
I. Hello, World
#include <stdio.h> is a preprocessor directive: it literally pastes the contents of the standard I/O header file into your source code before compilation. main is the entry point. It returns int: 0 means success, nonzero means failure. printf writes formatted text to standard output. \n is a newline. The semicolons and braces come from C, and every language that borrowed C’s syntax (Java, JavaScript, Go, Rust, C++, C#) inherited them.
The Compilation Pipeline
Understanding these stages matters. When you get a “undefined reference” error, that is a linker error (stage 4). When you get a “undeclared identifier” error, that is a compiler error (stage 2). Knowing which stage failed tells you where to look.
II. Types and Variables
C’s type sizes are not fully specified. An int is “at least 16 bits.” On modern 64-bit systems it is 32 bits, but the standard does not guarantee this. For portable code, use the fixed-width types from <stdint.h>:
III. Operators and Control Flow
IV. Functions
V. Pointers
Pointers are what makes C C. A pointer is a variable that holds the memory address of another variable. That is all it is. But from this single concept flows almost everything that distinguishes C from higher-level languages.
Function Pointers
Function pointers are how C does callbacks, polymorphism, and plugin architectures. The standard library’s qsort and bsearch use function pointers to work with any type. This is C’s version of generics — crude but effective.
VI. Arrays and Strings
C Strings
C has no string type. A “string” is an array of char terminated by a null byte ('\0'). This is the source of a disproportionate number of bugs and security vulnerabilities.
Buffer Overflows
C does not check array bounds. Writing past the end of a buffer overwrites whatever is next in memory. This is the root cause of a vast number of security vulnerabilities: stack smashing, code injection, remote code execution. Always use bounded functions (strncpy, snprintf, fgets) and always check sizes. The gets() function was so dangerous it was removed from the C standard entirely.
VII. Structs
VIII. Dynamic Memory
The stack is for local variables with known sizes. The heap is for data whose size is determined at runtime. You manage the heap manually: allocate with malloc, release with free. If you forget to free, memory leaks. If you free twice, undefined behavior. If you use after free, undefined behavior.
| Function | Purpose |
|---|---|
malloc(size) | Allocate size bytes (uninitialized) |
calloc(n, size) | Allocate n * size bytes (zero-initialized) |
realloc(ptr, size) | Resize existing allocation |
free(ptr) | Release allocated memory |
Every malloc must have a corresponding free. Every malloc must be checked for NULL. This is the discipline C requires. There is no garbage collector, no RAII, no defer. You track every allocation yourself. Get it wrong and you get memory leaks, crashes, or security vulnerabilities. Get it right and you have software that runs for decades without consuming more memory than it needs.
IX. The Preprocessor
The preprocessor runs before the compiler. It does pure text substitution: #include pastes files, #define replaces tokens, #ifdef conditionally includes code. Macros are powerful but dangerous: they have no type checking, no scoping, and can produce surprising results. Prefer const variables and inline functions when possible.
X. File I/O
| Mode | Description |
|---|---|
| "r" | Read (file must exist) |
| "w" | Write (creates or truncates) |
| "a" | Append (creates if needed) |
| "r+" | Read and write (file must exist) |
| "rb", "wb" | Binary mode (no newline translation) |
XI. The Compilation Model
Real C projects split code across multiple files. Each .c file compiles independently into an object file (.o). The linker combines them.
XII. Undefined Behavior
Undefined behavior (UB) is the most important concept in C that most programmers do not understand well enough. When the C standard says a program has undefined behavior, it means anything can happen. The program may crash. It may silently produce wrong output. The compiler may remove your code entirely.
The compiler assumes UB never happens and optimizes accordingly. If signed overflow is UB, the compiler can assume x + 1 > x is always true and optimize out your overflow check. This is not a bug in the compiler — it is the compiler following the rules. Use -fsanitize=undefined and -fsanitize=address to catch UB at runtime.
XIII. The Standard Library
| Header | What It Provides |
|---|---|
| <stdio.h> | printf, scanf, fopen, fclose, fgets, fread, fwrite, sprintf, snprintf |
| <stdlib.h> | malloc, calloc, realloc, free, exit, atoi, qsort, bsearch, rand |
| <string.h> | strlen, strcpy, strncpy, strcmp, strcat, strstr, memcpy, memset |
| <math.h> | sqrt, pow, sin, cos, log, exp, ceil, floor, fabs (link with -lm) |
| <ctype.h> | isalpha, isdigit, isspace, toupper, tolower |
| <assert.h> | assert macro (abort if false; disabled by NDEBUG) |
| <stdint.h> | int8_t, uint32_t, int64_t, SIZE_MAX, INT32_MAX |
| <stdbool.h> | bool, true, false |
| <errno.h> | errno, ENOENT, ENOMEM — error codes for system calls |
| <limits.h> | INT_MAX, INT_MIN, CHAR_BIT — implementation limits |
| <time.h> | time, clock, difftime, strftime |
| <signal.h> | signal, raise, SIGINT, SIGSEGV — signal handling |
The standard library is small and minimal. Unlike Python or Go, C does not ship with an HTTP server or a JSON parser. The philosophy is: give you I/O, memory management, strings, math, and sorting. Everything else you build yourself or use a third-party library. This minimalism is why C runs on everything from spacecraft to toasters.
XIV. Putting It Together
A complete linked list implementation demonstrating structs, pointers, dynamic memory, and error handling.
Notice the patterns: every malloc is checked, every allocation is freed in list_destroy, strings are bounded with strncpy, const marks read-only parameters, and the double-pointer trick in list_delete eliminates the need to special-case deleting the head node. This is idiomatic C: explicit, careful, and hiding nothing.
Summary
C gives you everything and protects you from nothing. There is no bounds checking, no garbage collection, no type safety beyond what the compiler checks at compile time. When your program has a bug, the consequence is not an exception with a stack trace — it is a segfault, corrupted memory, or a security vulnerability.
That is the bargain. In exchange you get:
- Performance. Native machine code, no runtime, no GC pauses. As close to the hardware as you can get without assembly.
- Portability. C compilers exist for every architecture on earth: x86, ARM, RISC-V, microcontrollers with 2 KB of RAM, supercomputers with terabytes.
- Longevity. Code written in the 1980s still compiles and runs. The language changes slowly. The ABI is stable. Your investment holds for decades.
- Universality. Every operating system, every language runtime, every serious library exposes a C API. If you know C, you can interface with anything.
Learn C not because it is easy, but because everything else is built on it. When you understand C, you understand what malloc actually does, why buffer overflows are dangerous, how a virtual function table works, why your Python program uses so much memory, and what the operating system does when you open a file. You understand the machine.