21 Feb 1996

Outline

Handling multiple files

There are several reasons to split a program into several files:

  1. It becomes easier for you and others to find things,
  2. It allows different parts to be compiled separately,
  3. It makes reusing code easier, and reused code is easier to keep consistent.

    Suppose we have a program that consists of several source files. In assignment 2, for example, we had queue.c, maze.c, and graph.c.

    The file graph.h contains header information used by graph.c and maze.c. The file queue.h contains header information used by queue.c and maze.c. The code for maze.c, then, contained the lines

    	#include "graph.h"
    	#include "queue.h"
    

    In ObjectCenter, we loaded this by typing

    	load graph.c queue.c maze.c 
    	build
    	run
    
    When the build command is used, only the changed files will be reloaded. For instance, if you modified graph.c then only graph.c gets reloaded. If you modified graph.h, then both graph.c and maze.c get reloaded since both include dict.h. (ObjectCenter is smart enough to know this.)

    The GNU compiler g++, on the other hand, isn't. So we write Makefiles to tell it how to figure it out.

    Writing Makefiles

    The Unix utility make makes use of a file called ``Makefile'' that explicitly expresses the dependencies that build uses in ObjectCenter.

    Here is a Makefile that captures the dependencies in the above example (You can substitute "CC" for g++ if you want):

    # comments begin with a "#" sign.
    
    maze.o: maze.c graph.h queue.h
    	g++ -c maze.c
    # the above line begins with a tab
    
    graph.o: graph.c graph.h
    	g++ -c graph.c
    queue.o: queue.c queue.h
    	g++ -c dict.c
    
    foo: maze.o graph.o queue.o
    	g++ maze.o graph.o queue.o -o foo
    
    The file is broken into pairs. The first of each pair specifies a specific file, and which other files it depends on. The second tells how to create that file. So:
          graph.o: graph.c  graph.h
                  g++ -c graph.c
    
    say that graph.o depends on graph.c and graph.h, and that to create graph.o use "g++ -c graph.c". This line must begin with a TAB character.

    In this example, a file with the suffix ".o" is an object file. These are files that can be linked together to form an excecutable file, such as foo. It should be possible to reuse a .o file such as graph.o in another C program without recompiling graph.c.

    When I type "make foo" to the shell, it finds the line "foo: maze.o graph.o queue.o", which tells it that in order to create foo, it must have (or create) maze.o, graph.o, and queue.o. It then recursively creates these files according to the same rules. Now if the ".o" files are up-to-date, it then asks if there exists a file called "foo" that is newer than the ".o" files. If there is, it is done, and no compiling occurs. If not, then it it executes the appropriate command to create foo.

    Suppose I "make foo" then make a change to queue.c, and type "make foo" again. It will only execute the commands "g++ -c queue.c" and "g++ main.o graph.o queue.o -o foo". It will not do "g++ -c main.c", for instance, because main.o is newer than all the files on which it depends: main.c, queue.h, and graph.h. On the other hand, if I had edited queue.h, then that step would have been done.

    Debugging hints

    Here are some debugging tips.

    Using gdb

    If you're using g++ to develop your programs, you can use a debugger called gdb to debug them. To use, compile your program with the `g' flag (g++ -g foo.c) and then type gdb a.out at the shell prompt. Then you'll get a gdb command line, at which you can type run to run the program. You can redirect files into the program like you do at the shell prompt: run < input1.

    If the program finishes successfully, gdb will say so and we're happy. You then quit gdb with the quit command. If there's an error, or if you type control-C, the gdb command line returns and you can see what the state of the program is.

    When looking at the program's state, you start out at the top of the call stack. You can see the top several calls of the program stack with the where command. With up and down you can investigate the state in different function calls on the stack (confusingly, up moves down the stack and down moves up the stack). And then you can print expression values with the print command: print i prints the current value of i in the function being investigated, and print fib(5) prints the value fib returns when given the argument 5.

    The gdb debugger has lots of other features, notably breakpoints. These are described briefly in the handout (if you've handed that out) and more extensively in its online help (with the help command, logically enough).