C++ Compilation & Build Process

How a .cpp file becomes a running program: preprocessing, compiling, assembling, and linking, plus the difference between a compiler error and a linker error.

A C++ program written by a programmer is source code. The computer cannot directly execute that source code as written. It must be processed through several stages to produce an executable program.

Overview of the Build Process

A simplified C++ build process is:

For a small program, you may run one command such as:

But internally, several steps are involved.

C++ Source Files
      │
      ▼
 Preprocessing
      │
      ▼
 Compilation
      │
      ▼
 Assembly
      │
      ▼
 Object Files
      │
      ▼
 Linking
      │
      ▼
 Executable
      │
      ▼
 Program Execution
g++ main.cpp -o main

1. Source Code

A C++ program begins as one or more source files.

This might be stored as main.cpp. The .cpp extension is commonly used for C++ source files. A larger project may contain:

Each .cpp file is processed as part of the build.

#include <iostream>
int main()
{
    std::cout << "Hello, World!";
    return 0;
}
Project/
├── main.cpp
├── calculator.cpp
└── calculator.h

2. What Is the Build Process?

The build process is the overall process of transforming source code into a usable executable or library. It can involve: Source Code → Preprocessing → Compilation → Assembly → Object Files → Linking → Executable.

The terms compilation and build are related but not identical. Compilation generally refers to translating source code into object code or another lower-level representation. A build includes the broader process of determining what needs to be compiled, compiling source files, linking object files, linking libraries, applying compiler/linker options, and producing the final executable or library.

Build systems such as CMake, Make, and Ninja automate this process.

3. Stage 1 — Preprocessing

Before normal C++ compilation, the source goes through the preprocessor.

PI is a macro handled by the preprocessor before compilation begins.

The preprocessor handles directives such as #include, #define, #if, #ifdef, #ifndef, and #endif. For example, #include <iostream> causes the appropriate header contents to be made available to the translation unit. Similarly, #define PI 3.14159 creates a macro definition that participates in preprocessing. The result is a preprocessed translation unit.

The preprocessor does not perform the complete C++ language compilation.

#include <iostream>
#define PI 3.14159
int main()
{
    std::cout << PI;
}
main.cpp
   │
   ▼
Preprocessor
   │
   ├── Process #include
   ├── Expand macros
   ├── Process conditional compilation
   └── Remove comments
   │
   ▼
Preprocessed source

4. What Is a Translation Unit?

A translation unit is an important C++ compilation concept. Roughly: a translation unit is a source file after preprocessing, including the contents brought in through its #include directives.

For example: main.cpp + included headers + macro processing → Translation Unit. If a project has main.cpp and calculator.cpp, they normally become separate translation units (Translation Unit 1 and Translation Unit 2) and are generally compiled independently.

5. Stage 2 — Compilation

After preprocessing, the resulting translation unit is processed by the C++ compiler. The compiler analyzes the C++ language constructs and translates them into lower-level code.

Conceptually: Preprocessed C++ → C++ Compiler (syntax checking, type checking, semantic analysis, optimization, code generation) → Lower-level code.

The compiler understands int, function declaration, parameters, the return statement, addition, and type rules, then generates machine-oriented code for it.

int add(int a, int b)
{
    return a + b;
}

6. Syntax Checking

The compiler checks whether the source follows C++ syntax rules.

Missing semicolon — the compiler will diagnose this as a compilation error.

int main()
{
    int x = 10
    return 0;
}
int main()
{
    int x = 10;
    return 0;
}

7. Type Checking

The compiler also checks many type-related rules.

Invalid because a string literal cannot initialize an int object this way.

This is invalid because a string literal cannot initialize an int object in this way. The compiler detects the type mismatch. This is one of the benefits of C++'s static type system.

int number = "Hello";

8. Semantic Analysis

Compilation involves more than checking syntax. The compiler also determines whether the program's constructs make sense according to C++ language rules.

The compiler needs to understand what add refers to, its parameter types, its return type, whether the function call is valid, and how the expressions should be interpreted. This is part of semantic analysis.

int add(int a, int b)
{
    return a + b;
}
int main()
{
    return add(10, 20);
}

9. Optimization

Compilers can optimize generated code.

A compiler may determine the result is always 30 and generate simpler machine code accordingly.

Optimization can involve removing unnecessary operations, inlining suitable functions, simplifying expressions, eliminating unreachable code, improving register usage, optimizing loops, and improving memory access. The exact optimizations depend on the compiler, optimization settings, target architecture, and program.

int calculate()
{
    return 10 + 20;
}

10. Stage 3 — Assembly

A compiler may generate assembly language for the target processor as an intermediate step.

Conceptually: C++ Code → Compiler → Assembly. A simple C++ operation such as int result = a + b; may eventually become processor-specific instructions. Assembly language is much closer to the machine's instruction set than C++. For example, an architecture might have instructions conceptually resembling mov, add, sub, and jmp. The exact instructions depend on the target CPU architecture.

11. Stage 4 — Assembling

The assembler converts assembly language into machine code and produces an object file.

Conceptually: Assembly Code → Assembler → Object File. An object file commonly contains machine code, data, symbol information, relocation information, and other metadata. Common object-file extensions include .o and .obj depending on the platform/toolchain.

12. Object Files

Suppose a project contains main.cpp and calculator.cpp. They may be compiled separately: main.cpp → main.o, calculator.cpp → calculator.o.

The object files are not necessarily complete executable programs. They may contain references to functions or objects that are defined elsewhere.

calculator.cpp defines add().

main.cpp only declares add() and uses it; its implementation lives in another translation unit.

main.cpp uses add, but its implementation is in another translation unit. The linker is responsible for connecting these pieces.

// calculator.cpp
int add(int a, int b)
{
    return a + b;
}
// main.cpp
int add(int, int);
int main()
{
    return add(10, 20);
}

13. Symbols

Object files contain information about symbols. A symbol can represent things such as functions, objects, or other linkable entities.

Creates symbol information associated with the function add.

Another object file might contain a reference to add. The linker uses symbol information to resolve such references.

int add(int a, int b)
{
    return a + b;
}

14. Stage 5 — Linking

The linker combines object files and libraries to produce the final executable or library.

Conceptually: main.o + calculator.o + libraries → Linker → Executable. For example: main.o + calculator.o + standard libraries → Linker → program.

15. What Does the Linker Do?

The linker performs several tasks.

Symbol Resolution — It connects references to definitions. For example, int add(int, int); may be used in one translation unit while int add(int a, int b) { return a + b; } is defined in another. The linker resolves the reference to the actual definition.

Relocation — Machine code may contain addresses or address-dependent references that cannot be finalized until the final layout is known. The linker adjusts these references appropriately.

Combining Object Files — Multiple object files can be combined into a single executable or library.

16. Static Libraries

A static library contains compiled code that can be linked into a program. Common extensions include .a and .lib depending on the platform.

Conceptually: calculator.o + string_utils.o → Static Library → Executable. When the executable is linked, required library code can be incorporated into the executable.

17. Dynamic Libraries

Dynamic libraries are separate libraries that can be loaded or used by programs at runtime or through platform-specific dynamic linking mechanisms. Common extensions include .so, .dll, and .dylib depending on the operating system.

Conceptually: an executable uses application code, and also uses a dynamic library which provides runtime support. Dynamic linking can reduce duplication of shared library code and allows libraries to be updated independently in some deployment models.

18. Complete Build Flow

A more detailed picture is:

This is a conceptual model; actual compiler toolchains may combine or rearrange some stages internally.

C++ Source
                      │
                      ▼
               Preprocessor
                      │
                      ▼
             Translation Unit
                      │
                      ▼
                 Compiler
                      │
                      ▼
                 Assembly
                      │
                      ▼
                Assembler
                      │
                      ▼
                Object File
                      │
              ┌───────┴────────┐
              │                │
           Object File      Libraries
              │                │
              └───────┬────────┘
                      ▼
                   Linker
                      │
                      ▼
                 Executable
                      │
                      ▼
                 OS Loader
                      │
                      ▼
                 Running Program

19. Compilation vs Linking Errors

Understanding this distinction is very important.

A compilation error: the compiler cannot correctly compile this source and reports something like "expected primary-expression".

A linker error: this compiles successfully because the declaration is valid, but if no definition of add is provided anywhere, the linker cannot resolve the call and reports something like "undefined reference to add(...)".

int main()
{
    int x = ;
}
int add(int, int);
int main()
{
    return add(10, 20);
}

20. Compilation Error vs Linker Error

A useful rule: the compiler asks "Is this translation unit valid C++?"; the linker asks "Can all required references be connected to their definitions?"

Problem                       Detected by   Example
Syntax error                  Compiler      Missing ;
Type error                    Compiler      Invalid type conversion
Invalid declaration           Compiler      Incorrect declaration
Missing function definition   Linker        Undefined reference
Duplicate symbol definition   Linker        Multiple conflicting definitions
Missing library               Linker        Required library not found

21. Example: Multi-File Build

calculator.h — the interface, guarded against multiple inclusion.

calculator.cpp — the implementation.

main.cpp — uses the declared function.

The build can conceptually happen as: calculator.cpp → Preprocess → Compile → calculator.o; main.cpp → Preprocess → Compile → main.o; then main.o + calculator.o + required libraries → Linker → executable.

#ifndef CALCULATOR_H
#define CALCULATOR_H
int add(int a, int b);
#endif
#include "calculator.h"
int add(int a, int b)
{
    return a + b;
}
#include <iostream>
#include "calculator.h"
int main()
{
    std::cout << add(10, 20);
    return 0;
}

22. Using GCC/G++

With GCC's C++ compiler driver, a simple command is:

This command asks the toolchain to build the program. The resulting executable is commonly named main on Unix-like systems. On Windows, the resulting executable typically has the .exe extension: main.exe.

g++ main.cpp -o main

23. Separate Compilation

For multiple source files, you can compile them separately.

The -c option tells the compiler driver to stop before the final linking step. Then:

links the object files.

g++ -c main.cpp -o main.o
g++ -c calculator.cpp -o calculator.o
g++ main.o calculator.o -o program

24. Preprocessing Only

You can ask a compiler toolchain to produce preprocessed output. For GCC:

The resulting file contains the source after preprocessing. This can be useful when investigating macro expansion, #include, and conditional compilation.

g++ -E main.cpp -o main.i

25. Compilation to Assembly

You can request assembly output. For GCC:

Conceptually: main.cpp → Preprocessing + Compilation → main.s. The .s file contains assembly code for the selected target.

g++ -S main.cpp -o main.s

26. Creating an Object File

You can create an object file using:

Conceptually: main.cpp → Compiler + Assembler → main.o. The object file is not necessarily a directly executable program.

g++ -c main.cpp -o main.o

27. Linking Object Files

After creating object files:

the linker combines them: main.o + calculator.o → Linker → program.

g++ main.o calculator.o -o program

28. Compiler Driver vs Compiler

Commands such as g++ are often called the compiler driver. The driver coordinates multiple tools involved in the build process. Depending on the toolchain and options, these can include the preprocessor, compiler, assembler, and linker.

Therefore, when you run g++ main.cpp -o main, you are not necessarily invoking only one internal program. The driver orchestrates the necessary stages.

29. Build Systems

As projects become larger, manually compiling every source file becomes inconvenient. For example, a project with 100+ source files needs many compilation commands, dependency management, library configuration, compiler options, and linker options.

Build systems automate this process. Common tools include Make, CMake, Ninja, and Meson. A build system can determine which files need rebuilding, which compiler options are required, which libraries are required, what should be linked, and where the output should be placed.

30. Incremental Builds

Suppose a project contains main.cpp, calculator.cpp, and display.cpp. You modify only calculator.cpp. A good build system can avoid recompiling everything.

Conceptually: main.cpp → main.o, calculator.cpp → calculator.o, display.cpp → display.o, all feeding into the executable. After modifying calculator.cpp, only calculator.cpp needs to compile again; main.cpp and display.cpp stay unchanged. Then the linker creates a new executable. This is called an incremental build.

31. Debug and Release Builds

C++ projects commonly have different build configurations.

Debug Build — usually designed to make debugging easier. It may include debugging information, lower optimization, and additional diagnostics.

Release Build — usually designed for deployment or performance testing. It may include higher optimization, reduced debugging information, and different assertions/production-specific settings. The exact configuration depends on the build system and project.

32. Compiler Options

Compilers provide options that affect compilation.

Here, -std=c++20 selects C++20 language mode.

These options enable additional compiler warnings. Warnings are valuable because they can reveal suspicious or potentially problematic code even when the program is technically valid.

g++ -std=c++20 main.cpp -o main
g++ -Wall -Wextra main.cpp -o main

33. Build Artifacts

A build can produce several different files: source (main.cpp); intermediate (main.i, main.s, main.o); final (main).

A typical build directory might therefore contain:

Intermediate files are usually generated automatically by the build system.

build/
├── main.o
├── calculator.o
└── application

34. From Executable to Running Program

After linking produces an executable, the operating system can load it.

Conceptually: Executable → Operating System Loader → Program loaded into memory → Runtime initialization → main() → Program execution.

The exact startup process depends on the operating system, executable format, runtime environment, and toolchain. The C++ language specifies the semantics of program startup and main, while the platform handles the lower-level executable loading and process setup.

35. Important Concept: Header Files Are Not Linked

For example, #include "calculator.h" is handled during preprocessing. It makes the declarations from the header available to the translation unit. The .cpp implementation file is separately compiled and later connected through linking.

Conceptually: #include "calculator.h" → Preprocessing → Declaration available; calculator.cpp → Compilation → calculator.o → Linking. Therefore, including a header and linking a source/object file are two different operations.

36. Why Linker Errors Happen

A linker error often occurs when a program has a declaration but cannot find the corresponding definition.

If int calculate(int value) { return value * 2; } is not provided anywhere, the linker cannot resolve this call.

Another common case is forgetting to include an object file:

when calculate.o is required. The correct build might be:

int calculate(int);
int main()
{
    return calculate(10);
}
g++ main.o -o program
g++ main.o calculate.o -o program

37. Typical Build Problems and Their Causes

Understanding where each kind of problem is detected helps you know which part of the toolchain to investigate.

Syntax errors — e.g. int x = ; — are usually detected during compilation.

Missing header — e.g. #include "unknown.h" — if the header cannot be found, compilation fails.

Undefined reference — a required definition is not available to the linker.

Multiple definitions — the linker may detect conflicting definitions of an entity that cannot legally have multiple definitions.

Missing library — the program uses a library but the linker cannot find the required library.

Wrong C++ standard — code requiring C++20 may fail when compiled in an older language mode. For example, g++ -std=c++17 program.cpp may not support all C++20 language features.

38. Complete Mental Model

Keep this model in mind:

SOURCE CODE
                   │
                   ▼
            PREPROCESSOR
                   │
                   ▼
          TRANSLATION UNIT
                   │
                   ▼
              COMPILER
                   │
                   ▼
              ASSEMBLER
                   │
                   ▼
             OBJECT FILE
                   │
          ┌────────┴────────┐
          │                 │
     Other .o files      Libraries
          │                 │
          └────────┬────────┘
                   ▼
                 LINKER
                   │
                   ▼
              EXECUTABLE
                   │
                   ▼
            OS / LOADER
                   │
                   ▼
             RUNNING APP

39. Compilation vs Build — Quick Comparison

What is a translation unit, and why do main.cpp and calculator.cpp normally become two separate ones?

A translation unit is a source file after preprocessing, including everything pulled in through its #include directives. Since main.cpp and calculator.cpp are preprocessed and compiled independently, each becomes its own translation unit.

A program compiles successfully but fails at the final step with "undefined reference to add(...)". What stage produced this error, and what is the most likely cause?

This is a linker error, not a compiler error — it happens after successful compilation, during linking. The most likely cause is that add() was declared and used but its definition was never provided or its object file was never included in the link step.

Explain why "#include links another file" is a misconception.

What is the difference between a static library and a dynamic library in terms of when their code becomes part of the running program?

A static library's code (e.g. a .a or .lib) is incorporated directly into the executable at link time. A dynamic library (e.g. a .so, .dll, or .dylib) stays separate and is loaded or linked to the program at runtime, which avoids duplicating the library's code across executables and lets it be updated independently.

What does the -c flag do when running g++ -c main.cpp -o main.o, and why is a separate linking command needed afterward?

-c tells the compiler driver to stop after producing the object file (main.o) and skip linking. Because the object file may still reference symbols defined in other translation units or libraries, a separate linking command (e.g. g++ main.o calculator.o -o program) is needed to resolve those references and produce the final executable.

This file compiles without errors. If no definition of add() exists anywhere in the project, what happens next, and at which stage?

Compilation succeeds because the declaration int add(int, int); is syntactically and semantically valid. The problem surfaces at the linking stage, where the linker cannot find a definition for add and reports an "undefined reference" error.

Write calculator.h (with an include guard) declaring int subtract(int a, int b);, calculator.cpp defining it, and main.cpp that calls it and prints the result of subtract(10, 4). Then give the two g++ commands that would compile each .cpp file separately into object files and a third that links them into a program named app.

// calculator.h #ifndef CALCULATOR_H #define CALCULATOR_H int subtract(int a, int b); #endif // calculator.cpp #include "calculator.h" int subtract(int a, int b) { return a - b; } // main.cpp #include <iostream> #include "calculator.h" int main() { std::cout << subtract(10, 4); return 0; }

Build with: g++ -c calculator.cpp -o calculator.o g++ -c main.cpp -o main.o g++ main.o calculator.o -o app The first two commands compile each translation unit to an object file (-c stops before linking); the third links both object files into the executable app.

At which build stage is this rejected, and which specific check catches it?

It is rejected during the compilation stage, specifically by the compiler's type checking — a string literal cannot be used to initialize an int object, so this is a type mismatch caught before code generation even happens.

Write the g++ commands that: (1) produce only the preprocessed output of main.cpp as main.i, and (2) produce only the assembly output of main.cpp as main.s.

g++ -E main.cpp -o main.i g++ -S main.cpp -o main.s -E stops the driver right after preprocessing and writes the expanded source to main.i. -S runs preprocessing and compilation but stops before assembling, writing the generated assembly to main.s.

Preprocessing   Processes #include, macros, conditional compilation, etc.
Translation unit  Preprocessed source file
Compilation     Analyzes C++ and generates lower-level code
Assembly        Converts assembly into machine-code object files
Object file     Compiled machine code plus symbols/relocations and other metadata
Linking         Combines object files and libraries and resolves references
Executable      Final program that can be loaded by the OS
Build           Overall process of producing the executable/library
Build system    Automates compilation, dependencies, linking, and configuration
int add(int, int);
int main()
{
    return add(10, 20);
}
int number = "Hello";