Using Make to Compile Multi-File C++ Programs


This page provides some quick notes on how to split a C++ program into multiple files and manage the compilation process with make. More information about make can be found in the Tutorial on Make provided by the UCSC Physics Department.
  • Header Files and Source Files
  • Declarations, Definitions, and #include
  • Compiling Without Make
  • Setting Up a Makefile
  • Running Make
  • DISCLAIMER

    Several things are somewhat oversimplified in this description. Do not mistake this document for a definitive reference to make or C++.

    Header Files and Source Files

    In general, a C++ program may consist of many header files (with names ending in .h), source files (with names ending in .c, .cc, or something else, depending on language choice and local conventions), and libraries. In general, header files include information that simply provides information to the compiler (e.g. a type declaration), and source files (and libraries) include program elements that cause the compiler to generate something in the executable program (e.g., a definition of a function or global variable). This discussion will cover the use of existing libraries, but not the creation of new libraries.

    During the compilation of a multi-file program, the compiler may scan each header file repeatedly, but will scan each source file or library only once. Thus, if something like a function body were placed in a header file, that function body would appear multiple times in the final program, which is generally a problem (unless the function is an inline function).

    Related program elements should be grouped into the same header (or source) file; unrelated ones placed in different files. For example, we might put the declaration for a type and declarations for several functions that work with objects of that type in one header, and then define that group of functions in one source file; a separate group of functions would be declared together in a different header file, and defined in a different source file. This approach fits together with the C++ "class" feature, which combines a type and functions that manipulate it. In C++ programs, it is not uncommon to find one header file and one source file for each class, plus one source file for the main function (this is a reasonable approach to start with if you're not sure about how to break up a C++ program into separate files).

    Declarations, Definitions, and #include

    Thus, the basic approach to dividing up a program into separate files is this: group together related program elements, and put each group of related declarations in one header file, and and each group of related definitions in a source file. When a source file refers to something declared in a header file, the #include directive is used to make the compiler scan the header file at the start of the source file (for example, if main.c uses the sqrt function, which is declared in the math.h header file, we use
    #include < math.h >
    
    at the start of main.c, to tell the compiler to scan the math header file). Angle brackets are used for system header files, and quotes for header files that you have created in the directory for your program. One header file may also #include another header file - if I create a header file wherewhen.h that that declares a type in terms of time_t (a type defined in the system header file time.h), I would use
    #include < time.h >
    
    in wherewhen.h, and then
    #include "wherewhen.h"
    
    in any source files that use my type. When header files include other header files, it may be the case that one header is ultimately included more than once in a given source file (for example, if countdown.h also included time.h, and then my main.c file included both wherewhen.h and countdown.h). This can cause problems unless we use the #define and #if directives to make the compiler skip the contents of the header file if it is scanned repeatedly. (Don't worry if you don't completely understand this problem - you can just mimic the pattern of #if/#define/#endif shown below in each of your header files.)
    /* This is the header file wherewhen.h */
    /* First, we ensure that we only look at this file if we haven't */
    /*  already defined the word _WHEREWHEN_H; then we define it.    */
    
    #if ! defined _WHEREWHEN_H
    #define _WHEREWHEN_H 1
    
    /* Now we include the other headers we need */
    
    #include < time.h >
    
    /* Finally, we declare a type and a related function */
    
    struct where_and_when {
       time_t when;
       double latitude, longitude;
    };
    
    /* Give the speed of travel between two events */
    double speed(where_and_when, where_and_when);
    
    #endif
    
    When a program is split into multiple files, it may be necessary to introduce declarations that would not have been needed in a single-file program. For example, we might not have needed a declaration of the "speed" function above in a single-file program; we would simply have put the definition of this function before it was used. However, in a multi-file program, we might define speed in one file (such as wherewhen.c), and use it in another (such as main.c); in this case, it will only be declared before it it is used in main.c if we put the declaration in wherewhen.h and #include wherewhen.h in main.c (as well as in wherewhen.c). Such function declarations for system library functions are generally provided in the system header files (for example, the body of the sqrt function is in the math library, and math.h gives the declaration).

    Compiling Without Make

    Assume we have a program with two header files (wherewhen.h and countdown.h) and three source files (wherewhen.c, countdown.c, and main.c). The file main.c #includes both of our headers and math.h, and each of the other .c files includes the corresponding header (i.e., wherewhen.c includes wherewhen.h). Both of our header files include time.h. We can compile our program in one step, by listing each of the .c files and libraries on the compilation command line, as follows:
    g++ -Wall -g wherewhen.c countdown.c main.c -lm -o myprogram
    
    Note that we have listed all of the source files, and used "-lm" to refer to the math library (library names can be hard to guess, but they can often be found on the manual page for functions - for example, the "SYNOPSYS" section of the sqrt man page shows the use of "-lm"). The "-o myprogram" tells the compiler to name the resulting program myprogram, "-g" tells the compiler to include support for the debugger, and "-Wall" tells it to warn us about all suspicious-looking code. If we change any of our source files or headers, we would need to use the same command to produce an updated executable program. If we had only made one small change to, say, wherewhen.c, it seems like a waste of time to recompile countdown.c and main.c as well. We can avoid this waste by compiling each source file separately, producing a separate "object code" file (ending with .o) for each one, and then combining these object files with the library in a final step: (the -c flag tells the compiler to generate a ".o" file).
    g++ -Wall -g -c wherewhen.c
    g++ -Wall -g -c countdown.c
    g++ -Wall -g -c main.c
    g++ -g wherewhen.o countdown.o main.o -lm -o myprogram
    
    Now, if we change wherewhen.c, we only need to redo part of the compilation process:
    g++ -Wall -g -c wherewhen.c
    g++ -g wherewhen.o countdown.o main.o -lm -o myprogram
    
    This involves more commands, but less work for the compiler. On a large project in which we keep making and testing changes to individual source files, this can save a lot of time.

    BUT ... what if we change wherewhen.h? Then we need to recompile every source file than #includes it, and every source file that includes a header that includes it, etc. In this example, we need to recompile wherewhen.c and main.c. But how would we keep track of this if there were lots of other files? We'd need to set up a table of what gets included where, and then check it every time we make a change, to see what needs to be recompiled.

    The program make automatically checks a set of dependencies (defined in a makefile) and issues commands to handle re-compilation. Thus, it solves the problems of having to type lots of commands during re-compilation, and figuring out which commands need to be run.

    Setting Up a Makefile

    A makefile is a file (usually named "makefile" or "Makefile" - I usually use the latter) that typically includes a list of entries, each of which has a target, list of dependencies, and a rule for creating the target from the dependencies. The simplest way to set up a makefile involves giving each step in the compilation as a separate entry in the makefile, like this:
    wherewhen.o : wherewhen.c wherewhen.h
    	g++ -Wall -g -c wherewhen.c
    
    countdown.o : countdown.c countdown.h
    	g++ -Wall -g -c countdown.c
    
    main.o : main.c wherewhen.h countdown.h
    	g++ -Wall -g -c main.c
    
    myprogram : main.o countdown.o wherewhen.o
    	g++ -g wherewhen.o countdown.o main.o -lm -o myprogram
    
    WARNING: You must use a tab (not 8 spaces) at the start of the line giving the rule. Doing otherwise will just confuse make (and you). Do not use tabs anywhere else in the Makefile.

    Note that the list of dependencies lists the files which would force us to recompile the target if they were to change, except for the system files (which we assume will not be changed).

    It is traditional to define certain variables in Makefiles to make it easy to do things like switch compilers or compilation flags. This would be done like so:

    CC = g++
    CFLAGS = -Wall -g
    LDFLAGS = -lm
    
    wherewhen.o : wherewhen.c wherewhen.h
    	${CC} ${CFLAGS} -c wherewhen.c
    
    countdown.o : countdown.c countdown.h
    	${CC} ${CFLAGS} -c countdown.c
    
    main.o : main.c wherewhen.h countdown.h
    	${CC} ${CFLAGS} -c main.c
    
    myprogram : main.o countdown.o wherewhen.o
    	${CC} ${CFLAGS} wherewhen.o countdown.o main.o ${LDFLAGS} -o myprogram
    
    There are also ways of simplifying things by introducing more general rules and patterns so that you don't need a rule for each source file, but those are outside the scope of this document.

    The one problem that we haven't solved so far is the construction of the list of dependencies. In this simple example, where none of our header files includes another of our header files, this is not a problem. But consider what would happen if wherewhen.h included another of our header files (say, location.h). We would then have to list location.h in the list of dependencies of every source file than includes wherewhen.h. This can make the construction of the makefile quite tedious; fortunately, there is a program called makedepend that builds makefiles automatically. The basic use of makedepend is to build a simplified makefile in which the lists of dependencies are empty, and then run the command "makedepend *.c"; more advanced use is outside the scope of this document.

    Running Make

    Once the program is written, in its separate files, and the makefile has been constructed, you just need to run "make" to compile the program. You can do this at the UNIX prompt, or use M-x compile to start make from within emacs. The advantages of running make within emacs are (1) if you were editing the files with that emacs process and forgot to save them, it will ask you, and (2) emacs will match up the error messages with the part of the program containing the error (you use M-x next-error, or C-x ` to do this).

    Return to the page of


    HTML 2.0 Checked! This page maintained by davew@cs.haverford.edu