article thumbnail

C Crash Course

The Language Everything Else Is Built On

21 min read
#programming, #c, #friday1

Nearly every language we've covered stands on someone's shoulders — the shoulders are almost always C's. The operating system you're reading this on — Linux, Windows, macOS — has a kernel written mostly in C. The Python interpreter is a C program. So is the PHP that renders this newsletter, the Ruby that runs your Rails app, the SQLite embedded in your phone, and the firmware in your car's brake controller. When people say a language is "fast," they usually mean "written in C, or fast enough to feel like it was."

C is not fashionable. It has no garbage collector, no package manager everyone agrees on, and no safety net under your feet. What it has is transparency: C is a thin, honest layer over what the machine actually does. Learn C and pointers stop being scary — you understand memory. Learn C and "the stack" stops being a metaphor. Learn C and you understand what the computer is actually doing. That's why it's still worth a Friday.

A Brief History

In 1972, Dennis Ritchie at Bell Labs needed a language to rewrite Unix. Assembly was too tied to one machine; the existing "B" language — created by Ken Thompson (of Unix fame) — was too limited. So Ritchie built C — powerful enough to write an operating system, portable enough to move that operating system to new hardware. Unix and C grew up together, which is why so much of C feels like it was designed for systems programming: it was.

In 1978, Ritchie and Brian Kernighan published The C Programming Language — "K&R" — one of the most influential programming books ever written. It was slim, dense, and set the tone for how C would be taught for decades. As C spread, dialects diverged, so in 1989 the American National Standards Institute standardized it: ANSI C, ratified by ISO in 1990 as C90. That standardization is a big reason C code from the 1990s still compiles today.

Milestone Year What happened
C created 1972 Dennis Ritchie, Bell Labs, to write Unix
K&R book 1978 The C Programming Language, 1st edition
ANSI C (C89) 1989 First official standard
ISO C90 1990 ANSI C adopted internationally

Hello, World

The tradition of printing "Hello, world" as your first program comes from the K&R book. Here it is:

#include <stdio.h>

int main(void) {
    printf("Hello, world\n");
    return 0;
}

Four small things are doing a lot of work:

Compiling

C is a compiled language: you translate source code into a native executable before running it. No interpreter sits between you and the CPU. The two dominant compilers are GCC (the GNU Compiler Collection) and Clang (the LLVM project's compiler).

gcc hello.c -o hello
./hello
# Hello, world

That one command hides a four-stage pipeline:

  1. Preprocess — handle #include, #define, and other # directives, producing pure C source.
  2. Compile — translate that C into assembly for your target CPU.
  3. Assemble — turn assembly into machine-code object files (.o).
  4. Link — stitch your object files together with the C standard library and any others into a single executable.

You can watch the stages if you like: gcc -E stops after preprocessing, gcc -S after compilation, gcc -c after assembly. Most of the time you just let gcc run the whole way through.

The Type System

C's types map closely onto how the hardware stores numbers. There's no "big integer that grows forever" — an int is a fixed box of bits, and if you overflow it, it wraps. This is what "close to the metal" means.

Type Typical size Holds
char 1 byte A single character / small integer
int 4 bytes Whole numbers (~±2.1 billion)
short 2 bytes Smaller whole numbers
long 4 or 8 bytes Larger whole numbers
long long 8 bytes Even larger whole numbers
float 4 bytes Single-precision decimals
double 8 bytes Double-precision decimals
unsigned int 4 bytes Non-negative whole numbers only

The word "typical" matters: the C standard only guarantees minimum sizes and relationships (a long is at least as big as an int), not exact byte counts. When you need a guaranteed width, C99 added <stdint.h> with types like int32_t and uint8_t. You can always ask the compiler with sizeof(int), which yields the size in bytes on your platform.

Pointers: The Heart of C

Here's the section that makes or breaks a C article. Take a breath — pointers are simpler than their reputation.

A variable lives somewhere in memory, and that somewhere has an address — like a house number on a street. A pointer is just a variable that stores an address instead of a value. Two operators connect the two worlds:

#include <stdio.h>

int main(void) {
    int x = 42;
    int *p = &x;   // p holds the address of x

    printf("x  = %d\n", x);    // 42
    printf("&x = %p\n", (void*)&x); // some address, e.g. 0x7ffd...
    printf("p  = %p\n", (void*)p);  // the SAME address
    printf("*p = %d\n", *p);   // 42 — follow p back to x

    *p = 100;                  // change x THROUGH the pointer
    printf("x  = %d\n", x);    // 100
    return 0;
}

Why does this matter? Three reasons that show up constantly:

  1. Functions can change their caller's data. C passes arguments by value — a function gets a copy. If you want a function to modify your variable, you hand it the address and let it reach back through the pointer.
  2. No copying huge things. Passing a pointer to a giant struct copies one address (8 bytes), not the whole struct.
  3. Dynamic memory. Everything you allocate at runtime from the heap comes back to you as a pointer.

Once & ("where is it") and * ("what's there") click, the rest of C opens up. Arrays, strings, structs, and memory allocation are all pointers wearing different hats.

Arrays and Strings

An array is a contiguous block of elements of the same type. Its name, in most contexts, decays into a pointer to its first element — so arrays and pointers are deeply intertwined.

int nums[5] = {10, 20, 30, 40, 50};
printf("%d\n", nums[2]);  // 30

C has no built-in string type. A string is just an array of char ending in a null terminator — the byte '\0' (value 0) that marks where the text stops.

char name[] = "Dennis";
// stored as: 'D' 'e' 'n' 'n' 'i' 's' '\0'  -> 7 bytes for 6 letters

The footguns come free with the power. Nothing stops you from reading or writing past the end of an array — C won't check. Forget the null terminator (or overwrite it) and functions like printf("%s", ...) or strlen will march off the end of your buffer looking for a '\0' that isn't there, reading whatever garbage lives beyond. Half of C's security history is variations on this theme.

Structs and Functions

A struct groups related fields into one compound type — C's way of saying "these belong together."

struct Point {
    int x;
    int y;
};

struct Point p = {3, 4};
printf("%d, %d\n", p.x, p.y);  // 3, 4

Functions in C are pass-by-value: the function receives a copy of each argument. That's fine for a small int, but it has consequences for structs.

// Pass by value: gets a COPY, changes don't stick
double distance(struct Point a, struct Point b) { ... }

// Pass by pointer: can read a big struct cheaply AND modify the original
void move(struct Point *p, int dx, int dy) {
    p->x += dx;   // "->" dereferences a pointer and grabs a field
    p->y += dy;   // equivalent to (*p).x
}

The -> operator is the everyday tool for reaching into a struct through a pointer. When you see it, read it as "follow the pointer, then take this field."

Managing Memory

C gives you two places to put data, and understanding the difference is essential.

The stack is automatic. Local variables live here; they're created when a function is called and destroyed when it returns. Fast, tidy, and you never think about cleanup — but the memory is gone the moment the function exits, and the stack is relatively small.

The heap is manual. You ask for memory with malloc, you get a pointer, and that memory stays yours until you hand it back with free. The heap is large and its lifetime is under your control — which is exactly why it's dangerous.

#include <stdlib.h>

int *arr = malloc(100 * sizeof(int));  // request room for 100 ints
if (arr == NULL) {                      // malloc can fail — always check
    return 1;
}
arr[0] = 42;
free(arr);      // give it back when done
arr = NULL;     // avoid a dangling pointer

This is C's great trade. There's no garbage collector deciding when to reclaim memory, so there's no unpredictable pause and no runtime overhead — you get precise, deterministic control. The bill for that control is that you must free everything exactly once. Free too little and you get a memory leak. Free something then use it and you get a use-after-free. Free it twice and you corrupt the heap. Entire tools (see below) exist just to catch these.

A Real Example

Here's a small, complete program that ties the pieces together — a struct, a function, a loop, and pointers doing real work. It tracks a few readings and reports the average and the warmest.

#include <stdio.h>

struct Reading {
    const char *city;
    double temp;
};

double average(struct Reading *r, int n) {
    double sum = 0.0;
    for (int i = 0; i < n; i++) {
        sum += r[i].temp;
    }
    return sum / n;
}

int main(void) {
    struct Reading readings[] = {
        {"Salt Lake City", 31.5},
        {"Phoenix",        44.2},
        {"Portland",       27.8}
    };
    int count = sizeof(readings) / sizeof(readings[0]);

    struct Reading *hottest = &readings[0];
    for (int i = 1; i < count; i++) {
        if (readings[i].temp > hottest->temp) {
            hottest = &readings[i];
        }
    }

    printf("Average temp: %.1f C\n", average(readings, count));
    printf("Hottest: %s at %.1f C\n", hottest->city, hottest->temp);
    return 0;
}

Notice the array-size trick sizeof(readings) / sizeof(readings[0]) — total bytes divided by the size of one element gives the count. And hottest is a pointer that "remembers" which reading is currently the warmest, updated as the loop scans.

The Standard Library

C's standard library is small by modern standards but covers the essentials. You pull in what you need with #include.

Header Provides
stdio.h Input/output: printf, scanf, fopen, fgets
stdlib.h Memory (malloc, free), conversions (atoi), rand, exit
string.h String handling: strlen, strcpy, strcmp, memcpy
math.h Math: sqrt, pow, sin, floor (often link with -lm)
ctype.h Character tests: isdigit, toupper
time.h Time and dates: time, clock

Common Pitfalls

C trusts you completely, and that trust is regularly misplaced. The classics:

The unifying theme is undefined behavior: when you break the rules, C doesn't promise a crash. It might work today, corrupt data tomorrow, and open a security hole next year. Discipline and tooling are your defense.

The C Standards

C is a living standard, though it evolves slowly and conservatively — a feature, given how much depends on it.

Standard Year Notable addition
C89 / C90 1989/90 The first standard; function prototypes
C99 1999 // comments, long long, <stdint.h>, declare-anywhere variables
C11 2011 Threads, atomics, _Static_assert, anonymous structs
C17 / C18 2017/18 Bug-fix release; no major new features
C23 2023/24 bool/true/false keywords, nullptr, #embed, binary literals

Tooling

You don't write C alone — a well-worn toolchain watches your back.

# Compile with warnings and the address sanitizer while developing
gcc -Wall -Wextra -g -fsanitize=address program.c -o program
./program

Turning on warnings and a sanitizer during development is the single highest-leverage habit a C programmer can build.

Why It Still Matters

Look under the hood of modern computing and it's C all the way down. The Linux kernel and huge parts of the Windows kernel are C. SQLite — quite possibly the most widely deployed database on Earth, sitting in your phone, browser, and countless apps — is C. The reference implementations of Python, PHP, Ruby, and Lua are C programs. The world's embedded devices — medical monitors, engine controllers, routers, spacecraft — run C because it fits in tiny memory and maps directly to hardware.

C's one great weakness is the flip side of its power: manual memory management is a perennial source of security vulnerabilities. That pain is precisely what modern languages are responding to. Rust offers C-level performance with a borrow checker that makes use-after-free a compile error. Go brought garbage collection and built-in concurrency to systems-adjacent work. Zig aims to be "a better C" — same transparency, fewer footguns. None of them exist without C to react against, and none of them have replaced it. They've joined it.

The Wrap-Up

C rewards you differently than most languages. It won't hold your hand, autocomplete your architecture, or forgive your mistakes — but it will show you the machine as it really is. After a few weeks of C, pointers stop being magic, memory stops being abstract, and the performance behavior of every other language you use suddenly makes sense. You'll understand why an array access is fast and a hash lookup less so, why copying a big object is expensive, why "the stack overflowed" is a literal sentence.

It's fifty-plus years old and it isn't going anywhere. Spend a weekend with gcc, a text editor, and the little programs above. Write something that leaks memory, then find the leak with valgrind. Break an array bound on purpose and watch the address sanitizer catch you. That's the fastest way to genuinely understand what your computer is doing — and it's a skill that quietly pays off in every language you'll ever write. Happy hacking.

Enjoyed this article? Share it with someone who'd love it too.

Most covered topics