C++ • Functions and Program Structure

#include & Declarations/Definitions — Use #include correctly and distinguish declarations vs definitions (conceptual)

Master the preprocessor's role in separate compilation and understand why declarations and definitions are fundamentally different.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

Historical Context & Motivation

The idea of composing programs from separately compiled modules predates C++ by decades. In early computing, programmers wrote monolithic programs where every function and variable lived in a single file. As software grew in scale, this approach became untenable — a change to one function required recompiling the entire program, and name collisions proliferated. The C preprocessor was introduced to solve this problem by allowing textual inclusion of shared interfaces, and C++ inherited and extended this mechanism. Understanding this history clarifies why #include works the way it does — as a simple but powerful text-substitution tool that predates the compiler itself.

1972 • Birth of C and the Preprocessor

Dennis Ritchie develops C at Bell Labs. The preprocessor — with #include and #define — is introduced to support separate compilation of the Unix kernel across multiple source files.

1978 • K&R C Formalizes Header Conventions

Kernighan and Ritchie's "The C Programming Language" establishes the convention of placing declarations in .h header files and definitions in .c source files, creating the declaration/definition split that persists today.

1985 • C++ Extends the Model

Bjarne Stroustrup releases C++ with classes, templates, and inline functions — all of which add nuance to what belongs in a header versus a source file. The One Definition Rule (ODR) becomes essential.

1998–2017 • Standardization Refines the Rules

The ISO C++ standards (C++98 through C++17) formalize include guards, inline variables, and the ODR. Header-only libraries become popular, blurring the traditional header/source boundary.

2020 • C++20 Modules Arrive

C++20 introduces modules as a modern alternative to #include, aiming to eliminate textual inclusion's pitfalls — but understanding the preprocessor model remains essential for existing codebases.

The central question this lesson addresses is deceptively simple: how does the compiler know about a function or variable that is defined in a different file? The answer lies in the interplay between the preprocessor's textual inclusion mechanism and the language's formal distinction between telling the compiler that something exists (a declaration) versus actually creating it (a definition). Mastering this distinction is the key to writing well-structured, multi-file C++ programs.

SECTION 2

Core Principles: Declarations, Definitions, and #include

Before examining #include in detail, we need to establish three foundational concepts that govern how C++ programs are assembled from multiple files. These concepts operate at different stages of the build process — the preprocessor runs before the compiler, and the linker runs after — so clarity about each stage prevents a wide class of common errors.

1

Declaration

A declaration introduces a name and its type to the compiler without allocating storage or providing an implementation. It says, "this entity exists somewhere." Example: int square(int x); declares a function but does not define its body.
2

Definition

A definition provides the complete implementation or allocates storage. It tells the compiler, "here is the actual entity." Example: int square(int x) { return x * x; } defines the function body. Every definition is also a declaration, but not vice versa.
3

#include Directive

The preprocessor directive #include performs textual substitution: it replaces itself with the entire contents of the specified file before the compiler sees the code. It has no semantic understanding of C++ — it is purely a text operation.
4

The One Definition Rule (ODR)

The ODR states that each non-inline function and non-inline variable must be defined exactly once across all translation units. Violations result in linker errors ("multiple definition of...") or, worse, undefined behavior for certain entity types.
5

Translation Unit

A translation unit is a single .cpp source file after all #include directives have been expanded. The compiler processes each translation unit independently, producing one object file per unit.
✦

KEY TAKEAWAY

Think of a declaration as a menu description — it tells you that a dish exists and what ingredients it uses, but it doesn't cook the dish. A definition is the actual recipe and preparation. The #include directive is like photocopying a page from a shared recipe book and pasting it into your own notebook. The copier doesn't understand cooking; it just duplicates text. If you accidentally photocopy a full recipe (a definition) into multiple notebooks, you'll have conflicting copies — that's an ODR violation.

SECTION 3

Visual Explanation: The Build Pipeline

The following diagram illustrates how #include fits into the C++ build pipeline. Notice that the preprocessor operates on raw text before the compiler ever parses C++ syntax. Two source files — main.cpp and math.cpp — both include the same header math.h, which contains only declarations. Each source file becomes an independent translation unit, compiled into a separate object file. The linker then resolves all cross-references by connecting each declaration to its matching definition.

math.h(declarations only)main.cpp#include "math.h"math.cpp#include "math.h" + definitionstext copytext copyPreprocessor → TU 1Preprocessor → TU 2Compiler → main.oCompiler → math.oLinker → executableLinker matches declarations in main.o to definitions in math.o
The build pipeline: the preprocessor copies header text into each source file, the compiler processes each translation unit independently, and the linker resolves cross-file references.

Observe that the header file math.h is physically copied into both source files by the preprocessor (shown by the dashed violet arrows). This is not symbolic linking or referencing — the preprocessor literally pastes the entire text of math.h into each .cpp file. Because the header contains only declarations, this duplication is safe. If the header contained a function definition, however, both object files would contain that definition, and the linker would report a multiple definition error — a direct ODR violation.

SECTION 4

How #include Actually Works

Angle Brackets vs. Double Quotes

The #include directive comes in two forms, and the distinction controls where the preprocessor searches for the file. #include <iostream> uses angle brackets, which instructs the preprocessor to search the system include paths — directories where the standard library headers and other system-level headers reside. #include "myheader.h" uses double quotes, which causes the preprocessor to first search the directory of the current source file, then fall back to the system paths. The convention is to use angle brackets for standard and third-party library headers, and double quotes for project-local headers.

Include Guards and #pragma once

Because #include is textual inclusion, the same header can be pasted into a translation unit multiple times if the inclusion graph has diamonds — for instance, if a.h includes c.h and b.h also includes c.h, then a source file that includes both a.h and b.h will get two copies of c.h. This causes redefinition errors for classes and structs. The standard solution is the include guard idiom, using #ifndef / #define / #endif to ensure the header body is included at most once per translation unit. Alternatively, most compilers support #pragma once as a non-standard but widely portable shorthand.

💡

Include Guard Pattern

#ifndef MATH_H #define MATH_H int square(int x); // declaration #endif // MATH_H The first time the preprocessor encounters this header, MATH_H is undefined, so the guard passes and the macro is defined. Any subsequent inclusion finds MATH_H already defined and skips the body entirely.

What Happens During Preprocessing

You can observe the preprocessor's output directly by passing the -E flag to GCC or Clang (e.g., g++ -E main.cpp). The output is the complete translation unit — often thousands of lines when standard headers are included — with all #include directives replaced by the included text, all macros expanded, and all conditional compilation blocks resolved. This expanded file is what the compiler actually parses. Understanding this transformation demystifies many common errors: if a header is missing, the preprocessor fails, not the compiler; if a symbol is undeclared, the relevant header was likely not included.

SECTION 5

Declarations vs Definitions: A Detailed Taxonomy

The declaration/definition distinction applies to every kind of named entity in C++: functions, variables, classes, templates, and more. The rules vary by entity type, and getting them wrong leads to either compiler errors ("undeclared identifier") or linker errors ("undefined reference" or "multiple definition"). The following table provides a comprehensive reference for the most common entity types encountered in functions and program structure.

EntityDeclaration (no storage / no body)Definition (allocates storage / provides body)
Functionint square(int x);int square(int x) { return x*x; }
Variableextern int count;int count = 0;
Classclass Vec; (forward decl)class Vec { int x, y; };
Inline functionSame as definition (permitted in headers)inline int sq(int x) { return x*x; }
TemplateSame as definition (must be in header)template<class T> T max(T a, T b) { ... }

Declaration vs definition for common C++ entities

Declaration vs Definition: What Goes WhereHeader File (math.h)Shared across translation units#ifndef MATH_H#define MATH_Hint square(int x); // declextern int globalCount; // declclass Vec; // forward decl#endif // MATH_H✓ Can be included many timesNo storage allocatedNo function bodiesSource File (math.cpp)Compiled once into math.o#include "math.h"int square(int x) { return x * x; // def}int globalCount = 0; // def✓ Compiled exactly onceStorage allocated hereFunction bodies live hereViolet = declarations (header) · Cyan = definitions (source)
Side-by-side comparison of a header file containing only declarations (left, violet) and a source file containing the corresponding definitions (right, cyan). Note the include guard pattern at the top of the header.
ℹ

The extern Keyword

For variables, the keyword extern is critical: extern int count; is a declaration (no storage allocated), while int count; at file scope is a definition. For functions, the distinction is syntactic: a semicolon after the parameter list makes it a declaration; a brace-enclosed body makes it a definition. No keyword is needed.

SECTION 6

Worked Example: Structuring a Multi-File Program

Let us walk through the complete process of creating a two-file C++ program with a shared header. We will write a small geometry library that provides a function to compute the area of a circle, then call it from a separate main.cpp file.

Building a Multi-File Geometry Program

  1. 1

    Step 1 — Write the Header (geometry.h)

    We create a header file that contains only the function declaration and an include guard. This header will be shared between the implementation file and any file that needs to call circleArea. #ifndef GEOMETRY_H #define GEOMETRY_H double circleArea(double radius); #endif
    Header contains only a declaration — safe to include in multiple translation units.
  2. 2

    Step 2 — Write the Source File (geometry.cpp)

    The source file includes the header to ensure its definition matches the declaration, then provides the function body. #include "geometry.h" #include <cmath> double circleArea(double radius) { return M_PI * radius * radius; }
    The definition lives in exactly one source file, satisfying the ODR.
  3. 3

    Step 3 — Write main.cpp

    The main file includes the same header to obtain the declaration, then calls the function. It does not need to know the implementation. #include "geometry.h" #include <iostream> int main() { double r = 5.0; std::cout << "Area: " << circleArea(r) << std::endl; return 0; }
    main.cpp sees the declaration from the header; the linker will connect the call to the definition in geometry.o.
  4. 4

    Step 4 — Compile and Link

    We compile each source file separately, then link them: g++ -c geometry.cpp -o geometry.o g++ -c main.cpp -o main.o g++ geometry.o main.o -o program The -c flag tells the compiler to produce an object file without linking. Each .o file contains compiled machine code, with unresolved references to functions defined elsewhere.
    The linker resolves circleArea's reference in main.o to its definition in geometry.o, producing the final executable.
  5. 5

    Step 5 — Verify: What If We Put the Definition in the Header?

    If we mistakenly placed the full function body in geometry.h without the inline keyword, both geometry.o and main.o would contain a definition of circleArea. The linker would produce an error: multiple definition of `circleArea(double)`
    ODR violation: non-inline definitions in headers cause linker errors when included in multiple translation units.

SECTION 7

Common Pitfalls and Best Practices

Even experienced C++ developers occasionally misuse #include or confuse declarations with definitions. The following table contrasts common mistakes with their corrections, providing a practical reference for writing robust multi-file programs.

PitfallSymptomCorrect Practice
Defining a non-inline function in a headerLinker error: "multiple definition of..."Put only the declaration in the header; define in one .cpp file
Defining a variable at file scope in a header (no extern)Linker error: "multiple definition of..."Use extern in the header and define in exactly one .cpp file
Missing include guardCompiler error: "redefinition of class..."Always use #ifndef/#define/#endif or #pragma once
Forgetting to include a headerCompiler error: "undeclared identifier" or "use of undeclared..."Include the header that declares the entity you use
Including .cpp files instead of .h filesLinker error or duplicate symbolsNever #include a source file; include only headers

Common #include and declaration/definition pitfalls

✦

KEY TAKEAWAY

The golden rule of header design is: headers should contain declarations; source files should contain definitions. Exceptions exist for inline functions, templates, and constexpr variables, all of which the standard explicitly permits in headers because the compiler needs their full definition at every point of use. When in doubt, ask: "If two source files include this header, will the linker see two copies of the same definition?" If the answer is yes, refactor.

SECTION 8

Connection to Advanced Concepts: Modules, Linkage, and ODR

The #include model has served C and C++ for over fifty years, but it carries inherent limitations: textual inclusion is slow (the compiler re-parses the same headers in every translation unit), macro pollution can cause subtle bugs, and include order can change program semantics. These issues motivate several advanced features in modern C++ that build upon, or replace, the concepts covered in this lesson.

Classic Model (#include)Modern Alternative (C++20 Modules)
Textual copy-paste of header contentsBinary module interface; parsed once, imported many times
Include guards needed to prevent duplicate inclusionNo guards needed; import is idempotent by design
Macros leak across headers (macro pollution)Macros do not leak across module boundaries
Include order can matter (e.g., macro redefinition)Import order is irrelevant
Declarations and definitions manually separatedModule interface/implementation partitions separate automatically

#include vs C++20 modules

Beyond modules, the concepts of internal and external linkage refine the declaration/definition story. A name with external linkage (the default for functions and non-const global variables) is visible across translation units — this is what makes cross-file function calls possible. A name with internal linkage (achieved via static or an anonymous namespace) is confined to its translation unit, effectively giving each .cpp file its own private copy. Understanding linkage is the natural next step after mastering declarations and definitions, and it is essential for controlling symbol visibility in large projects.

📝

Looking Ahead

While C++20 modules are the future, the vast majority of production C++ code still uses the header-based model. Proficiency with #include, declarations, and definitions is therefore not legacy knowledge — it is a prerequisite for understanding both existing codebases and the problems that modules are designed to solve.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why #include is described as "textual inclusion" rather than "importing." What practical consequences does this distinction have for the C++ build process?
Show answer
The #include directive is handled by the preprocessor, which operates purely on text before the compiler ever sees C++ syntax. It physically replaces the #include line with the entire contents of the referenced file, character for character. This means macros defined in the included file affect subsequent code, include order can matter, and the same header can be pasted in multiple times if include guards are absent. An "import" mechanism (like C++20 modules) would instead process the file once and expose only its public interface, avoiding these issues.

Question 2: BASIC CALCULATION

Consider the following header file util.h: int add(int a, int b) { return a + b; } int counter = 0; This header is included in both main.cpp and helper.cpp. Identify all errors that will occur and explain how to fix each one.
Show answer
There are three problems. First, there is no include guard, so if this header is included more than once in the same translation unit (e.g., via a diamond dependency), the compiler will report redefinition errors. Second, add is defined (not just declared) in the header: both translation units will contain a definition, causing a linker error ("multiple definition of add"). Fix: move the body to a .cpp file and leave only int add(int a, int b); in the header, or mark the function inline. Third, int counter = 0; is a definition (it allocates storage), causing the same multiple-definition error. Fix: use extern int counter; in the header and define int counter = 0; in exactly one source file.

Question 3: INTERMEDIATE

A project has three files: a.h (includes c.h), b.h (includes c.h), and main.cpp (includes both a.h and b.h). Header c.h defines a struct Point but has no include guard. Describe exactly what error occurs and at which stage of the build process (preprocessing, compilation, or linking). Then provide the fix.
Show answer
After preprocessing, the translation unit for main.cpp will contain two copies of the struct Point definition — one pasted in via a.h → c.h and another via b.h → c.h. The error occurs at the compilation stage (not preprocessing, not linking), because the compiler sees two type definitions with the same name in the same translation unit — a direct violation of the ODR within a single TU, which is always a compiler error. The fix is to add an include guard to c.h: #ifndef C_H / #define C_H / ... / #endif. This ensures the preprocessor includes the body of c.h at most once per translation unit.

Question 4: APPLIED

You are designing a physics simulation library with files vector3.h, vector3.cpp, particle.h, particle.cpp, and simulation.cpp. The Particle class has a Vector3 position member. Describe exactly what declarations and definitions belong in each file, and explain why particle.h must #include "vector3.h" rather than using a forward declaration of Vector3.
Show answer
vector3.h should contain the full class definition of Vector3 (with member declarations), plus declarations of any non-inline member functions or free functions. vector3.cpp includes vector3.h and provides the member function definitions. particle.h must #include "vector3.h" because Particle has a Vector3 member by value — the compiler needs to know Vector3's size to compute sizeof(Particle). A forward declaration only tells the compiler the name exists, not its layout, so it suffices only for pointers or references. particle.cpp includes particle.h and provides Particle's member function definitions. simulation.cpp includes particle.h (which transitively includes vector3.h) and contains the simulation logic.

Question 5: CRITICAL THINKING

Templates and inline functions are permitted to be defined in header files, while ordinary functions and variables are not. Analyze why the One Definition Rule treats these categories differently. What property of templates and inline functions makes their presence in headers safe despite being included in multiple translation units?
Show answer
The key property is that templates and inline functions are exempt from the strict "exactly one definition" rule across translation units — instead, the ODR permits multiple identical definitions of these entities, provided every translation unit that uses them sees an identical token sequence. The compiler and linker cooperate to collapse these duplicates: the linker uses COMDAT folding (on most platforms) to merge identical copies into a single definition in the final executable. Templates require this exemption because they must be instantiated at the point of use — the compiler needs the full template body to generate specialized machine code for each set of template arguments. Inline functions similarly need their body visible at every call site so the compiler has the option to inline them. Ordinary functions and variables, by contrast, are compiled once and accessed via their address, so having multiple copies would create ambiguity about which copy is the "real" one. This is why the ODR mandates exactly one definition for non-inline, non-template entities.

SUMMARY

Summary

Summary

The #include directive is a preprocessor mechanism that performs textual inclusion — it copy-pastes the contents of a specified file into the current source file before compilation begins. Declarations introduce a name and its type to the compiler without allocating storage (e.g., int f(int); or extern int x;), while definitions provide the implementation or allocate storage. The One Definition Rule (ODR) mandates that non-inline, non-template entities be defined exactly once across all translation units.

Best practice dictates that header files contain only declarations (plus inline functions, templates, and class definitions), protected by include guards, while source files contain definitions and are compiled exactly once. Use angle brackets for system/library headers and double quotes for project-local headers. These principles form the foundation of C++ program structure and remain essential even as C++20 modules begin to offer a modern alternative.

Varsity Tutors • C++ • #include & Declarations/Definitions