Part I — Foundations: the C way
The language itself, in order: the toolchain, then every piece a C program is made of — types, control flow, functions, arrays and pointers, strings, structs, and the preprocessor. Three checkpoint sections (pointers, strings, structs/macros) let you confirm you're solid before moving on instead of finding out three sections later. Two terminal mini-projects close out the Part before Part II adds dynamic memory on top.
1. What C is, why it still matters, and which C you're learning
1.1 The problem C solved, and still solves
C was designed by Dennis Ritchie at Bell Labs (1969–1973) to rewrite Unix in something more portable than assembly. Its entire design philosophy is one sentence: give the programmer direct, predictable control over memory and machine instructions, and trust them with it. Nothing is inserted behind your back — no garbage collector, no hidden bounds checks, no implicit allocations, no exceptions unwinding a stack you didn't ask to unwind. Every cost is visible in the source.
That's why, 50+ years later, C is still common near the bottom of the stack: Linux and many embedded kernels; important parts of Windows; language runtimes such as CPython, the JVM, V8, and Go's early runtime; SQLite; Redis; curl; OpenSSL; git's core; PostgreSQL. If a program needs to run close to the hardware, on a tiny device, or as a reference implementation other languages bind against, C is often one of the first languages considered.
1.2 What C deliberately does not have
No classes, no exceptions, no garbage collector, no built-in strings
(a "string" is a convention: a pointer to char plus a '\0'
terminator), no bounds checking on arrays, no function overloading (until
_Generic in C11, which is a macro trick, not real overloading), no
namespaces. This is not primitiveness — it's the reason a C ABI is the
lingua franca every other language calls into (Python's ctypes, Rust's
extern "C", Go's cgo, Java's JNI all speak C calling conventions,
because nothing simpler exists to agree on).
1.3 Which C you're learning: standards and what changed
| Standard | Year | What it added that you'll actually use |
|---|---|---|
| C89/C90 (ANSI C) | 1989 | The baseline everyone assumes: function prototypes, void*, const |
| C99 | 1999 | // comments, stdint.h fixed-width types, variable-length arrays (VLAs), designated initializers, for (int i = ...), inline, _Bool, compound literals |
| C11 | 2011 | <stdatomic.h>, <threads.h>, _Generic, _Static_assert, anonymous structs/unions, _Noreturn |
| C17 | 2018 | Bugfix release, no new features — the safest "modern C" target today |
| C23 | 2024 | nullptr, bool/true/false as keywords (no <stdbool.h> needed), constexpr, typeof/typeof_unqual, #embed, [[attributes]] ([[nodiscard]], [[maybe_unused]], [[deprecated]]), binary literals (0b1010), digit separators (1'000'000), _BitInt(N), removal of implicit-int and K&R-style function definitions |
Which one should you target? C17 for anything that must compile on
older distros (RHEL, embedded toolchains) or interop with a wide range of
compilers. C23 when you control the toolchain — it removes several classes
of footgun (nullptr vs. NULL's type ambiguity, bool finally being a
real keyword) with zero downside on GCC 13+/Clang 17+. This guide defaults
to C17 syntax and calls out C23 alternatives inline.
# Check what your compiler actually supports
clang --version
gcc --version
clang -std=c23 -dM -E -x c /dev/null | grep __STDC_VERSION__
# 202311L == C23. 201710L == C17. 201112L == C11.
1.4 War story
CVE-2014-0160 — Heartbleed — was a missing bounds check in OpenSSL's
TLS heartbeat extension: the code read payload_length bytes from a
buffer using a length supplied by the attacker, with no check that the
buffer actually contained that many bytes. One memcpy with an
attacker-controlled length leaked private keys, session cookies, and
passwords from millions of servers for two years before anyone noticed.
The fix was three lines. The lesson that shaped an entire industry's
approach to C (Part V of this guide) was: in C, the compiler will never
save you from reading past a buffer — only you, your tests, your
sanitizers, and your code review can.