Working with Multiple Files
As our projects grow in size, it becomes beneficial to spread our code over multiple files. This has several benefits:
It logically groups functions by task, making it easier to understand
It allows for easy code reuse.
Smaller files compile quicker than a larger file, and we only need to compile a file if the code changes.
In version control, there is less chance of conflict when different people edit at once.
Generally speaking, we’ll talk about two types of files:
Header files : these contain the forward declarations of the functions we are going to use, as well as any types (
structs
, etc.) that we define.As we’ll see shortly, we can also put entire (usually short) functions in headers so the compiler can inline the code, resulting in faster performance.
A header file will be
#include
-ed into source files.Source files : these implement the functions defined in the header (the function definitions corresponding to the forward declarations), and of course the
main()
function.
Let’s consider our example of sorting planets.
We’ll start with a header file defining our struct
and the operator
declaration:
#ifndef PLANET_H
#define PLANET_H
#include <string>
#include <iostream>
struct Planet
{
std::string name{};
double a{}; // semi-major axis
double e{}; // eccentricity
};
std::ostream& operator<< (std::ostream& os, const Planet& p);
#endif
A few things to note:
At the very top is a header guard.
The is used to ensure that we don’t include a header twice in any program unit. As with the
#include
directives we’ve been using, the#ifndef
and#define
directives are processed by the C preprocessor.We only have the forward declaration for
operator<<
and not the definition of the function itself here.
Note
Header files usually have the extension .H
or .hpp
. I’ll use .H
throughout this course.
Now we’ll create a source file that implements the <<
operator:
#include <iostream>
#include <iomanip>
#include "planet.H"
std::ostream& operator<< (std::ostream& os, const Planet& p) {
os << std::setw(12) << p.name << " : ("
<< std::setw(8) << p.a << " , "
<< std::setw(8) << p.e << ")";
return os;
}
Here, note the following:
We use
"
in the#include
forplanets.H
:#include "planets.H"
This gives us the forward declaration we need for this function.
For the standard C++ headers we use
<>
in the#include
.These are treated differently by the preprocessor. For
"
, the compiler will first look in the local directory and a list of include paths you provide, while with<>
the compiler will look in the default system locations for the headers.
Finally, we’ll put the main()
in a third file:
#include <iostream>
#include <vector>
#include <algorithm>
#include "planet.H"
int main() {
std::vector<Planet> planets {{"Mercury", 0.3871, 0.2056},
{"Venus", 0.7233, 0.0068},
{"Earth", 1.0000, 0.0167},
{"Mars", 1.5237, 0.0934},
{"Jupiter", 5.2029, 0.0484},
{"Saturn", 9.5370, 0.0539},
{"Uranus", 19.189, 0.0473},
{"Neptune", 30.070, 0.0086}};
std::sort(planets.begin(), planets.end(),
[] (const Planet& a, const Planet& b) {return a.e < b.e;});
for (auto p : planets) {
std::cout << p << std::endl;
}
}
Compiling
To produce an executable, we need to compile each of the .cpp
files first
and then link them all together. Here are the steps:
g++ -c planet.cpp
g++ -c planet_sort_split.cpp
g++ -o planet_sort_split planet.o planet_sort_split.o
The first two commands are the compilation step. The take the source file
and produce an object file (e.g., planet.cpp
→ planet.o
).
The final command is the link step—notice that we use the same command,
g++
, as the compiler and linker. In this case, we tell it the name
for our executable and then give it the list of all object files that it
needs to link together.
Notice that we don’t explicitly do anything with the header file,
planet.H
—the preprocessor includes this as part of the
compilation step.
try it…
What happens if you don’t pass planet.o
to the link step?
One Definition Rule
An important concept when working with multiple files is the One Definition Rule (ODR).
A consequence of the ODR is that if you put a function entirely in a header, then you need to make it inline.