How to Write Makefiles for C++ Programs

Prepared by Professor Spiegel

Kutztown University

When to use a makefile

In any realistic application, there will be many modules. This idea of separate compilation matches the concepts of information hiding and the separation of concerns.

Many .cpp files have a corresponding .h file. .cpp files that have companion .h files ordinarily contain:

An .h file corresponding to one of these .cpp  files might contain:

.h files provide "services" to other .cpp files, via inclusion. There are two types of .h files, those that come with C++, and those written by a programmer (like you).

For example, suppose you wrote a Date class. You would have a .h file which included the class declaration. Suppose you needed a Date object in a class. Then, you would #include "Date.h” (using quotes to tell the compiler to find the file in the logged directory, the directory of the source files).

Compiling a .cpp file

What does the compiler do when it sees #include of a .h file in a .cpp file? For example, suppose again you are compiling a class which contains the line #include "Date.h".

The preprocessor, which handles #include  (and all # directives) will "insert" the Date.h file into a copy of the .cpp source code. After the preprocessing phase has completed, the preprocessor returns the result of its work, in this case the .cpp file with the definitions in the .h file included, for submission to the compiler.

What information does the compiler have about Date when compiling the .cpp file? It only knows the things declared in the .h file. For a class definition, these would be the data members and the method signatures. A signature of a method consists of, in order, its return type, name and arguments, by type It likely doesn’t know how the methods are implemented, since this information is not ordinarily stored in a .h file.

This is one of the advantages of separating the definitions and implementations between .h  and .cpp files. A compiler need only see definitions to be able to compile. For example, when the compiler sees a function call, it need only have seen the function’s signature, which you first saw as a function prototype in CS I. The implementations in a .cpp file are an issue for the linker, which means that the compiler never accesses Date.cpp .

For example, a demo program named TestDate.cpp is an application that uses the Date class. The executable is named testdate. It is available in /usr/users/public/spiegel/cis136/C++/Date on pooh  or at URL: http://faculty.kutztown.edu/spiegel/cis136/C++/Date. You can choose either the Simple or Operator Overload demos.  Each example has TestDate, an application that uses the Date class. When you compile Date.cpp, you only create Date.o, the result of compiling Date.cpp. This is called an object file, and is ordinarily created in a .o file. Note that previously, in CS I, when you compiled a program consisting of a single file, a .o file was created, it just occurred in a temporary location, since the results of compilation were immediately submitted to the linker to ultimately create an executable (named a.out if you didn’t specify differently). The .o file was deleted at the end of g++. The object file doesn't have enough information to run by itself. It is not an executable. This should make sense, since Date is a class. You can’t make an executable program from just a class, as it doesn’t have any main() function to run.

Typically, when you have many .cpp files, you will end up creating a corresponding .o file. You need to prepare the results of compiling the .cpp file so that the linker can be specifically invoked with all the .o files. In g++ and cxx (and most UNIX C++ compilers), to get a .o file in the directory, compile with the -c option.

Here is a portion of a makefile for the testdate example (a complete version appears soon):

--------------------------------------------------------------------------------------------------------

Date.o: Date.cpp Date.h

        g++ -c Date.cpp

--------------------------------------------------------------------------------------------------------

Note the –c in the g++ command. The -c option can appear anywhere in the g++ command.

When the compiler sees this option, it only compiles its object and will create a .o file from the .cpp file. When compiling a .cpp file, the compiler does not require a main() function to appear in the file, although you can have a main().

Instead, the compiler only checks that any methods defined in .h files are called correctly, i.e. the signature of the function definition is matched by the name and actual argument types in the call.

Because of the way C++ compiles files, makefiles are advantageous to use, irrespective of the fact that your instructor expects your program to compile simply by typing the word make at the Unix command prompt. One advantage is that a properly written makefile will only act upon files requiring updating. If a subset of files in a program were updated, only those files will be acted upon.

There are many possible items in a makefile. This document will only address a small subset of possible entries. For exhaustive makefile documentation, enter man make from the Unix command line or got to the gnu website: Gnu Makefile Manual. Each entry has:

Here’s the makefile for the testdate example in the Date/Simple directory on pooh:

--------------------------------------------------------------------------------------------------------

testdate: TestDate.o Date.o

        g++ -o testdate TestDate.o Date.o

 

TestDate.o: TestDate.cpp  Date.h

        g++ -c TestDate.cpp

 

Date.o: Date.cpp Date.h

        g++ -c Date.cpp

--------------------------------------------------------------------------------------------------------

The basic syntax of an entry looks like:

 
<target>: [ <dependency > ]*
   [ <TAB> <command> <endl> ]+
 

A makefile typically consists of many entries. First, give the name of a target, followed by a colon. When the make command is issued, the first target in the makefile is made. The command on the line below gives how to construct the target. This command MUST be indented using a TAB. Spaces won’t work.

After the target comes the dependencies. The dependencies are (usually) a list of files which the target file depends on. The dependencies are used by the makefile to determine when the target needs to be reconstructed (usually by recompiling code) and also what must be made before the target itself is satisfied. The make utility decides when to reconstruct the target based on the timestamps of the dependencies.

For example, in the testdate example, testdate has dependencies Date.o and TestDate.o . The object of that target (the g++ -o testdate TestDate.o Date.o command) is to be made if the dependencies aren’t satisfied. If all dependencies are satisfied, the make command will announce that the target is ‘up to date’. So, TestDate.o and Date.o must be checked. Date.o and TestDate.o are not only filenames created by compiling, they are also targets. The makefile checks each target’s dependency list. If any of those files have been altered since the last make, then those dependencies may be satisfied only by executing the command(s) associated with that dependency list. Of course, before the g++ command can be executed, the dependencies must be checked.

For example, suppose after running testdate, Date.cpp is altered and the make command is subsequently issued. The first target, testdate, is to be made; if it’s not up to date, its g++ command will be issued. First, it checks its targets: TestDate.o, is satisfied since neither TestDate.cpp nor Date.h has been altered. In other words, g++ -c TestDate.cpp need not be executed, since the .o file is current, because neither Date.h nor TestDate.cpp were changed (recall that only Date.cpp has been changed since the last make). The other dependency of testdate, Date.o, checks its dependencies and determines that it isn’t up to date, because the timestamp of Date.cpp follows that of the executable testdate. Since the dependencies refer only to files, and not to other targets, the   g++ -c Date.cpp can be executed. This command causes creation of a new version of Date.o, which then satisfies the Date.o target. Now, testdate can be satisfied by executing its associated g+ command. That in turn creates a new testdate executable, and satisfies the testdate target. make is complete.

 

What about that last command? It is g++ -o testdate TestDate.o Date.o . This is a g++ command with no .cpp files! This command takes all the object code, the result of compiling C++ files, and creates an executable. What’s happening here?

 

Linking

 

In TestDate.cpp one or more Date objects are declared and used. For example, there is a function setDate() that can be called to set the date held in a date object. The code for executing a call of setDate() is in Date.cpp. But, when you compile TestDate.cpp, Date.cpp isn’t involved. The compiler need only know about setDate()’s signature, which is part of Date.h, to successfully compile TestDate.cpp

 

So, how does a call of setDate() in TestDate.cpp know what code to execute? This can only be accomplished by hooking up the call in TestDate.cpp with the code, or implementation, found in Date.cpp. The process of connecting up calls and implementations is called linking. Linking connects up calls and implementations between object (.o) files. So, the command g++ -o testdate TestDate.o Date.o creates an executable named testdate (so named because the –o option of g++ was in use) as the result of linking TestDate.o and Date.o. This command always occurs last, as linking is the last step in creating an executable from program code.

More on Dependencies

The dependencies of a .o file are almost always:

.cpp files other than the one being compiled are never dependencies. A .cpp file shouldn’t depend on other .cpp files in order to be compiled.

 

The dependencies for a .o file are obviously the .cpp file with the same name and the .h file of the same name, if there is one (note that TestDate.cpp, being an application, has no associated .h file). Sometimes, applications have .h files. Often, they do not.

But, often, a .cpp file has dependencies other than those that are obvious. In the example, TestDate.cpp depends on Date.h. This isn’t obvious, but it certainly is true, because TestDate.cpp includes Date.h . When composing a dependency list for the compilation of a .cpp file, you need to add any .h file that’s part of the program that you wrote. This is because these files can be changed.

Wait. Why not put iostream in the dependency list? Because iostream isn’t going to be changing. It’s part of C++, you don’t need to react to any changes, since they can’t and won’t occur.

One last thing: If you have any kind of a hierarchy, you’ve got to deal with that, too. For example, if you have a State class that includes the State’s date of admission, this could be represented using a Date object. So, inside State.h will be a declaration of a Date. Here’s a makefile for such a situation:

stateapp: Date.o State.o StateApp.o

        g++ -o stateapp StateApp.o State.o Date.o 

 

StateApp.o: StateApp.cpp State.h Date.h

        g++ -c StateApp.cpp 

 

State.o: State.cpp State.h

        g++ -c State.cpp

 

Date.o: Date.cpp Date.h

        g++ -c Date.cpp 

 

To compile StateApp.cpp (that is, to make StateApp.o), Date.h is a dependency, since a State object contains a Date object. What if you forget to include Date.h in the dependency list? Then, if only the Date class definition is changed, StateApp.cpp won’t get recompiled. Linking will occur with an obsolete version of StateApp.o possibly (but not necessarily) causing problems.

 

This leads to the question, when could there be a problem? Problems could occur if any function signatures defined in Date.h were changed. Suppose the signature of setDate() changed. In that case, the linker would have the newer Date.o, with the implementation of  setDate() with its new signature. Now, if there was a call of setDate() in StateApp.cpp, the call would match the previous signature, which wouldn’t be found in the new version of Date.o. In short, a linker error would occur. Worse, it would be an error that would be difficult to diagnose.

 

Note that dependencies are transitive. We could leave Date.h out of the dependency list for StateApp.cpp if we rewrote the makefile as follows:

 

stateapp: Date.o State.o StateApp.o

        g++ -o stateapp StateApp.o State.o Date.o 

 

StateApp.o: StateApp.cpp State.h

        g++ -c StateApp.cpp 

 

State.o: State.cpp State.h

        g++ -c State.cpp

 

Date.o: Date.cpp Date.h

        g++ -c Date.cpp 

 

StateApp.cpp: Date.h

 

Would we want to do this? No. The dependency can and should go in StateApp.o’s list. But this would work.

 

Ultimately, for a makefile to work at its best, you need to get all the dependencies in the right places. A correct makefile will only compile those files that need to be compiled, and will create executables representing the most recent versions of its constituent files.

Executables’ targets

The main purpose of a makefile is almost always to create an executable. That target is usually the first target in the makefile. For example,

 

stateapp: Date.o State.o StateApp.o

        g++ -o stateapp StateApp.o State.o Date.o 

 

If stateapp is the first target in your makefile, then when you type "make", it will run the commands for stateapp. "make" always runs the commands from the first target in the makefile. It won't run any of the commands from other targets by default. But, other targets can be made by specifying them. For example, if you issue

 

make StateApp.o

 

that target will get made, along with any dependencies that cause commands to be issued. stateapp will not be made in this situation, as it isn’t a dependency of StateApp.o (the opposite doesn’t hold; StateApp.o is a dependency of target statetst).

makefile Naming

The "make" utility uses the following rules to determine which makefile to run.

  1. If you type make and there's a file called makefile, then it will run the commands from the first target of that file, provided the dependent files are more recent than the target.
  2. If you type make and there's a file called Makefile, but no file called makefile, then it will run the commands from the first target of that file, provided the dependent files are more recent than the target.
  3. If you type make -f <filename> then it will run the commands from the first target of <filename>, provided the dependent files are more recent than the target.

Name your makefiles makefile.

Running make on the command line

There are several ways to run make.

  1. Type make at the Unix prompt. If there is a makefile in the current directory, the commands of the first target will be run, according to the dependencies, unless the file is up to date.
  2. Type make -f <filename> at the Unix prompt. The system will look for a makefile with the name <filename> in the current directory and run the commands of the first target.
  3. Type make <target> at the Unix prompt. This time, <target> in makefile or Makefile will be made. <target> does not have to be the first target. The commands will be run provided the dependencies are more recent than the target.

Macro definitions

Macros can make a makefile much more flexible. Macros can be defined, and then used as desired. A macro simply defines a substitution that is made each time the macro is encountered within the instruction area of a makefile, akin to a #define in C++. For example, the –g flag is used with g++ to include debugging information. It must be used on all compilations. This information is comprised of declarations and C++ statements that wouldn’t ordinarily be part of an executable file. Use of the –g flag is appropriate during development, but because the executable is much larger, a final version shouldn’t be compiled using –g.

Here is a makefile with a simple macro:

DebugFlag=-g

 

testdate: Date.o TestDate.o

        g++ -o testdate TestDate.o Date.o  $(DebugFlag)

 

TestDate.o: TestDate.cpp date.h

        g++ -c TestDate.cpp  $(DebugFlag)

 

Date.o: Date.cpp date.h

        g++ -c Date.cpp  $(DebugFlag)

Macros are placed at the beginning of a makefile. They need to be first so their definitions can be known before the processing of the targets and associated commands begins.

In this example, there is one macro:

DebugFlag=-g

 

This defines the term DebugFlag to be equal to –g. In order to use the macro, each command ends with $(DebugFlag). Here’s the command to satisfy TestDate.o:

 

        g++ -c TestDate.cpp  $(DebugFlag)

 

A macro is accessed using the following syntax: $(<Macro name>)

 

Thus, the command actually executed will be:

 

        g++ -c TestDate.cpp  -g

 

with the –g obtained from the macro definition.

 

So, what’s the advantage? When done with development, all the developer need do is remove the –g following the = sign:

 

DebugFlag=

 

Now, $(DebugFlag) holds nothing. In this way, the –g can be added or removed from all compilations at one spot, rather than having to change each g++ command. 

Now, a more involved example:

DebugFlag=-g

ObjectFiles=Date.o TestDate.o

Compile=g++

Libraries=

 

testdate: $(ObjectFiles)

        $(Compile) -o testdate $(ObjectFiles) $(DebugFlag) $(Libraries)

 

TestDate.o: TestDate.cpp date.h

        $(Compile) -c TestDate.cpp  $(DebugFlag)

 

Date.o: Date.cpp date.h

        $(Compile) -c Date.cpp  $(DebugFlag)

OK. We’ve talked about DebugFlag. What about ObjectFiles? This one isn’t overly useful; it simply designates the .o files that will be both linked and also the dependencies of testdate. Note its use. Next is Compile. Compile designates the compiler to be g++. This can be quite useful, as sometimes, when a program is designed to be ported among different platforms, different compilers might be designated for composing the executable. With the Compile flag, changing the compiler requires only changing the value of the macro. Lastly, the Libraries macro is included, not for any use now, but again, it is possible that on a different platform (another machine), a library will have to be specified for the linker to access during its operation.

Notes:

¨      If a line is too long, you can use a backslash followed by a newline to indicate a continuing line. This can be used in macros, dependencies, commands, etc.

¨      Most publications list macro names in all capitals. This is not required.

¨      Use macros in makefiles for the same reason you use constants in programs. It's easier to update the files. It adds flexibility and potentially cuts down on errors.

Other Makefile Targets

There are times when you don't want to create a target. Instead, you want to run command(s).

 

Here is a very common example of a command run in a makefile:

 

clean:

     rm -f *.o testdate

     touch *

This target/command pair is often found at the end of a makefile. clean is not found as a dependency in any rule above it. So, why is it there? the clean target allows a programmer to wipe the slate clean. By entering make clean at the Unix prompt, all .o files, along with the executable, will be deleted. Also, the following line, touch * can be included also, to update the timestamp of all files. This has two common uses:

¨      When development is done, remove all compilation files to save space. If an executable needs to be generated later, all that is needed is a call to make.

¨      Facilitate recompilation of all files in a project

Sometimes, a makefile will be used to generate multiple executables. In this case, a makefile might contain two or more targets that themselves cause generation of an executable. In these situations, a makefile might look like this:

all: exec1  exec2  exec3

 

exec1: exec1.o main1.o

   g++ exec1.o main1.o -o exec1

 

exec2: exec2.o main2.o

   g++ exec2.o main2.o -o exec2

 

exec3: exec3.o main3.o

   g++ exec3.o main3.o -o exec3

 

Note the three targets in the file named exec1, exec2, exec3. Target all would cause all three to be made. Note that there is no command associated with all.

 

Conclusion

 

You now have enough information to write a working makefile. Certainly, this document only presents the basics, but here you have obtained the foundational material to which you can add further functionality as necessary.

 

Don’t forget the most common makefile error: Forgetting that the command must be prefaced by a TAB. You can’t use spaces, you must use a TAB.