Why C & Thinking in C
Origins and Purpose
C was not designed in a committee meeting. It grew out of the need to rewrite UNIX in a language that was almost as fast as assembly but portable enough to move between machines. That origin explains the entire personality of the language: everything is explicit, everything is close to the metal, and nothing is hidden.
From B to the Universal Lingua Franca
Ritchie's B was an untyped language with a single word-length data type — inconvenient for systems work. C added typed variables, structures, and a pointer model that maps one-to-one onto the hardware. Because C has no runtime and no garbage collector, it compiles to a thin, predictable layer over the machine. That is why its call convention became the de facto standard: when Python, Rust, or Zig talks to a C library, they are speaking C's language.
Where C Lives Today
You find C wherever performance, control, and stability matter more than convenience:
- Operating system kernels — Linux, the BSDs, Windows kernel components.
- Embedded and real-time firmware — microcontrollers, drivers, bootloaders.
- Language runtimes and virtual machines — CPython, Lua, Node's V8 core.
- Databases and infrastructure — SQLite, Redis, nginx, curl.
- The FFI boundary — every other language exports and imports C-compatible interfaces.
Knowing C means you can read the code that runs the world and you can reason about what any higher-level language is really doing underneath.
What C Does Brilliantly
An honest introduction starts with the strengths, because they explain why C outlived every language invented after it:
- Predictable performance. The cost of every construct is visible: an array index is an address computation, a function call is a jump with a frame push. There is no allocator hiding in a
+operator. - Tiny, portable runtime. A freestanding C program needs almost nothing under it — the same code crosses microcontrollers, phones, and supercomputers.
- A stable ABI. The C calling convention and layout rules are the contract the whole software ecosystem builds on. Code written decades ago still links today.
- Universality. C compilers exist for every platform that has ever run software. Your C skills transfer everywhere, and C is the common ground between teams and languages.
- Honesty. Nothing is hidden. If you understand your C program, you understand what the machine does — no interpreter magic, no hidden allocation, no implicit copy semantics.
The Lineage of C and Its Rivals
The diagram below maps the two great families of systems languages. C descends from B and ALGOL's statement model; Oberon descends from Wirth's Pascal family with a radically different safety philosophy; Zig, Rust, and Go are the modern attempts to keep C's power while removing its footguns.
Figure 1 — two philosophies diverge from the same roots: explicit control (C family) versus constructive safety (Wirth family).
Where C Hurts — The Critical Eye
C gives you total control, and total control means you can make total mistakes. These are not hypothetical: buffer overflows and use-after-free bugs are the majority of security vulnerabilities in real systems. C's dangers come from a small set of design decisions that were reasonable in 1972 and expensive today.
Undefined Behavior Is Everywhere
The C standard defines some programs as undefined behavior (UB): integer overflow, signed overflow, out-of-bounds array access, use of a freed pointer, uninitialized reads. The compiler is allowed to assume UB never happens and can optimize aggressively on that assumption — so a bug can rewrite your program's meaning instead of politely failing. Every other language on this page was designed, in part, to shrink this category.
Memory Safety Is Manual
You allocate with malloc and you must remember to free exactly once at the right time. Forget it — leak. Do it twice — crash. Keep the pointer after freeing — use-after-free, exploitable by attackers. Do this in a large codebase with many contributors and no compiler assistance, and errors become inevitable. This is why the industry has moved toward Rust, Zig, and Go for new systems code.
Weak Abstraction and Tooling Gaps
C versus Zig — The Practical Successor
Zig (Andrew Kelley, 2016) was designed as a better C: same level of control, same C interop, but with hidden behavior removed and safety checks added. Zig keeps C's explicitness where it is cheap and fixes C's dangers where they are expensive. The two languages speak the same ABI, so Zig can call C directly and C can call Zig.
| Concern | C | Zig |
|---|---|---|
| Nullability | Any pointer can be NULL, silently | Optional types (?*T) make absence explicit |
| Error handling | Return codes and errno, no discipline | Error unions — failures are typed values you must handle |
| Memory safety | All undefined behavior; no checks | Bounds and overflow checks on by default; opt-out per scope |
| Allocation | malloc/free, allocator implied globally | Explicit allocator parameter; arena and GPA provided |
| Metaprogramming | Preprocessor macros, textual and fragile | comptime — real code executed while compiling |
| Build system | Make, CMake, Meson — external tools | Built-in build.zig, cross-compiles out of the box |
| Learning curve | Small core, but every footgun must be learned | Similar core, far fewer footguns |
C versus Oberon — The Other Philosophy
Oberon (Niklaus Wirth and Jürg Gutknecht, 1987) comes from the Pascal tradition and answers the same systems question in the opposite way. Where C trusts the programmer and punishes mistakes, Oberon constructs safety into the language: no pointer arithmetic, no malloc/free pair to get wrong (a garbage collector reclaims memory), arrays are bounds-checked, and a module system gives real encapsulation. Oberon-07 is so small it fits in your head — the entire language report is a few pages, and Wirth's whole Oberon system (OS + compiler in Oberon) was designed to be understood by one person.
| Concern | C | Oberon-07 |
|---|---|---|
| Designer philosophy | Trust the programmer; minimal safety | Wirth's Minimalism: safety by construction |
| Pointer arithmetic | Core feature (p++, p+n) | Forbidden — pointers only point to typed records |
| Memory reclamation | Manual free, dangling pointers | Garbage collector — no use-after-free, no leaks by hand |
| Bounds checking | None; UB | Compile- or runtime-checked array/string bounds |
| Modules | Headers + preprocessor macros | Real module system with explicit imports |
| Metaprogramming | Textual macros | None needed — the language stays tiny |
| Runtime | Nearly zero, no GC | GC and module loader; larger but still small |
Neither philosophy is wrong: C maximizes control and ubiquity; Oberon maximizes correctness and clarity. Zig sits between them — C's control, plus structured safety. Understanding the triangle helps you choose the right tool and appreciate why C remains so hard to replace.
Thinking in C — Five Habits
If you adopt these habits now, the rest of this roadmap (and every C codebase you touch) will be dramatically safer:
- Assume the worst. Every input is hostile, every buffer can overflow, every pointer can be NULL. Check before you trust.
- Know who owns memory. For every allocation ask: who frees it, when, and what happens if a copy escapes? Write the free next to the malloc.
- Treat UB as poison. If the compiler may assume something about your program, never rely on what "usually happens" — test with
-fsanitize=address,undefined. - Prefer the safe idiom.
snprintfoversprintf,strncpywith explicit termination,fgetsovergets(never —getswas removed from the standard for good reason). - Compile loudly.
-Wall -Wextra -Wpedantic -Werrorturns warnings into errors; warnings are how C tells you it noticed the trap.
Next, set up your toolchain and compile your first program with these habits already in place.