CSC 161 Grinnell College Fall 2016
Scribbler 2
CSC 161:
Imperative Problem Solving and Data Structures
Scribbler 2
Course Home Syllabus Schedule MyroC Documentation Acknowledgments

Program Management: Header Files and make

The namelist.c Program

Previous labs, Scheme-like linked Lists and Linked Lists for a Movie, developed a program scribbler-list-movie.c that kept track of pictures taken by a robot. In that program, successive pictures were stored on a linked list, in which each node contained a picture and next pointer.

To simplify the current discussion, this reading utilizes a program namelist.c that just stores a name and next pointer in each node. That is, a node contains a character array and next field:

/* Maximum length of names */
#define strMax 20
struct node
{
   char data [strMax];
   struct node * next;
};

In contrast to scribbler-list-movie.c, all functions in namelist.c have been completely implemented.

Reading Outline


Reorganizing namelist.c

The program namelist.c contains all components of the linked-list code in a single file. Specifically, this program contained:

While such a monolithic framework works fine for small projects, the use of a single file for an entire program has several drawbacks:

In C (and other languages), such problems are resolved following a two-pronged approach:

  1. A program is divided into multiple files.
  2. Compiling is automated, so that multiple files can be compiled as needed using a simple command line.

Dividing the namelist.c Program Into Pieces

Since namelist.c contains several independent components, a separate component could be defined for each component. The relevant files and their dependencies are shown below:


list program file dependencies

As this diagram indicates, the original namelist.c program may be divided into the following four components:


Within this structure, node.h is independent of the others. However, information about a node structure is needed elsewhere, so that both list.h and main.c contain references to node.h in include statements. Similarly, both implementation files (list-proc.c and main.c) reference tree operations, so both contain references to list-proc.h.

#ifndef Statements

Technically, you may have noted that list-proc.h includes node.h, so an explicit inclusion of node.h in main.c is unnecessary. However, in such a distributed structure of files, it is not uncommon that some definitions are referenced in several places. (A programmer could track down all possible references, but this may undermine some of the advantages of dividing the program into pieces.)

Unfortunately, this multiple referencing of a file could mean that a definition appears twice in a program, and compilers take a dim view of such matters. To resolve this problem, node.h contains the lines:


#ifndef _NODE_H
#define _NODE_H

...

#endif

In C, files can define identifiers for the compiler, and the compiler can check if an identifier has been defined previously. For example, the identifier strMax is defined as the number 20 for a global constant, just as was done in previous programs. However, in node.h, a new identifier _NODE_H also is defined. With this new identifier, when a file first references node.h, the identifier _NODE_H will not have been defined. The test #ifndef asks the compiler if an identifier is not defined, and in this case processing continues within the if statement. This first call, therefore, defines identifier _NODE_H. With any subsequent references to node.h, identifier _NODE_H will have been defined, so processing within the ifndef statement will not happen a second time.


Compiling

With this structure, the header files node.h and list-proc.h contain definitions, but do not yield any code directly. Files list-proc.c and main.c, however, must be compiled. Since these files are independent, they can be compiled in either order, with the commands:


gcc -c list-proc.c
gcc -c main.c

Here the -c flag tells the compiler to produce a machine-language or "object" file, but not to expect the whole program to be present. The resulting files have a .o extension.

These pieces then can be linked together with the command:


gcc -o main main.o list-proc.o

Alternatively, if main.c is to be compiled after list-proc.c, then compiling and linking of main.c can be done in one step. The resulting commands are:


gcc -c list-proc.c
gcc -o main main.c list-proc.o

As this illustrates in the second line, the main .c program is given before any object files.


make and Makefile

While the division of software into multiple files can ease development, the manual compiling all of the pieces can be tedious. Linux, Unix, and Mac OS X provide a make capability to automate this process, where instructions for compiling are given in a file called Makefile.

The discussion that follows involves several main components:

To illustrate the make and Makefile concepts, the following subsections will utilize a sample Makefile that is designed for use with the namelist.c program described above.

While this Makefile is slightly more complex than is absolutely necessary, this version shows several common elements of many Makefiles.

Using make

To illustrate the use of the make facility for the files main.c and list-proc.c, as described above, the following interaction shows the result of running make twice at a workstation.


$ make
gcc -ansi -c main.c
gcc -ansi -c list-proc.c
gcc -o main main.o list-proc.o
$ make
make: Nothing to be done for `all'.

As this interaction suggests, make and Makefile keep track of what needs to be done to compile and link the designated files. Work occurs only as needed. Thus, the first time make was run, both programs were compiled and the resulting object files linked. However, the second time make was run, the machine detected that no files had changed from the first time, so no further work was needed. To expand on this point, if file list-proc.c were changed, but no other changes were make, running make might produce the following:


$ make
gcc -ansi -c list-proc.c
gcc -o list list.o list-proc.o

Here, nothing related to file main.c had changed, so that was not recompiled. More generally, make reviews the status of all relevant files and compiles and links only those that are out of date.


Writing Makefiles

With this overview of make, we now look at the Makefile instructions more carefully.

A simple Makefile identifies one or more elements, called rules, interspersed with comments. Most Makefiles also contain several variables to specify various common parameters and options.

Makefile Documentation

The discussion here closely follows the online document, GNU make Manual, by the Free Software Foundation, 2006.


Comments in a Makefile begin with the character #. The comment continues for the rest of the line, as in bash or csh shell programming.

Makefile comments are analogous to C comments starting with //. A comment starts with a special symbol (# for a Makefile, // for C) and continue through the current line.


A rule within a Makefile has the form

target: prerequisites
     recipe

To interpret this pattern,

Example: main/list-proc


Ultimately, the goal for the above example is to produce the executable program main. Intermediate goals include generating the object files main.o and list-proc.o. Overall, the targets for this work include main.o, list-proc.o and main.




Macros:

While such explicit specification of commands works fine within a Makefile, this approach sometimes may cause trouble if the software is to be compiled and linked on multiple platforms. To anticipate such matters, it is common to use macros to specify various compiling details. Then, if the files are moved to other systems, only the macros need be changed -- not the entire Makefile.

Macros may be considered as variables, defined early within a Makefile and look much like an assignment statement in C.

var = ...

Once defined, the variable can be used later by preceding the variable name with a dollar sign $. Parentheses are allowed for clarity.


Cleaning up your Directory

In addition to compiling a program, other rules may be added to a Makefile to perform common tasks. One such task involves cleaning up a directory, deleting unneeded .o files and emacs backups to your .c programs.

For this work, we are not creating a new file, but we can create a rule (sometimes called a phony rule) that provides a recipe for cleaning up.


Makefile Abbreviations

Beyond these basic capabilities, make and Makefile allow many additional features. For many common applications, the details covered to this point in the reading may be adequate. However, the make command also recognizes some additional variables (called automatic variables) that can be useful in writing some recipes:

Automatic Variable Meaning
$@ the target of the rule
$< the first prerequisite for the rule
$^ the collection of all prerequisites, separated by spaces

Yes, the abbreviations are awkward and hard to remember. Perhaps the most useful is $^, as one does not have to repeat all of the pre requites on the line for compiling.


Further Reading

Extensive documentation regarding make may be found through the online GNU make Manual, Free Software Foundation, 2006.



created 3 December 2001
revised 4 April 2010 by Henry M. Walker
revised 16-18 November 2011 by Henry M. Walker
revised 12 December 2011 by Henry M. Walker
minor editing 22 August 2012 by Henry M. Walker
substantial revision, reformatting 11 November 2016 by Henry M. Walker
Valid HTML 4.01! Valid CSS!
For more information, please contact Henry M. Walker at walker@cs.grinnell.edu.