Department of Engineering

IT Services

Source File organisation

The needs of large-scale organisation and support for many platforms may make modules incomprehensible unless some understanding of the overall structure is gained first.

Preprocesser Facilities

The preprocessor has some useful options.

sourcefile inclusion :-

#include "defines.h"
...
#include <defines.h>
The difference between these two variants is that with the included file in quotes, it is first looked for in the directory of the source file. In each case, the standard include directories on the system are searched as well as any directories mentioned on the command line after the `-I' flag. See the `cpp' man page for more details.

macro replacement :-

Note that these macros are expanded before the compiler is called. They aid legibility. In the first example below, a simple substitution is done. In the second, an in-line macro is defined, whose execution should be faster than the equivalent function.

#define ARRAY_SIZE 1000
char str[ARRAY_SIZE];
...
#define MAX(x,y) ((x) > (y) ? (x) : (y))
int max_num;
    max_num = MAX(i,j);
...

conditional inclusion :-

Blocks of code can be conditionally compiled according to the existence or value of a preprocessor variable. A variable can be created using the `#define' preprocessor directive or using the `-D' option at compilation time. The first two examples shows how debugging statements can easily be switched on or off. The final example shows how blocks of code can be de-activated.

#ifdef DEBUG
  printf("got here\n");
#else
  something();
#endif /*DEBUG*/
...

#if defined(DEBUG)
#define Debug(x) printf(x)
#else
#define Debug(x)
#endif

 if ( i == 7 ){
     j++;
     Debug(("j is now %d\n", j));
 }

#if 0
 /* this code won't reach the compiler */
 printf("got here\n");
#endif

Multiple Source Files

Modularisation not only makes the source more easy to manage but it speeds up re-compilation: you need only recompile the changed source files. Also, by keeping the I/O components in one file (and perhaps the text to be printed into another) one can more easily convert the software to run on other machines and in other natural languages.

By default, functions and variables defined outside of functions can be accessed from other files, where they should be declared using the extern keyword. If however the variable is defined as static, it can't be accessed from other files. In the following example, `i', `j' and the function `mean' are created in file1.c but only `i' can be accessed from file2.c.

/* file1.c */
int i;        
static int j;
static int mean(int a, int b){
...
/* file2.c */
extern int i;

Names of external variables should be kept short; only the first 6 initial characters are guaranteed to be significant (though in practise the first 255 character often are).

You should keep to a minimum the number of global variables. You can use include files to manage your global variables.

  1. Construct a `globals.h' file with all of your #defines and variable declarations in it. Make sure all variables are defined as externs. Include this file in all the relevant source files.

  2. In the file that contains your main(), you again have all the variable definitions, minus the externs. This is important - if they are all defined extern, the linker will not be able to allocate memory for them.

You can achieve this with the help of the pre-processor if your globals.h looks like this:-

#ifdef LOCAL
#define EXTERN
#else
#define EXTERN extern
#endif
	
EXTERN int num_of_files;
..

In this way, the `EXTERN' becomes `extern' in every file that includes globals.h. The trick is then to have

#define LOCAL
#include "globals.h"

in the file containing the main routine.

If you're calling a routine in one file from another file it's all the more important for the formal parameters to be declared correctly. Note especially that the declaration `extern char *x' is not the same as `extern char x[]' - one is of type `pointer-to-char' and the other is `array-of-type-char' (see online).

Make

If you have many source files you don't need to recompile them all if you only change one of them. By writing a makefile that describes how the executable is produced from the source files, the make command will do all the work for you. The following makefile says that pgm depends on two files a.o and b.o, and that they in turn depend on their corresponding source files (a.c and b.c) and a common file incl.h:

   pgm: a.o b.o
       cc -Aa a.o b.o -o pgm
   a.o: incl.h a.c
       cc -Aa -c a.c
   b.o: incl.h b.c
       cc -Aa -c b.c

Lines with a `:' are of the form

target : dependencies

make updates a target only if it's older than a file it depends on. The way that the target should be updated is described on the line following the dependency line (Note: this line needs to begin with a TAB character).

Here's a more complex example of a makefile for a program called dtree. First some variables are created and assigned. In this case typing `make' will attempt to recompile the dtree program (because the default target is the first target mentioned). If any of the object files it depends on are older than their corresponding source file, then these object files are recreated.

The targets needn't be programs. In this example, typing `make clean' will remove any files created during the compilation process.

# Makefile for dtree
DEFS =  -Aa -DSYSV
CFLAGS = $(DEFS) -O
LDFLAGS = 
LIBS = -lmalloc -lXm -lXt -lX11 -lm

BINDIR = /usr/local/bin/X11
MANDIR = /usr/local/man/man1

OBJECTS_A = dtree.o Arc.o Graph.o 	#using XmGraph

ARCH_FILES = dtree.1 dtree.c Makefile Dtree Tree.h TreeP.h \
   dtree-i.h Tree.c Arc.c Arc.h ArcP.h Graph.c Graph.h GraphP.h

dtree: $(OBJECTS_A)
        $(CC) -o dtree $(LDFLAGS) $(OBJECTS_A) $(LIBS)

Arc.o:	Arc.c
        $(CC) -c $(CFLAGS) Arc.c

Graph.o:	Graph.c
        $(CC) -c $(CFLAGS) Graph.c

dtree.o:	dtree.c
        $(CC) -o dtree.o -c $(CFLAGS) -DTREE dtree.c

install: dtree dtree.1
        cp dtree $(BINDIR)
        cp dtree.1 $(MANDIR)

clean:
        rm -f dtree *.o core tags a.out