Declarations, definitions, and header files
Split a C++ program into understandable files, distinguish declarations from definitions, and understand the One Definition Rule at a practical level.
Learning objectives
- Distinguish a declaration from a definition.
- Explain the role of header and source files.
- Compile and link a small multi-file program.
- Describe header guards and the One Definition Rule (ODR).
Prerequisites: function declarations and definitions, source files, and basic compiler use. Review Functions and Compiler toolchain first.
Declaration versus definition
A declaration tells the compiler that a name and its type exist. A definition provides the entity itself, such as a function body or the storage for a variable. A function prototype is a declaration; a function body is a definition.
int multiply(int left, int right); // declaration
int multiply(int left, int right) { // definition
return left * right;
}The declaration lets code call the function before the compiler has seen its definition. The linker later connects the call to the compiled definition.
Why split files?
As programs grow, putting every function in one file becomes difficult to maintain. A common layout is:
project/ main.cpp math_utils.h math_utils.cpp
The header (.h or .hpp) contains declarations that other source files need. The source file (.cpp) contains the function definitions. The main file includes the header and calls the function.
A small working example
math_utils.h
#ifndef MATH_UTILS_H #define MATH_UTILS_H int multiply(int left, int right); #endif
math_utils.cpp
#include "math_utils.h"
int multiply(int left, int right) {
return left * right;
}main.cpp
#include <iostream>
#include "math_utils.h"
int main() {
std::cout << multiply(6, 7) << '\n';
}Compile both source files together, for example with GCC or Clang:
g++ -std=c++17 -Wall -Wextra -Wpedantic main.cpp math_utils.cpp -o app ./app
Expected output: 42. On Windows with MinGW, run app.exe. In an IDE, add both .cpp files to the same project; do not compile the header as a separate source file.
What happens during the build?
- The preprocessor handles
#includeby making header declarations available to the source file. - The compiler compiles each
.cppsource file as a translation unit and produces object code. - The linker combines object files and libraries, resolving references such as the call to
multiply.
A translation unit is, roughly, one source file after preprocessing. Each .cpp file is compiled independently, which is why declarations must be visible where a name is used.
Header guards and duplicate inclusion
The #ifndef, #define, and #endif lines form a header guard. They prevent the same header's contents from being processed more than once within one translation unit. Many compilers also support #pragma once, but include guards are the portable traditional form.
The One Definition Rule (ODR)
The ODR is a set of rules that limit how many definitions an entity may have across a program. A common beginner-safe design is: declare an ordinary non-inline function in a header and define it once in one .cpp file. If you put the same non-inline function definition in a header included by multiple source files, you can create multiple definitions and a linker error.
Some entities, including appropriately defined templates and inline functions, are designed to have definitions in headers under specific ODR rules. Learn those cases separately; do not mark every function inline just to silence a linker error.
Common mistakes
- Declaring a function in a header but forgetting to compile/link its source definition.
- Including a header without include guards and causing repeated declarations or definitions.
- Defining a non-inline ordinary function in a header included by multiple source files.
- Using angle brackets for your own project header; use quotes for local headers such as
#include "math_utils.h". - Trying to compile a header by itself instead of compiling the relevant source files.
Practice exercises
- Write a declaration for
double area(double radius)and a definition that returns pi times radius squared. - Which file should contain the function body in the three-file layout above?
- What is the purpose of the
#ifndefheader guard? - Predict the likely problem if the same non-inline function body is placed in a header included by
main.cppandhelpers.cpp.
Solutions
Show exercise solutions
- Declaration:
double area(double radius);. Definition:double area(double radius) { return 3.141592653589793 * radius * radius; }. - Put the body in
math_utils.cpp; put the declaration inmath_utils.h. - It ensures the header's contents are included at most once per translation unit, avoiding repeated processing.
- Each source file may emit a definition, so the linker can report a multiple-definition error. Keep one definition in a single source file or use a valid inline/template header design.
Summary
Headers publish declarations; source files commonly hold definitions. Each source file is compiled separately, then the linker combines the results. Header guards prevent repeated inclusion in a translation unit. Follow ODR rules to avoid duplicate definitions.