Contents

Chapter 20

Splitting a Program Across Files

Headers, include guards, a Makefile, and the linker error this book is named after.

Chapter 14’s contact book is a few hundred lines and does three separate jobs: describes what a contact is, does things to contacts, and runs a menu. Chapter 19’s card game is heading the same way.

One file is fine until it isn’t. Scrolling to find a function, and rebuilding every line because you changed one, are both symptoms of the same thing.

Two kinds of line

Splitting a program rests on a distinction you’ve been using without naming.

A declaration says a thing exists and what shape it is:

void print_contact(const Contact& c);

A definition says what it actually does:

void print_contact(const Contact& c) {
    std::cout << c.name << "  " << c.phone << '\n';
}

Chapter 9 told you to define functions above main because the compiler reads top to bottom, and promised chapter 20 would show you the other way. Here it is: the compiler needs the declaration before the call. The definition can be anywhere at all, including a different file.

That single fact is what makes splitting possible.

Header and source

The convention is two files per component:

// contact.h
#include <string>

struct Contact {
    std::string name;
    std::string phone;
};

void print_contact(const Contact& c);
Contact read_contact();
// contact.cpp
#include "contact.h"
#include <iostream>

void print_contact(const Contact& c) {
    std::cout << c.name << "  " << c.phone << '\n';
}

Contact read_contact() {
    Contact c;
    std::getline(std::cin, c.name);
    std::getline(std::cin, c.phone);
    return c;
}

Anyone who wants contacts writes #include "contact.h" and gets the declarations.

Note the quotes. #include <iostream> with angle brackets means “a library header, look where the compiler keeps those”. #include "contact.h" with quotes means “one of mine, look here first”. Get them the wrong way round and the compiler usually still finds it, which is exactly the kind of thing that works on your machine and not on someone else’s.

Include guards

If #include is copy-and-paste, what happens when a file gets included twice, say main.cpp includes both contact.h and book.h, and book.h also includes contact.h?

error: redefinition of 'Contact'
note: './contact.h' included multiple times, additional include site here

Two copies of the struct, and the compiler objects. The fix is a wrapper every header gets:

#ifndef CONTACT_H
#define CONTACT_H

// ... everything ...

#endif

Read it as: if CONTACT_H has not been defined, define it, and use everything down to the #endif. The first include defines the name and takes the contents. The second finds it already defined and skips straight to the end.

The name is arbitrary but must be unique across the whole program, and CONTACT_H matching contact.h is the universal convention, so follow it.

You will also see this:

#pragma once

One line, same effect, supported by every compiler you’re likely to meet, but not in the standard. Guards are what you’ll find in existing code, so learn to read them; use whichever your project already uses.

Compiling more than one file

Hand the compiler both:

g++ -std=c++17 -Wall -Wextra -g main.cpp contact.cpp -o book

The same command you’ve typed since chapter 0, with an extra filename. Leave one out and you get something new:

Undefined symbols for architecture arm64:
  "read_contact()", referenced from:
      _main in main.o
  "print_contact(Contact const&)", referenced from:
      _main in main.o
ld: symbol(s) not found for architecture arm64

This is not a compile error. main.cpp compiled perfectly. It had the declarations from the header, so it knew the calls were legitimate. The failure comes later, from the linker, whose job is to match every call to an actual definition and which could not find these two.

A Makefile

Typing that command with three files in it gets old, and it recompiles everything every time. make fixes both.

CXXFLAGS = -std=c++17 -Wall -Wextra -g

book: main.o contact.o
	g++ $(CXXFLAGS) main.o contact.o -o book

main.o: main.cpp contact.h
	g++ $(CXXFLAGS) -c main.cpp

contact.o: contact.cpp contact.h
	g++ $(CXXFLAGS) -c contact.cpp

clean:
	rm -f book main.o contact.o

Each block is a rule: a thing to build, then after the colon the things it depends on, then the command. make rebuilds a target only if something it depends on is newer.

make
g++ -std=c++17 -Wall -Wextra -g -c main.cpp
g++ -std=c++17 -Wall -Wextra -g -c contact.cpp
g++ -std=c++17 -Wall -Wextra -g main.o contact.o -o book

Run it again with nothing changed and it says book' is up to date. Change only contact.cpp and it recompiles that one file and relinks. Change contact.h and it rebuilds both, because both rules list the header as a dependency, which is why they list it.

-c is the new flag: compile this file to a .o and stop, don’t try to link. That’s the step that was invisible when you passed one file.

Before you move on

Every one of those is worth doing by hand once. Chapter 21 builds a program across several files and assumes this is no longer interesting.

Project

Split the contact book

Roughly 60 minutes

Chapter 14’s contact book, in three files. No new behaviour. The finished program does exactly what it did, and that is the point. You should be able to run it afterwards and see no difference at all.

The split:

FileHolds
contact.hthe Contact struct, and declarations of every function on it
contact.cppthe bodies: print_contact, read_contact, by_name
main.cppthe vector, the menu loop, and nothing else

Do it in this order, because it fails less:

  1. Make contact.h with the guard and the struct. Include it from your existing single file, delete the struct from there, and check it still builds. One change, verified.
  2. Move one function to contact.cpp, leaving its declaration in the header. Build with both files. Now you’ve seen the whole mechanism work on the smallest possible example.
  3. Move the rest.
  4. Add the Makefile last, once everything works. Debugging a Makefile and a split at the same time is twice the difficulty for no reason.

What goes in the header is a real decision. read_contact is used by main, so it must be declared there. A helper that only contact.cpp uses should stay in contact.cpp and out of the header entirely. A header is a public interface, and the same instinct as chapter 18’s private: applies.

Stretch one, a third component. Pull the menu-printing and choice-reading into menu.h and menu.cpp. Now main.cpp is about twenty lines that name what the program does, and the Makefile grows a rule.

Stretch two, break it deliberately. Change print_contact’s parameter from const Contact& to Contact in the .cpp only, leaving the header alone. Predict what happens before you build. The answer is one of the three linker causes above, and seeing it once is worth more than reading about it.