Skip to content
C++
Language
since C++11
Beginner

Understand How C++ Turns Source Code into a Running Program

Learn how the C++ compiler transforms your source files into an executable, and why that process shapes every decision you make as a C++ programmer.

By the end of this page, you will understand how C++ transforms plain text source files into a running executable, what a translation unit is, what the preprocessor and linker do, and why that knowledge helps you read error messages and structure your code confidently.

What and Why

Most scripting languages read your source file and run it directly. C++ is different. It was designed so that each source file can be compiled independently, without knowing anything about the others, and the resulting pieces are stitched together afterward. This design is decades old, and it has deep consequences for how you write and organize code.

Once you understand this model, questions that baffle newcomers suddenly make sense:

  • Why do I need #include at the top of every file?
  • What is a "linker error," and how is it different from a "compiler error"?
  • Why can I call a function before I've written its body?

The answer to all three lives in the three stages the compiler toolchain runs every time you build: preprocessing, compilation, and linking.

Step by Step

Stage 1: Preprocessing

Before the compiler ever sees your code, a separate tool—the preprocessor—runs over every source file. It handles lines that start with #, and its job is purely textual: copy, paste, and replace.

The most common directive is #include. When the preprocessor encounters this line:

cpp
#include <iostream>

it opens the file named iostream and pastes its entire contents in place. The compiler never sees the #include directive itself—by the time compilation begins, the file is already one large block of expanded text.

Stage 2: Compilation

After preprocessing, each .cpp file (called a translation unit) is handed to the compiler. The compiler reads it independently, checks types and syntax, and produces an object file—a .o file on Linux and macOS, or a .obj file on Windows. Object files contain machine code, but they are not yet runnable; they may reference names that live in other translation units.

Here is the smallest possible translation unit:

cpp
#include <iostream>

int main() {
    std::cout << "Hello, world!\n";
    return 0;
}

Save this as hello.cpp. You can compile it to an object file without linking by running g++ -c hello.cpp -o hello.o. The resulting hello.o is machine code that the computer cannot execute on its own yet.

Stage 3: Linking

The linker combines all the object files your program needs, plus any libraries, into a single executable. Its job is to resolve every reference: if hello.o calls std::cout, the linker finds the definition inside the standard library and wires them together.

Running g++ hello.cpp -o hello performs all three stages in one command. You can also do them separately:

cpp
// Stage 1+2: g++ -c hello.cpp -o hello.o
// Stage 3:   g++ hello.o -o hello
// Run:       ./hello

A Two-File Example

Real programs span multiple files. Here is a minimal example that shows every stage interacting.

The header file declares that greet exists. It does not define what greet does:

cpp
// greet.h
#ifndef GREET_H
#define GREET_H

void greet(const char* name);

#endif

The implementation file defines what greet actually does:

cpp
// greet.cpp
#include "greet.h"
#include <iostream>

void greet(const char* name) {
    std::cout << "Hello, " << name << "!\n";
}

The entry point calls greet without knowing how it works—only that it exists:

cpp
// main.cpp
#include "greet.h"

int main() {
    greet("Alice");
    return 0;
}

Build with: g++ main.cpp greet.cpp -o app. The compiler processes main.cpp and greet.cpp into separate object files, then the linker joins them. main.o has a placeholder saying "call something called greet"; greet.o has the actual code. The linker fills in the address.

Common Patterns

Pattern 1: Header Guards

Every header must be protected against being pasted twice into the same translation unit. Without a guard, if two of your .cpp files both include utils.h, the preprocessor would paste its contents twice, and the compiler would see duplicate declarations.

cpp
// math_utils.h — classic guard
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int square(int x);

#endif

Many compilers also support #pragma once, which is shorter and equally effective in practice:

cpp
// math_utils.h — pragma once variant
#pragma once

int square(int x);

Pattern 2: Declarations in Headers, Definitions in .cpp

Put declarations (what exists) in headers. Put definitions (the actual code) in .cpp files. A definition must appear exactly once across all translation units—the One Definition Rule (ODR). If two object files both define the same function, the linker rejects the program.

cpp
// counter.h
#pragma once

int add(int a, int b);   // declaration only
cpp
// counter.cpp
#include "counter.h"

int add(int a, int b) {  // one and only definition
    return a + b;
}
cpp
// main.cpp
#include "counter.h"
#include <iostream>

int main() {
    std::cout << add(3, 4) << "\n";
    return 0;
}

Pattern 3: Inline Functions in Headers

Short functions defined directly in a header must be marked inline. This tells the linker that identical copies will appear in every translation unit that included the header—that is expected and fine; keep just one.

cpp
// math_utils.h
#pragma once

inline int square(int x) {
    return x * x;
}

Without inline, including this header in two .cpp files would produce a "multiple definition" linker error.

What Can Go Wrong

Forgetting to include a header

cpp
// WRONG — <string> was never included
int main() {
    std::string s = "hello";   // error: 'string' is not a member of 'std'
    return 0;
}
cpp
// CORRECT
#include <string>

int main() {
    std::string s = "hello";
    return 0;
}

Do not rely on transitive includes. A header you include today might include <string> as a side effect, but that is an implementation detail that can change.

Defining a non-inline function in a header

cpp
// utils.h — WRONG
int double_it(int x) {
    return x * 2;    // definition in a header without inline
}

If two .cpp files include this header, the linker sees two definitions of double_it and fails. Fix it by adding inline, or by moving the definition to utils.cpp.

Missing a source file from the build command

cpp
// WRONG — greet.cpp is missing from the build
// g++ main.cpp -o app
// linker error: undefined reference to `greet(char const*)`
cpp
// CORRECT
// g++ main.cpp greet.cpp -o app

"Undefined reference" is the linker saying it found a call to a name but no definition anywhere in the object files it was given. Either add the missing .cpp file, or link the library that contains the definition.

Quick Reference

StageInputOutputTypical error
Preprocessing.cpp + headersExpanded source textCannot open include file
CompilationExpanded source textObject file (.o)Syntax error, type mismatch
LinkingObject files + librariesExecutableUndefined reference, multiple definition

Terms to know:

  • Translation unit — one .cpp file after preprocessing; the atom the compiler works on
  • Declaration — tells the compiler a name exists and its type; can appear many times
  • Definition — provides the actual implementation; must appear exactly once (unless inline)
  • Header guard#ifndef/#define/#endif (or #pragma once) that prevents double inclusion
  • One Definition Rule (ODR) — every non-inline entity must be defined in exactly one translation unit

What's Next

With the compilation model in hand, you are ready to explore how C++ manages memory and objects during a program's lifetime:

  • Memory Model — the rules C++ defines for reading and writing memory safely across threads and compiler optimizations
  • Object Model — what an "object" is in C++, how objects occupy storage, and how the compiler lays them out

Once you have written a few multi-file programs, look into build systems like CMake, which automate the compile and link commands so you do not have to list every .cpp file by hand.