Question
How does the C++ compilation and linking process transform source code into an executable program? Explain the roles of preprocessing, compilation, assembling, object files, libraries, and the linker.
Short Answer
By the end of this page, you will understand how a C++ program moves from .cpp and header files to an executable, why declarations and definitions matter, and how to diagnose common linker errors.
Concept
C++ programs are usually built in stages rather than all at once. A compiler does not normally compile an entire project as a single unit. Instead, each source file is processed independently and produces an object file. A linker then combines those object files and any required libraries into the final executable or library.
A typical native C++ build has these stages:
- Preprocessing: Handles directives such as
#include,#define, and conditional compilation. - Compilation: Parses C++ code, checks types, and translates each source file into lower-level code.
- Assembly: Converts assembly output into machine-code object files. Many compiler commands perform compilation and assembly together.
- Linking: Connects references between object files and libraries, producing an executable or shared library.
A .cpp file plus all headers included into it forms a translation unit. Every translation unit is compiled separately. Header files usually contain declarations and reusable definitions such as templates or inline functions; they are textually included into each translation unit that includes them.
The linker works with symbols: named items such as functions, global variables, and class methods. If one object file calls printMessage() and another object file defines it, the linker connects the call to that definition.
This matters because successful compilation does not guarantee successful linking. The compiler can accept a declaration such as void save();, but the linker must still find exactly one suitable definition of save() when creating the final program.
Mental Model
Think of a C++ project as a set of chapters written by separate authors.
- A header is a table of contents: it tells an author that a function exists and how it can be called.
- A source file contains the actual writing for one chapter.
- The compiler checks and translates each chapter independently.
- An object file is a translated chapter that still contains references such as “see chapter 4.”
- The linker is the editor who joins all translated chapters, resolves every cross-reference, and binds them into a complete book: the executable.
If a chapter refers to a function that no chapter actually defines, the editor reports an unresolved reference. If two chapters both claim to provide the same non-inline function, the editor reports a duplicate definition.
Syntax and Examples
A common project layout separates declarations from definitions.
// math_utils.h
#pragma once
int add(int left, int right); // Declaration
// math_utils.cpp
#include "math_utils.h"
int add(int left, int right) { // Definition
return left + right;
}
// main.cpp
#include <iostream>
#include "math_utils.h"
int main() {
std::cout << add(2, 3) << '\n';
}
Build the files together with a compiler driver such as g++ or clang++:
g++ -std=c++20 main.cpp math_utils.cpp -o calculator
The compiler first preprocesses and compiles main.cpp and math_utils.cpp separately. The linker then sees that main.cpp calls add and that math_utils.cpp provides its definition.
To view separate object-file steps explicitly:
g++ -std=c++20 -c main.cpp -o main.o
g++ -std=c++20 -c math_utils.cpp -o math_utils.o
g++ main.o math_utils.o -o calculator
The -c option means “compile, but do not link.” The final command performs the link step.
Step by Step Execution
Consider these files:
// greeting.h
#pragma once
void greet();
// greeting.cpp
#include <iostream>
#include "greeting.h"
void greet() {
std::cout << "Hello!\n";
}
// main.cpp
#include "greeting.h"
int main() {
greet();
}
Build command:
g++ -std=c++20 main.cpp greeting.cpp -o app
What happens:
- The preprocessor processes
main.cpp. Its#include "greeting.h"is effectively replaced by the declarationvoid greet();. - The compiler checks that calling
greet()is valid because it knows the declared function signature. - It creates an object file for
main.cppcontainingmainand an unresolved reference togreet. - The preprocessor and compiler process
greeting.cpp. It includes the same declaration and then compiles the definition ofgreet. - An object file for
greeting.cppis produced. It exports a symbol forgreet. - The linker combines both object files, matches the call from
mainwith the definition fromgreeting.cpp, and writesapp. - When
appruns, execution starts atmain(), which calls the linkedgreet()function.
Real World Use Cases
- Large applications: A project may have hundreds of
.cppfiles. Separate compilation means only changed files usually need recompilation. - Third-party libraries: Your code can include a library header for declarations and link against the library binary containing definitions.
- Operating system APIs: A header describes an API function, while a system library supplies its implementation.
- Static libraries: A reusable collection of object files can be packaged into a static library such as
libhelpers.a. - Shared libraries: A shared library such as
libhelpers.soon Linux or a DLL on Windows can be loaded with the program or at runtime. - Build systems: CMake, Ninja, and Make record which sources produce which object files and which libraries must be linked.
Real Codebase Usage
Real projects use file boundaries deliberately.
Public headers and implementation files
A library commonly exposes declarations in headers while keeping non-public implementation details in .cpp files.
// logger.h
#pragma once
void logError(const char* message);
// logger.cpp
#include <iostream>
#include "logger.h"
void logError(const char* message) {
std::cerr << "Error: " << message << '\n';
}
This reduces recompilation and hides details from users of the library.
Header guards
A header can be included indirectly more than once. Use #pragma once or include guards to avoid repeated declarations in one translation unit.
#ifndef LOGGER_H
#define LOGGER_H
void logError(const char* message);
#endif
Templates and inline definitions
Templates usually need their full definitions in headers because the compiler must generate code for each used template specialization in each translation unit.
// max_value.h
#pragma once
template <typename T>
T maxValue(T a, T b) {
return a > b ? a : b;
}
Small functions defined in headers should generally be marked inline to permit identical definitions across translation units.
Linking external libraries
Build tools specify both include paths and link libraries. For example, include paths help the compiler find headers, while -l options tell the linker which library to use.
g++ main.cpp -Iinclude -Llib -lwidget -o app
-Iinclude is for headers. -Llib -lwidget is for linking libwidget from the library directory.
Common Mistakes
Declaring a function but never defining it
// main.cpp
void process();
int main() {
process();
}
This often compiles but fails at link time because there is no definition of process().
Avoid it: Add a matching definition in a compiled .cpp file, and ensure that file is included in the build.
Forgetting to compile or link a source file
g++ main.cpp -o app
If main.cpp calls code defined in helpers.cpp, this command does not include it.
Fix:
g++ main.cpp helpers.cpp -o app
Defining a normal function in a header
// broken_util.h
int square(int value) {
return value * value;
}
If several .cpp files include this header, each produces a definition of square. Linking can fail with a multiple-definition error.
Avoid it: Put the definition in one .cpp file, or make a header-defined function inline when that design is appropriate.
inline int square(int value) {
return value * value;
}
Mismatched declarations and definitions
// header
void print(int value);
// source
void print(long value) {
}
These are different overloads. A call to print(int) may remain unresolved.
Avoid it: Include the function's own header in its .cpp file. The compiler can then detect many mismatches.
Confusing compiler errors with linker errors
- A compiler error means a translation unit is not valid C++: syntax errors, unknown types, or invalid calls.
- A linker error means compiled pieces cannot be connected: missing or duplicate definitions.
Relying on include order
Headers should include what they directly need rather than assuming another header was included first. This makes each translation unit reliable.
Comparisons
| Term | Main job | Typical input | Typical output |
|---|---|---|---|
| Preprocessor | Expands includes and processes macros/conditions | Source and headers | Preprocessed source |
| Compiler | Checks C++ and translates it | One translation unit | Assembly or object code |
| Assembler | Converts assembly to machine code | Assembly | Object file |
| Linker | Resolves symbols and combines binary pieces | Object files and libraries | Executable or library |
| Item | Contains | Main purpose |
|---|---|---|
Header (.h, .hpp) | Declarations, templates, inline definitions | Shares an interface with other source files |
Source file (.cpp) | Function and class-member definitions | Implements program behavior |
Object file (.o, .obj) | Compiled machine code plus symbols | Intermediate file sent to linker |
Static library (.a, .lib) | Archive of object files | Code is copied into the final executable as needed |
Shared library (.so, .dylib, .dll) | Separately loadable compiled code | Code can be shared by multiple programs |
A declaration tells the compiler that something exists. A definition provides the actual storage or executable implementation. Most functions need one definition in the complete program, subject to C++ rules such as the One Definition Rule.
Cheat Sheet
- A translation unit is one
.cppfile after its#includedirectives are processed. - Compile each translation unit independently.
- Use headers for declarations shared by multiple source files.
- Use
.cppfiles for ordinary function and variable definitions. -ccompiles to an object file without linking:
g++ -c file.cpp -o file.o
- Link object files into an executable:
g++ main.o helpers.o -o app
- Missing definition: commonly an undefined reference or unresolved external linker error.
- Duplicate ordinary definition: commonly a multiple definition linker error.
- Put templates and usually
inlinefunction definitions in headers. - Use
#pragma onceor include guards in headers. - Include a
.cppfile's matching header in that.cppfile to catch interface mismatches. - Header search paths (
-I) help compilation; library options (-L,-l) affect linking.
FAQ
What is the difference between compiling and linking in C++?
Compiling translates each translation unit into an object file and checks C++ rules. Linking combines object files and libraries, resolving calls and references between them.
Why does my C++ program compile but fail to link?
The compiler found valid declarations, but the linker could not find a matching definition, a required object file, or a required library.
Does #include link a file?
No. #include is preprocessing: it inserts header text into a translation unit. Linking happens later with object files and libraries.
Should I include .cpp files?
Usually no. Compile .cpp files separately and link their object files. Include headers to access declarations.
Why are templates usually defined in header files?
The compiler needs the template definition while compiling each use so it can generate code for the required type arguments.
What does an undefined reference mean?
It means code refers to a symbol, such as a function or global variable, but the linker cannot find a compatible definition in the supplied object files and libraries.
What is the One Definition Rule?
It is a group of C++ rules that, among other things, generally requires one definition of each ordinary function or variable in the complete program. Certain entities, including inline functions and templates, have special rules.
Is a static library the same as a shared library?
No. A static library is an archive of object files used while linking. A shared library remains a separate binary that is loaded by the executable or operating system.
Mini Project
Description
Build a small two-file temperature utility. The project demonstrates a realistic separation between a public interface in a header, implementation in a source file, and an application that links against that implementation.
Goal
Create an executable that converts Celsius temperatures to Fahrenheit using a function defined in a separate .cpp file.
Requirements
Create a header that declares celsiusToFahrenheit.
Implement celsiusToFahrenheit in a separate source file.
Include the header from both the implementation file and main.cpp.
Call the function from main.cpp and print the result.
Build all source files into one executable.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.