Procedural Programming with C · Basic · Week 1

Computers, Programs, and the C Toolchain

Before writing C, it helps to know what a program actually is to a machine, and what the compiler does to your text file to produce one. This week builds that picture and then puts a working toolchain in your hands.

By the end of this week you can
  • Explain what the processor, main memory, and storage each contribute when a program runs.
  • Describe the difference between a compiler, an interpreter, and an assembler, and say where C sits.
  • Install a C compiler and confirm it works from a terminal.
  • Write, compile, and run a first C program from the command line.
  • Read a compiler diagnostic, find the line it refers to, and distinguish an error from a warning.

1How a computer executes a program

A running program is not the file you wrote. The file is text. What runs is a sequence of numeric instructions that a particular processor knows how to carry out. Understanding the three pieces of hardware involved makes almost everything later in this course easier to reason about.

The processor

The central processing unit repeats one simple cycle, billions of times per second: fetch the next instruction from memory, decode what it means, execute it, and move on. Each instruction is tiny — add two numbers, copy a value from one place to another, jump to a different instruction if some condition holds. Nothing more sophisticated than that ever happens. Everything a computer does is built from these steps.

The processor keeps a small number of extremely fast storage slots called registers. There are only a few dozen of them, and they hold the values the processor is working with right now. A key fact for later: a register holds a value, and it has no name of its own. When you write int count = 0; in C, the compiler decides whether count lives in a register or in memory, and it may change its mind partway through a function.

Main memory

Main memory, or RAM, is a very long numbered list of bytes. Every byte has an address — a plain integer. That is the whole model. There are no variables in memory, no types, no names; there are only bytes at addresses. Your program's instructions live there, and so does its data.

This is the single most important idea to carry forward. C is unusual among modern languages precisely because it lets you work with those addresses directly. Weeks 13 through 20 are, in a sense, one long consequence of this paragraph.

Storage

Disks and solid-state drives keep files when the power is off. They are enormously larger than memory and enormously slower. Your source file lives there; so does the executable the compiler produces. When you run a program, the operating system copies it from storage into memory and then points the processor at its first instruction.

Orders of magnitude. A register access takes well under a nanosecond. A main-memory access takes tens of nanoseconds. A solid-state drive read takes tens of microseconds — roughly a thousand times longer again. These ratios are why week 34 spends a whole session on memory layout and cache behavior.

2Compilers, interpreters, and assemblers

The processor understands only machine code: raw numbers encoding its instruction set. Humans do not write machine code. Several kinds of programs exist to close that gap, and they close it in different ways.

Assembler

An assembler translates assembly language — a thin, human-readable naming of machine instructions — into machine code. The mapping is almost one instruction to one instruction. Assembly is specific to one processor family; assembly written for an ARM chip means nothing to an x86 chip.

Compiler

A compiler translates a whole source file written in a higher-level language into machine code, ahead of time. The result is a standalone executable file. Compilation happens once; the program then runs at full speed, with no translator present. C is a compiled language, and this is why C programs are fast and why the compiler is such a central character in this course.

Interpreter

An interpreter reads source code and carries out its meaning directly, statement by statement, every time the program runs. There is no separate executable. This is convenient — no build step — but slower, because translation happens repeatedly while the program runs. Python and JavaScript are usually run this way.

The practical consequence for you is that C has a build step. You will edit a file, run a command, and get a second file. If you change the source and forget to rebuild, you will run the old program and be thoroughly confused. This happens to everyone at least once.

hello.csource preprocessor#include compilerassembly assemblerhello.o linkerexecutable

What gcc hello.c actually runs. Week 2 takes each of these stages apart and inspects its output.

3Installing a toolchain

You need two things: a compiler and a text editor. Both are free on every platform.

Linux

The compiler is almost certainly one command away. On Debian or Ubuntu:

sudo apt update
sudo apt install build-essential gdb

On Fedora, sudo dnf install gcc gdb make. On Arch, sudo pacman -S base-devel gdb.

macOS

Install the Command Line Tools, which provide Clang under the name gcc:

xcode-select --install

Windows

The most useful option is the Windows Subsystem for Linux, because it gives you the same environment the rest of this course assumes:

wsl --install

Then follow the Linux instructions inside the resulting Ubuntu shell. The alternatives are MSYS2 or MinGW-w64 if you prefer to stay on native Windows.

Confirming it works

gcc --version

You should see a version banner. If instead you see command not found, the compiler is not installed or not on your PATH; do not continue until that line produces output.

For an editor, anything that saves plain text will do. Visual Studio Code with the C/C++ extension is a reasonable default. Avoid word processors — a .c file must be plain text with no formatting.

Use the terminal, at least at first. An IDE hides the build step behind a button. That is convenient later and harmful now: the entire point of weeks 1 and 2 is to see what the button does. Type the commands yourself until they are boring.

4Your first program

Create a file called hello.c:

#include <stdio.h>

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

Compile it and run it:

gcc -Wall -Wextra -g -o hello hello.c
./hello

On Windows outside WSL the executable will be hello.exe and you run it as hello.exe.

Line by line

LineWhat it does
#include <stdio.h>Tells the preprocessor to paste in the declarations of the standard input and output library, which is where printf comes from. Without it the compiler does not know what printf is.
int main(void)Declares the function the operating system calls to start your program. It returns an int, and (void) says it takes no parameters. Every C program has exactly one main.
{ … }Braces group the statements that make up the function body.
printf("Hello, world\n");Calls the library function that writes text to the terminal. \n is one character — a newline — not a backslash followed by an n. The semicolon ends the statement.
return 0;Ends main and hands 0 back to the operating system, conventionally meaning success. Week 3 returns to this.

About those flags

Use -Wall -Wextra from your very first compilation and never stop. They turn on warnings about code that is legal C but almost certainly a mistake. A large fraction of the bugs this course spends weeks diagnosing are bugs the compiler would have mentioned if asked. -g includes debugging information, which you will want from week 18 onward, and -o hello names the output file.

Without -o, the compiler writes a file called a.out — a name inherited from 1970s Unix that has outlived every reason for existing.

5Worked example: breaking it on purpose

The goal this week is not to write a correct program. It is to become unafraid of the compiler's output. So take the working hello.c and damage it, one change at a time, rebuilding after each.

Experiment 1 — remove the semicolon

    printf("Hello, world\n")
    return 0;

GCC reports something close to:

hello.c: In function 'main':
hello.c:5:33: error: expected ';' before 'return'
    5 |     printf("Hello, world\n")
      |                             ^
      |                             ;
    6 |     return 0;
      |     ~~~~~~

Read that carefully, because every diagnostic you will ever see has the same shape. hello.c:5:33 is file, line, column. error is the severity. Then the message, then the offending source line with a caret under the exact position — and here GCC even suggests the fix.

Notice that the compiler blames line 5 but only noticed on line 6. It read printf(...), found no semicolon, kept reading, hit return, and only then knew something was wrong. The reported line is where the compiler gave up, not always where you erred. Look one line above as a reflex.

Experiment 2 — remove the include

Delete the #include line. Depending on your compiler and standard you will get either a warning about an implicit declaration or a hard error. Under a modern compiler:

hello.c:3:5: error: implicit declaration of function 'printf' [-Wimplicit-function-declaration]

The compiler has no idea what printf is, because nothing told it. This is your first encounter with the fact that C requires everything to be declared before use, which is what headers exist for and what week 28 is about.

Experiment 3 — misspell a name

Change printf to printff while keeping the include. Now the compiler is satisfied enough to proceed, but the linker fails:

/usr/bin/ld: /tmp/ccXXXXXX.o: in function `main':
hello.c:(.text+0xe): undefined reference to `printff'
collect2: error: ld returned 1 exit status

This message looks nothing like the earlier ones — different program, different vocabulary, no line-and-column caret. Recognizing which tool is complaining is a skill in itself. "undefined reference" always means the linker could not find the body of a function you called. Week 29 covers this in depth.

Experiment 4 — an unused variable

Add int x = 5; to main and rebuild. The program still compiles and runs, but:

hello.c:5:9: warning: unused variable 'x' [-Wunused-variable]

A warning does not stop the build. The compiler is saying that this is legal C but probably not what you meant. Treat warnings as errors in your own habits — later, in week 29, you will make the build system enforce it with -Werror.

6Reading compiler messages

Three rules cover most of the first weeks:

Fix the first error only, then rebuild. One mistake often cascades into a dozen messages. The second through twelfth are usually fiction produced by a confused parser. Fixing the first frequently clears them all.

Read the word after the colon. error means no program was produced. warning means a program was produced that the compiler doubts. note is extra context attached to the message above it, not a separate problem.

Look at the caret, then one line up. The caret points at the token where the compiler noticed, which is frequently one token past the real mistake.

MessageUsual cause
expected ';' before …Missing semicolon at the end of the previous statement.
implicit declaration of function 'X'Missing #include, or the name is misspelled.
undefined reference to 'X'Linker stage: the function was declared but its body was never found. Misspelling, or a missing library such as -lm.
'X' undeclared (first use in this function)Using a variable that was never declared, or declared in a different scope.
expected declaration or statement at end of inputAn unclosed brace. Check the indentation of the whole file.
control reaches end of non-void functionA function that promises to return a value has a path that does not.
format '%d' expects argument of type 'int'A printf format specifier does not match the argument given. Week 8.

The classic first-week trap. You fix the source, rebuild in one terminal, and run the old binary from another directory — or you edit hello.c but compile hello2.c. When a change appears to have no effect whatsoever, the first thing to doubt is whether you actually rebuilt and ran what you think you did.

7Common mistakes

MistakeWhat happensFix
Running gcc hello.c and then ./helloNo such file or directoryWithout -o hello the output is a.out. Always name your output.
Saving the file as hello.c.txtCompiler complains the file type is unrecognizedTurn off "hide known file extensions" in your file manager.
Using curly quotation marks copied from a web pagestray '\342' in programType the code rather than pasting it, or paste into a plain-text editor first.
Writing Printf or MainImplicit declaration, or the program does not linkC is case-sensitive everywhere, always.
Expecting output without \nText appears late, or merges with the shell promptOutput is buffered; the newline usually flushes it. Week 26 explains why.
Ignoring warnings because the program runsA bug survives into week 20 and costs an eveningFix every warning the day it appears.

8Check yourself

Answer before opening each one.

Why does a C program need a build step when a Python program does not?

C is compiled ahead of time into machine code for a specific processor, so translation happens once, before the program runs, and produces a separate executable file. Python is normally interpreted: the translation happens while the program runs, every time. The C approach costs you a build step and buys you speed and a program that runs with no interpreter present.

The compiler reports an error on line 12, but line 12 looks perfectly fine. What should you suspect?

That the actual mistake is on line 11 or earlier. The compiler reports where it could no longer make sense of the text, which is often one token past the omission — most commonly a missing semicolon or an unclosed brace or parenthesis further up.

What is the difference between an error and a warning?

An error means compilation failed and no executable was produced. A warning means an executable was produced, but the compiler noticed something that is legal C and probably wrong. Warnings are advice from a tool that has read millions of programs; the professional habit is to treat them as errors.

You get undefined reference to 'sqrt' even though you included <math.h>. What is going on?

The header told the compiler what sqrt looks like, so the compile stage succeeded. But the header does not contain the function's body — that lives in the math library, which the linker must be told to search. Add -lm to the command line. The distinction between declaration and definition is exactly what week 28 is about.

Where does a variable actually live while a program runs?

In a register, or in main memory at some address — and the compiler decides which, sometimes changing its mind within a single function. The name you wrote does not exist at run time. Holding on to this idea now will make weeks 13 and 17 much easier.

9Where this leads

You now have a working toolchain and a program that runs. Week 2 opens up the single command you have been typing: gcc -o hello hello.c silently runs four separate programs in sequence, and you will inspect the output of each one. That week also places C historically and explains what changed between C89 and C23 — which matters more than it sounds, because the compiler's behavior depends on which standard you ask for.