Headers and multiple files organize a C++ program by separating what a piece of code *offers* from what it *does*. A header declares the interface, a source file implements it, and the compiler stitches them together. Once you can split a program across three files and rebuild it with one command, you have the structure every larger C++ project uses.
What a header contains
A header is a text file that a source file includes. It typically contains function declarations, class definitions, constants and template definitions. It does not normally contain function bodies, because those belong in a source file so they are compiled once.
Here is the interface for a bakery pricing module.
// pricing.hpp
#pragma once
#include
namespace bakery {
struct LineItem {
std::string name;
double unit_price;
int quantity;
};
double line_total(const LineItem& item);
double subtotal(const LineItem items[], int count);
double apply_discount(double total, double percent = 10.0);
} // namespace bakeryThree things are worth noting. The #pragma once line tells the compiler to include the file only once, no matter how many files ask for it. The declarations carry no bodies. And the namespace keeps the names from colliding with anything else in the program.
What a source file contains
The source file implements the declarations. It includes its own header first, so that if the declaration and the definition ever drift apart the compiler catches the mismatch immediately.
// pricing.cpp
#include "pricing.hpp"
namespace bakery {
double line_total(const LineItem& item) {
return item.unit_price * item.quantity;
}
double subtotal(const LineItem items[], int count) {
double sum = 0.0;
for (int i = 0; i < count; ++i) {
sum += line_total(items[i]);
}
return sum;
}
double apply_discount(double total, double percent) {
return total - total * percent / 100.0;
}
} // namespace bakeryThe default value of 10.0 for percent appears in the header only, exactly as it should.
The two kinds of include
There are two forms of #include, and the difference is where the compiler looks.
#include <iostream>— angle brackets. The compiler searches its system include directories, which is where the standard library lives.#include "pricing.hpp"— quotes. The compiler searches the directory of the current file first, then the system directories.
The rule of thumb: angle brackets for library headers, quotes for your own.
Putting it together
The main file uses the module and knows nothing about how it is implemented.
// main.cpp
#include
#include "pricing.hpp"
int main() {
bakery::LineItem items[2] = {
{"sourdough", 4.50, 3},
{"rye", 5.25, 2}
};
double before = bakery::subtotal(items, 2);
double after = bakery::apply_discount(before, 15.0);
std::cout << "subtotal: " << before << '\n';
std::cout << "after 15%: " << after << '\n';
return 0;
}Here is the same program as one file, which is what you write while you are still working out the design. The declarations sit at the top, the definitions follow, and main comes last.
#include
#include
namespace bakery {
struct LineItem {
std::string name;
double unit_price;
int quantity;
};
double line_total(const LineItem& item);
double subtotal(const LineItem items[], int count);
double apply_discount(double total, double percent = 10.0);
double line_total(const LineItem& item) {
return item.unit_price * item.quantity;
}
double subtotal(const LineItem items[], int count) {
double sum = 0.0;
for (int i = 0; i < count; ++i) {
sum += line_total(items[i]);
}
return sum;
}
double apply_discount(double total, double percent) {
return total - total * percent / 100.0;
}
} // namespace bakery
int main() {
bakery::LineItem items[2] = {
{"sourdough", 4.50, 3},
{"rye", 5.25, 2}
};
double before = bakery::subtotal(items, 2);
double after = bakery::apply_discount(before, 15.0);
std::cout << "subtotal: " << before << '\n';
std::cout << "after 15%: " << after << '\n';
return 0;
}The output shows the subtotal and the discounted total:
subtotal: 24
after 15%: 20.4The three files together compile and link into one program:
# compile each translation unit, then link them
g++ -std=c++17 -Wall -Wextra -c pricing.cpp -o pricing.o
g++ -std=c++17 -Wall -Wextra -c main.cpp -o main.o
g++ pricing.o main.o -o bakeryEach source file becomes an object file containing machine code plus a table of the symbols it defines and the symbols it needs. The linker combines the object files and resolves those symbols. That is why a missing definition produces an "" error at link time rather than a compile error.undefined reference
For a quick test you can let the compiler do all three steps at once by listing the source files together.
g++ -std=c++17 -Wall -Wextra main.cpp pricing.cpp -o bakery
./bakeryThe single-header style
Small projects sometimes keep everything in one header and include it wherever it is needed. The functions are marked inline so that two translation units including the same header do not produce duplicate symbols at link time.
#include
#include
namespace bakery {
inline double line_total(double unit_price, int quantity) {
return unit_price * quantity;
}
inline double apply_discount(double total, double percent = 10.0) {
return total - total * percent / 100.0;
}
} // namespace bakery
int main() {
double total = bakery::line_total(4.50, 3);
std::cout << "subtotal: " << total << '\n';
std::cout << "after 15%: " << bakery::apply_discount(total, 15.0) << '\n';
return 0;
}The output shows the two calculations:
subtotal: 13.5
after 15%: 11.475This style is convenient for a utility library that is shared as a single file. It stops being convenient as soon as the implementation grows, because every consumer recompiles whenever anything changes, and that is the point at which the separate header and source file layout pays for itself.
Two files, one program
The split version of the same pricing module has a header that declares and a source file that defines. Everybody who needs the interface includes the header and never sees the implementation.
#include
#include
namespace bakery {
struct LineItem {
std::string name;
double unit_price;
int quantity;
};
double line_total(const LineItem& item) {
return item.unit_price * item.quantity;
}
} // namespace bakery
int main() {
bakery::LineItem item{"sourdough", 4.50, 3};
std::cout << "line total: " << bakery::line_total(item) << "\n";
return 0;
}The output shows the line total computed from the two members:
line total: 13.5Include guards
The traditional way to prevent a header being included twice is an include guard, which relies on the preprocessor's macro rules.
// pricing.hpp
#ifndef BAKERY_PRICING_HPP
#define BAKERY_PRICING_HPP
// declarations go here
#endif // BAKERY_PRICING_HPPThe #ifndef asks whether the macro is not yet defined. If so, the header is processed and the macro is defined, which makes every later include skip the file. #pragma once does the same thing in one line and is supported by every modern compiler, so it is the form most projects use today.
Common mistakes
- Forgetting the include guard. The second inclusion produces "redefinition of ..." errors that are confusing the first time you meet them.
- Putting function bodies in a header. The code is compiled into every translation unit that includes the header, producing duplicate symbol errors at link time.
- Declaring in one place and defining in another incorrectly. If the header declares a function and the source file never defines it, the linker reports an
.undefinedreference - Including a header you do not need. It slows the build and can introduce name clashes.
- Using a relative path that only works from one directory. Keep includes relative to the file containing them, or add an include path to the compiler command.
Quick reference
- A header declares; a source file defines.
- Put
#pragma onceat the top of every header. - Include your own header first in the matching source file.
- Use angle brackets for the standard library and quotes for your own files.
- Each source file compiles to an object file; the linker combines them.
- A missing definition shows up as an
at link time.undefinedreference
Try it yourself: split your order-calculator program into three files — a header with the declarations, a source file with the implementations, and a main file. Compile it with the one-line command and then with the separate compile-and-link commands, and confirm both produce the same binary. Finally, deliberately delete one function definition and read the linker error carefully, because you will meet it again.