Hello, machine
How source text becomes a running program, and what each stage can go wrong at.
By the end of this chapter you can
- Explain what the preprocessor, compiler, assembler, and linker each do
- Read a compiler error and locate the line it refers to
- Compile and run a program from source
A C++ program starts life as text you can read and ends as instructions a processor can execute. Nothing in between is magic, and every stage of the journey has its own kind of failure. Learning which stage a message came from is most of what it takes to stop being afraid of error output.
Here is the whole thing. Press Run.
#include <iostream>
int main() {
std::cout << "Hello, machine\n";
}That worked, so a lot of machinery worked. Let us take it apart.
What each line is doing
#include <iostream> is not a C++ statement. It is a preprocessor directive,
and it runs before the compiler proper sees anything. It means: find the file
called iostream and paste its entire contents here. That file declares
std::cout and the << operator, which is why the next line can use them.
int main() declares a function named main. It is not special because of its
name alone — it is special because the runtime that starts your program is
written to call a function with exactly that name. Every C++ program has exactly
one. The int says main hands back an integer when it finishes; the operating
system uses that number to decide whether your program succeeded. Zero means
success. You did not write a return, and main is the one function allowed to
omit it: leaving it out means return 0.
std::cout << "Hello, machine\n"; sends characters to standard output.
std::cout is an object representing that output stream; << is an operator
that has been given a meaning for streams. std:: says both live in the
namespace std, which is where everything in the standard library lives.
Four programs run before yours does
When you compile a single file, four separate programs handle it in sequence. Each one produces input for the next, and each one has its own vocabulary of complaints.
| Stage | Input | Output | A failure here says |
|---|---|---|---|
| Preprocessor | your .cpp |
one long expanded file | No such file or directory |
| Compiler | expanded source | assembly | error: expected ';', no matching function |
| Assembler | assembly | an object file .o |
almost never fails |
| Linker | object files | an executable | undefined reference to … |
The distinction that matters most in practice is the last two rows. A compiler error means one file, on its own, does not make sense. A linker error means every file made sense individually, but something they promised each other was never delivered.
Try producing one of each. This program compiles but does not link — it
declares that mystery exists somewhere and calls it, but nowhere in the
program is it ever defined:
int mystery(int x); // a promise: this exists somewhere
int main() {
return mystery(3);
}Read the message. It does not talk about lines or syntax; it talks about a reference it could not resolve. That is the shape of every linker error you will ever see.
Now a compiler error, in the same spirit:
#include <iostream>
int main() {
std::cout << "the semicolon is missing\n"
}The message names a file, a line, and a column. Compilers report the point where
the text stopped making sense, which is often just after the mistake — here it
points at the closing brace, because that is where a ; was expected. When a
message points at a line that looks fine, look at the line above it.
What the machine actually gets
Nothing above the assembler survives into the final program. Names, comments, types, the structure of your functions — all of it exists to constrain what the compiler is allowed to produce, and then it is gone.
Press Assembly on this one to see what a two-line function becomes:
int add(int a, int b) {
return a + b;
}One instruction to add, one to return. Notice what is not there: no trace of
the names add, a, or b, and no check that you passed integers. The type
system did its work at compile time and then evaporated. This is the trade C++
makes everywhere — checking happens early, so nothing has to happen late.
Change -O0 to -O2 in the dropdown and run it again. At -O2 the call to
add inside main disappears entirely: the compiler computed 2 + 3 itself.
The build, on your own machine
You will not always have a Run button. The equivalent on a terminal:
g++ -std=c++20 -Wall -Wextra -o hello hello.cpp
./helloRead that as: use the C++20 rules, turn on the useful warnings, put the
executable in a file called hello, and build it from hello.cpp.
The two warning flags are not optional in practice. C++ will compile a great
deal of code that is technically legal and definitely wrong, and -Wall -Wextra is how you get told. Chapter 1.6 covers the rest of the toolchain, but
adopt those two flags today.