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:

Listing 41 planet.H
#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:

Listing 42 planet.cpp
#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 for planets.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:

Listing 43 planet_sort_split.cpp
#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.cppplanet.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.