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).
#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).
g++ -Wall -g wherewhen.c countdown.c main.c -lm -o myprogramNote 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 myprogramNow, 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 myprogramThis 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.
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 myprogramWARNING: 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.
Return to the page of
This page maintained by
davew@cs.haverford.edu