C

Introduction to C

C is the language most of this notebook eventually sits on top of — the syntax firmware gets written in, the language C++ extends, and the reason "systems programming" means what it means. This chapter assumes nothing: what C actually is, how a plain text file becomes a running program, and your very first working program, explained one line at a time.

Quick reference
What it is
A compiled, statically-typed, low-level general-purpose programming language, created in 1972, that gives direct control over memory with almost no runtime overhead.
Mental model
A precise, structured way of writing instructions that get translated once, ahead of time, into the exact machine code the CPU runs — no interpreter and no virtual machine sitting between your code and the hardware.
Key numbers
Each .c file compiles independently into a .o object file; a working "Hello, World" is about 6 lines; C99/C11/C17/C23 are the modern standard revisions in active use.
Reach for it when
You need direct hardware control, a predictable and minimal memory/performance footprint (an OS kernel, a microcontroller, a database engine), or you need to interoperate with existing code — almost everything else is ultimately built on top of C.

Why C still matters

C was created in 1972 by Dennis Ritchie at Bell Labs, originally to rewrite the Unix operating system in something more portable than raw assembly. That single design goal — a language low-level enough to replace assembly, but structured enough to actually reason about — is still exactly why C hasn't gone away. Every operating system kernel you've ever used (Linux, Windows, macOS, the one running inside your router) is written substantially in C. Most other programming languages' own runtimes — Python, Ruby, the core of Node.js — are themselves implemented in C. And every microcontroller in the Embedded domain of this notebook is, in practice, programmed in C, because nothing else gets as close to the hardware with as little overhead.

C is old, in other words, for the same reason a well-designed hand tool is old — nothing since has needed to replace what it's actually for.

The compilation pipeline

Unlike a language like Python, where a program is read and executed line-by-line by an interpreter every time it runs, a C program is translated once, ahead of time, all the way down to the exact machine instructions your CPU understands. That translation happens in four distinct stages:

A .c source file passes through the preprocessor, compiler, and assembler to become an object file, which the linker then combines with library code into a final executable hello.c source text Preprocessor expands #include, #define, macros Compiler source → assembly Assembler assembly → hello.o hello executable The Linker (not pictured as a box — it's the last arrow) combines hello.o with library code (like printf's implementation) into the final executable
Four stages, always in this order — a mismatch anywhere in this chain (a missing header, an undefined symbol) fails at a different, specific stage, which is the first clue for diagnosing it.
Preprocessor
A text-substitution pass that runs before real compilation — it expands #include files inline, replaces #define macros, and resolves #ifdef conditional blocks. It has no understanding of C syntax at all; it's closer to find-and-replace.
Compiler
Translates the preprocessed C source into assembly language for your target CPU — this is the stage that actually understands C's grammar, types, and rules, and where most syntax errors are caught.
Assembler
Translates human-readable assembly into an object file (.o) — actual machine code, but not yet a runnable program, since it may reference functions (like printf) that live elsewhere.
Linker
Combines one or more object files with any library code they reference into a single, runnable executable — the stage that resolves "where is the actual code for printf."

This same pipeline, described in far more depth for a bare-metal target, is exactly what Embedded C's Compilation & Linking chapter covers — this section is the plain-language version worth understanding first.

Your first program, line by line

#include <stdio.h>

int main(void) {
    printf("Hello, World!\n");
    return 0;
}
#include <stdio.h>
A preprocessor directive — before real compilation starts, this line is replaced by the entire contents of the "standard I/O" header, which declares printf and friends so the compiler knows they exist.
int main(void)
Every C program has exactly one main function — it's where execution starts. int means it returns a whole number to the operating system; (void) means it takes no arguments.
{ ... }
Curly braces mark the start and end of a block — here, the body of main. Every opening brace needs exactly one matching closing brace.
printf("Hello, World!\n");
Calls the printf function ("print formatted") with one argument, a string literal. \n inside the string is an escape sequence for a newline — it isn't two characters, it's C's way of writing "one line-break character" in source text.
return 0;
Ends main, handing the value 0 back to the operating system. By long-standing convention, 0 means "the program finished successfully"; any nonzero value signals an error.
;
Every statement in C ends with a semicolon — it's how the compiler knows where one instruction stops and the next begins, since C otherwise ignores whitespace and line breaks.

Compiling and running it yourself

Save the code above as hello.c, then, with GCC or Clang installed, run this from a terminal in the same folder:

gcc hello.c -o hello
./hello
# Hello, World!

The first command runs the entire pipeline above in one step and produces an executable named hello; the second line actually runs it. This is the exact loop you'll repeat for every example in this domain: edit, compile, run, read the output, repeat.

The standard library, at a glance

C's language core is deliberately tiny — almost everything useful comes from the standard library, a set of headers every conforming compiler ships. You'll meet all of these properly in later chapters; this is just so the names stop looking unfamiliar:

HeaderWhat it provides
stdio.hInput/output — printf, scanf, file operations.
stdlib.hGeneral utilities — memory allocation, random numbers, program exit.
string.hString/memory manipulation — strlen, strcpy, memcpy.
math.hFloating-point math — sqrt, pow, sin/cos.
ctype.hCharacter tests/conversions — isdigit, toupper.

Real-world applications

Gotchas

Field notes