Why C & Thinking in C

C was created by Dennis Ritchie at Bell Labs in 1972 while building UNIX. Half a century later it is still the lingua franca of systems programming: operating system kernels, embedded firmware, language runtimes, and the ABI every other language must talk to. This page looks at C with a critical eye — what it does brilliantly, where it genuinely hurts, and how two very different descendants, Zig and Oberon, answer the same questions differently.

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.

Lineage diagram of C, Zig, and Oberon language families

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.

ConcernCZig
NullabilityAny pointer can be NULL, silentlyOptional types (?*T) make absence explicit
Error handlingReturn codes and errno, no disciplineError unions — failures are typed values you must handle
Memory safetyAll undefined behavior; no checksBounds and overflow checks on by default; opt-out per scope
Allocationmalloc/free, allocator implied globallyExplicit allocator parameter; arena and GPA provided
MetaprogrammingPreprocessor macros, textual and fragilecomptime — real code executed while compiling
Build systemMake, CMake, Meson — external toolsBuilt-in build.zig, cross-compiles out of the box
Learning curveSmall core, but every footgun must be learnedSimilar 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.

ConcernCOberon-07
Designer philosophyTrust the programmer; minimal safetyWirth's Minimalism: safety by construction
Pointer arithmeticCore feature (p++, p+n)Forbidden — pointers only point to typed records
Memory reclamationManual free, dangling pointersGarbage collector — no use-after-free, no leaks by hand
Bounds checkingNone; UBCompile- or runtime-checked array/string bounds
ModulesHeaders + preprocessor macrosReal module system with explicit imports
MetaprogrammingTextual macrosNone needed — the language stays tiny
RuntimeNearly zero, no GCGC 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:

  1. Assume the worst. Every input is hostile, every buffer can overflow, every pointer can be NULL. Check before you trust.
  2. 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.
  3. Treat UB as poison. If the compiler may assume something about your program, never rely on what "usually happens" — test with -fsanitize=address,undefined.
  4. Prefer the safe idiom. snprintf over sprintf, strncpy with explicit termination, fgets over gets (never — gets was removed from the standard for good reason).
  5. Compile loudly. -Wall -Wextra -Wpedantic -Werror turns 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.