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.
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 |
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:
#include <stdio.h> — a preprocessor directive. Before compilation, it pastes in the declarations for the standard I/O library so the compiler knows what printf is.int main(void) — the entry point. Every C program starts at main. It returns an int (the exit code) and here takes no arguments.printf("Hello, world\n") — prints text to standard output. The \n is a newline escape.return 0; — tells the operating system the program finished successfully. Non-zero conventionally means an error.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:
#include, #define, and other # directives, producing pure C source..o).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.
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.
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:
&x means "the address of x" (where does x live?).*p means "the value at the address p holds" (go to that house and look inside). This is called dereferencing.#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:
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.
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.
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."
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.
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.
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 |
C trusts you completely, and that trust is regularly misplaced. The classics:
<= n instead of < n, or forgetting the extra byte a string needs for its null terminator.free — memory leaks that slowly starve a long-running program.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.
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 |
You don't write C alone — a well-worn toolchain watches your back.
make.-fsanitize=address (or undefined) and the compiler injects runtime checks that catch memory errors the instant they happen.-Wall -Wextra — turn on the compiler's warnings. Many bugs are caught for free before you ever run the program.# 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.
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.
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.