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.
- 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
.cfile compiles independently into a.oobject 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:
- Preprocessor
- A text-substitution pass that runs before real compilation — it expands
#includefiles inline, replaces#definemacros, and resolves#ifdefconditional 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 (likeprintf) 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
printfand friends so the compiler knows they exist. int main(void)- Every C program has exactly one
mainfunction — it's where execution starts.intmeans 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
printffunction ("print formatted") with one argument, a string literal.\ninside 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 value0back to the operating system. By long-standing convention,0means "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:
| Header | What it provides |
|---|---|
stdio.h | Input/output — printf, scanf, file operations. |
stdlib.h | General utilities — memory allocation, random numbers, program exit. |
string.h | String/memory manipulation — strlen, strcpy, memcpy. |
math.h | Floating-point math — sqrt, pow, sin/cos. |
ctype.h | Character tests/conversions — isdigit, toupper. |
Real-world applications
- The Linux kernel — tens of millions of lines of C, still the reference example of what C was originally built to do.
- SQLite — the world's most widely deployed database engine, written entirely in C specifically for its small footprint and predictable performance.
- Every microcontroller firmware in this notebook's Embedded domain — from a blinking LED to a full automotive ECU.
- Other languages' own runtimes — CPython (the reference Python implementation) and Ruby's MRI interpreter are both written in C.
Gotchas
- C is case-sensitive —
Mainandmainare two completely different, unrelated names to the compiler. - A missing semicolon usually produces a compiler error pointing at the next line, not the one actually missing it — if an error looks like it's in perfectly fine code, check the line just above it first.
- C has no dedicated boolean type in its original design (C99 added
_Bool/<stdbool.h>) — historically,0means false and literally anything else means true, a convention that still shows up throughout the language. - Unlike Python or JavaScript, there's no safety net for most mistakes — an out-of-bounds array access or a wrong pointer doesn't raise a friendly exception, it's undefined behavior: the program might crash, might silently corrupt unrelated data, or might appear to work by pure luck. This single fact is why the rest of this domain, and all of Embedded C, exists.