Structuring source code

This web page shows how to structure your code to make it more modular and more reusable. The example code here implements the car/engine example we've discussed in class.

I start out with all the code in a single file -- clearly, not a very modular design. In the second version I break out the car and engine classes, but still have all of their code in header files. The final version is the most modular: the car and engine code is divided into interface (header) and implement (cpp). This is how I would like you to structure your code.

One big file

Here I put all of the code in a single cpp file: main.cpp.

There are several problems with this approach. I have to compile all the code every time I change anything. It would also be very hard for multiple people to work on the code, since we'd all be editing the same file. Finally, there is not a clean separation between the interfaces and implementations of the classes.

Separate files for each class

In this version I separated the car and engine classes from the main, but their interfaces and implementations are still combined in a single header file. Notice that main.cpp must include the two header files.

This version is much more modular, but still doesn't separate interface from implementation. I could not, for example, send someone the interface for the engine without also sending them the implementation (which might be secret!).

See the code here.

Separate interfaces

The final version shows the most modular design: separate headers for each class, each with its own separate cpp file for the implementation. Again, note how the files use #include to get the interfaces they need.

See the code here.

Automating compilation

Even for small projects it is often desirable to automate the process of compiling the code. You can automate this build process by writing a compile script or by designing a Makefile. The Makefile approach is more efficient, but also trickier to get right.

Compile scripts

The simplest way to do this is write a compile script: a program that consists of shell commands:

clang++ -c engine.cpp
clang++ -c car.cpp
clang++ -c main.cpp
clang++ engine.o car.o main.o -o simulator

You write this program just like any other and store it in a file (call it compile). You "run" this program (to build your C++ program) using the shell interpreter:

lab116a% bash compile
lab116a% ls simulator
simulator
lab116a% ./simulator
Speed is 0
Speed is 12
Speed is 36

You can also tell the system to treat "compile" as a program that you can run directly. Add the name of the interpreter to the top of the compile script:

#!/bin/bash
clang++ -c engine.cpp
clang++ -c car.cpp
clang++ -c main.cpp
clang++ engine.o car.o main.o -o simulator

To run the script:

lab116a% ./compile
lab116a% ls simulator
simulator

Makefiles

A Makefile is another kind of interpreted programming language, which you run using the "make" command. The main difference between a compile script and a Makefile is that a Makefile describes the specific dependences between the files, allowing make to selectively compile only the parts of the program affected by changes.

Each entry of a Makefile describes (a) how a file depends on other files, and (b) how to rebuild that file if it becomes out-of-date:

afile: otherfile yetanotherfile
        Commands to rebuild afile from otherfile and yetanotherfile

Make looks at the timestamp (the date/time of the most recent modifications) of each file on the right side of the colon. If any of them are newer than the file on the left, make runs the commands.

WARNING 1: The command part must have a tab character before it (not a sequence of spaces), or the Makefile will not work properly!

For our projects we will be dealing with two kinds of dependences: object files (.o files) depend on cpp and header file, and programs depend on object files. The key idea to tell make what it needs to do when parts of the source change. So, for example, what cpp files need to be recompiled if a particular header file changes?

Here is a complete Makefile for the example above:

simulator: main.o car.o engine.o
        clang++ main.o car.o engine.o -o simulator

main.o: main.cpp car.h engine.h
        clang++ -c main.cpp

car.o: car.cpp car.h engine.h
        clang++ -c car.cpp

engine.o: engine.cpp engine.h
        clang++ -c engine.cpp

WARNING 2: It is completely up to you to make sure that the Makefile you write properly represents the dependences between the files. If you lie to make, it will not build your code correctly!

What's nice about make is that it does only as much work as is necessary. For example, if I only modify car.h, it can figure out that engine.cpp does not need to be recompiled.

lab116a% make
clang++ -c main.cpp
clang++ -c car.cpp 
clang++ main.o car.o engine.o -o simulator

Back to Comp15.