Question

C++ Compilation and Linking Process Explained

cpplinkerc++-faq

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:

  1. Preprocessing: Handles directives such as #include, #define, and conditional compilation.
  2. Compilation: Parses C++ code, checks types, and translates each source file into lower-level code.
  3. Assembly: Converts assembly output into machine-code object files. Many compiler commands perform compilation and assembly together.
  4. 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.

Take Quiz

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:

  1. The preprocessor processes main.cpp. Its #include "greeting.h" is effectively replaced by the declaration void greet();.
  2. The compiler checks that calling greet() is valid because it knows the declared function signature.
  3. It creates an object file for main.cpp containing main and an unresolved reference to greet.
  4. The preprocessor and compiler process greeting.cpp. It includes the same declaration and then compiles the definition of greet.
  5. An object file for greeting.cpp is produced. It exports a symbol for greet.
  6. The linker combines both object files, matches the call from main with the definition from greeting.cpp, and writes app.
  7. When app runs, execution starts at main(), which calls the linked greet() function.

Real World Use Cases

  • Large applications: A project may have hundreds of .cpp files. 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.so on 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.
Take Quiz

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

TermMain jobTypical inputTypical output
PreprocessorExpands includes and processes macros/conditionsSource and headersPreprocessed source
CompilerChecks C++ and translates itOne translation unitAssembly or object code
AssemblerConverts assembly to machine codeAssemblyObject file
LinkerResolves symbols and combines binary piecesObject files and librariesExecutable or library
ItemContainsMain purpose
Header (.h, .hpp)Declarations, templates, inline definitionsShares an interface with other source files
Source file (.cpp)Function and class-member definitionsImplements program behavior
Object file (.o, .obj)Compiled machine code plus symbolsIntermediate file sent to linker
Static library (.a, .lib)Archive of object filesCode is copied into the final executable as needed
Shared library (.so, .dylib, .dll)Separately loadable compiled codeCode 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.

Take Quiz

Cheat Sheet

  • A translation unit is one .cpp file after its #include directives are processed.
  • Compile each translation unit independently.
  • Use headers for declarations shared by multiple source files.
  • Use .cpp files for ordinary function and variable definitions.
  • -c compiles 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 inline function definitions in headers.
  • Use #pragma once or include guards in headers.
  • Include a .cpp file's matching header in that .cpp file 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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more