[CSE2304]

Code Modularity and the C Programming Language

Torsten Seemann
School of Computer Science and Software Engineering,
Faculty of Information Technology, Monash University

Revised 16 February 1999

Introduction

In this handout we discuss some of the issues involved with developing reasonably sized C programs under DOS/Windows or Unix. In particular, we look at splitting up your source code into separate files and modules, and what C contructs are available to help you with this. We assume some basic knowledge with the C programming language.

Splitting up your source code

Many new C programmers will quite happily write a large program, and have all the source code in the one huge file. They press F9 and it compiles quite quickly on the latest machines, so why bother splitting it up into separate C files?

Under DOS and Windows, creating programs with many source files is easy. Just use the ``Project'' facility of the integrated compiler/editor. You should have a small project window where you can add/delete files to/from the project using the INS/DEL keys. Under Unix, you usually have to do things manually, although there are various tools to help you, like make and makedepend.

What happens when you compile a C program?

Let us assume you have a small project consisting of three files: main.c, stack.c, stack.c (attached to the end of this document)

When to type F9 on DOS/Windows or make under Unix, many things happen.

Firstly, each .c file is pre-processed by the C pre-processor (called cpp or cpp.exe. This takes all the compiler directive lines like #include and #define and produces a new C file where all these directives have been resolved -- the #included files are literally included, and the #define constants and macros have been subsituted for what they represent.

Secondly, the post-processed .c file is compiled. This converts all the C code into native object code, resulting in an object file. These have extension .obj under DOS and .o under Unix. Note that .h files are not actually compiled into code -- they simply provide information to the compiler.

Finally, all the compiled .o files are linked together (along with some compulsory start-up and exit code, and any libraries) to produce an executable file you can run. Under DOS/Windows these have a .exe extension.

What is a .h file?

A .h file is a C header file. Header files should not contain any source code. They are used purely to store function prototypes, common #define constants, and any other information you wish to export from the C file the header file belongs to.

In our example, we have a stack module. The source code is in stack.c. It contains two global variables (an array for the stack, and a stack pointer), and four functions (init, push, pop, empty). The header file contains four prototypes of these four functions. These prototypes are identical to the function names in the source file. No mention is made of the two variables in stack.c as they are private to the stack module, and we do not want anyone modifying them except via the four supplied stack functions.

Now, main.c needs the stack module to work. Note that is does not #include the stack.c source code. It doesn't need to -- it will be linked in at the end. However, it does include the stack.h header file. This is not strictly required in C, but is essential for writing bug-free programs. The reason is that each C file is compiled separately. When main.c is compiled, the compiler sees all these calls to functions it doesn't know about (like initstack()). However, because we supplied prototypes in the header file, the compiler knows what parameters (how many, what type) the stack functions take, and what they return! Without that knowledge, the compiler will let you pass as many parameters you want, and assume every function returns an integer. In most cases, this is not a desirable thing!

What about stdio.h and those other header files?

When you first begin programming, most examples you see will #include <stdio.h> and maybe some other header files with angle brackets. When you do this, you are not including any code for the functions. You are only including protype definitions, macros, type definitions, and constants. Here is an extract out of a Unix stdio.h file:

Extract of stdio.h

extern int      fgetc(FILE *);
extern char     *fgets(char *, int, FILE *); 
extern int      fputc(int, FILE *);
extern int      fputs(const char *, FILE *);

#ifndef NULL
#define NULL            0L
#endif 

#define getc(p) (--(p)->_cnt < 0 ? __filbuf(p) : (int)*(p)->_ptr++)

Here we see four prototypes (the extern keyword is optional), a constant, and a macro. The actual code for all these functions is contained in a library call libc which is automatically linked in with the other object files to produce an executable. Nearly all the documented header files like stdlib.h, string.h etc have their code in libc. The reason there are many separate header files is to reduce the load on the pre-processor and compiler, because the programmer only includes those header files containing information his program needs.

Keeping modules modular

When writing your programs, you should try and split up your source code into modules. Modules are self-contained units which can hopefully be re-used, and are easy to maintain and debug.

Global variables

Whenever you declare a variable outside any functions, it is a global variable. This means it is possible for any function anwywhere in any of your code to access it and alter it. Obviously, this can be dangerous, and the use of global variables should be minimized.

The static keyword

C provides some mechanism for variable privacy. If a global variable is prefixed by the static keyword, it will be global to only the C file it exists in, and not outside. This is what I did with the array and stack pointer in the stack.c source code. It is also possible to make C functions static as well. This is useful if you have some internal functions which you don't want other files to access. Obviously, the prototypes for static functions should not be put in the module's header file.

The extern keyword

If a module does have a global variable, and it is not static, it is accessible by any other C file. However, before it can do so, the C file wanting to use it must declare its existence, a bit like a function prototype declares its existence. You can do this with the extern keyword.

For example, if the stack pointer variable in stack.c was not static (private), then one could access it from main.c. Firstly, one would add the line

    extern int stackPointer;

somewhere to the main.c source code. This tells the compiler that the variable exists somewhere in another .o file, and has type integer. This could be useful for seeing how many elements are currently in the stack. A better way would be to write a stackSize() function in the stack module.

A Small Example

Here is a example of part of a possible graphics library module:

graphics.h

#ifndef graphics_h
#define graphics_h

#define BLACK	0
#define WHITE	255

typedef unsigned char Pixel;

void drawLine(int x1, int y1, int x2, int y2);

#endif

graphics.c

#include <stdio.h>

Pixel penColour = BLACK;
static Pixel* frameBuffer = 0xA000000L;

void drawLine(int x1, int y1, int x2, int y2)
{
  /* draw line from (x1,y1)-(x2,y2) with current penColour */
}

static int deleteBuffer(void)
{
  /* delete memory allocated for some buffer */
  /* internal use only */
}

For someome using this graphics library, they are allowed to call the drawLine() function, and alter the penColour variable. They could do this by putting an extern Pixel penColour; line in their code, or it could have possibly been placed in the graphics.h file itself by the module author. The user can not alter the frameBuffer pointer, nor call the deleteBuffer() function. These are both static or private to the graphics.c source file.

Improving the stack module

Although the stack.c module given earlier was an improvement on just having the stack code mixed in with our program code, it can be improved further. For example, only one stack can be used at time, as there is only one global array and stack pointer in the stack.c source file. What we would like is to be able to create as many stacks as we need, but without significantly altering the code we already have.

To do this, we can encapsulate all the data/variables connected with maintaining a stack, and place them in a record or C struct.

Extract of stack2.h

#define MAX_STACK_SIZE          100

typedef struct 
{
  int stack[MAX_STACK_SIZE];
  int stackPointer;
}
Stack;

In our case, there were three variable associated with a stack -- the array, the pointer, and the size of the array, which I have left as a constant. The listing above shows the definition of a new C type called Stack which encapsulates these variables.

Now that we can have different stacks, we need to alter the stack functions to operate on a given stack, and not just on the one global one. To do this, we must pass all the required stack information to each function.

Extract of stack2.c

#include "stack2.h"

void pushOntoStack(Stack* s, int number)
{
  s->stack[ s->stackPointer ] = number;
  (s->stackPointer)++;
}

In the example above we see that ``push'' operation has been modified to additionally accept a pointer to a stack (The old prototype was void pushOntoStack(int number)). We pass it is a pointer for two reasons. Firstly, it is more efficient than passing it by value, and secondly, we require that the push function modify the variables in the Stack instance. If we did pass it by value, a copy of it would be given to the function, and altering the stackPointer for example would only change the one in the copy, leaving the original as it was.

The previous program example using the new version of the code is attached, and uses the files main2.c, stack2.c, stack2.h.

Summary


stack/Makefile

CC = gcc
CFLAGS = -Wall 
LIBS = -lm

BIN = bw
OBJS = main.o stack.o

$(BIN):: $(OBJS)
	$(CC) $(CFLAGS) -o $(BIN) $(OBJS) $(LIBS)

main.o:: stack.h

stack.o:: stack.h

clean::
	/bin/rm -f *~ *.o

stack/stack.h

#ifndef stack_h
#define stack_h

void initStack(void);
void pushOntoStack(int number);
int popFromStack(void);
int stackEmpty(void);

#endif

stack/stack.c

#include <stdio.h>

/* Include it's own prototypes. This is good practice. */

#include "stack.h"

/* These are the variables used by the stack module. They are
   global to this file ("static"). The stackPointer points to
   the NEXT AVAILABLE index in the stack array. Thus, if it is
   0, the stack is empty. It should never be negative! */
   
#define MAX_STACK_SIZE	100   		/* Maximum pushes we can handle */
   
static int stack[MAX_STACK_SIZE];
static int stackPointer = 0;

/*************************************************************************/

void initStack(void)
{
  stackPointer = 0;
}

/*************************************************************************/
 
void pushOntoStack(int number)
{
  stack[ stackPointer ] = number;
  stackPointer++;
}

/*************************************************************************/

int popFromStack(void)
{
  stackPointer--;
  return stack[ stackPointer ];
}

/*************************************************************************/

int stackEmpty(void)
{
  if (stackPointer > 0)	
    return 0;	/* false - the stack is not empty */
  else 
    return 1;	/* true */
}

/*************************************************************************/

stack/main.c

#include <stdio.h>

/* Include the prototypes from the stack module */

#include "stack.h"

/* 
 * NAME:
 *	main()
 * INPUT:
 *	Integers can be typed in one at a time from the command line,
 * 	terminated by the value QUIT_VALUE (see code). 
 * OUTPUT:
 *	The input integers are then printed back to the user, except 
 *	in _reverse_ order.
 * IMPLEMENTATION:
 *	Each integer is pushed onto a stack. To print them in reverse,
 *	they are just popped of the stack, which gives them back in
 *	the opposite order to what they were put in. This is an example 
 *	of a LIFO (Last In, First Out) data structure.
 */

int main(int argc, char* argv[])
{
  const int QUIT_VALUE = -999;
  int number;
  
  /* Initialise the stack */
  
  initStack();
  
  /* Go into a loop, reading integers and adding them to the stack */

  printf("Please enter some numbers(%d to terminate):\n", QUIT_VALUE);

  while (1)
  {
    /* Read in an integer from the keyboard (stdin) */  
    
    scanf(" %d", &number);

    /* If this is the number used to finish inputting, leave the loop */
    
    if (number == QUIT_VALUE)
      break;
   
    /* Else, push the number onto the stack */
      
    pushOntoStack(number);
  }
  
  /* Now we want to print them backwards (if they entered any!) */
  
  if ( stackEmpty() )
  {
    printf("You did not enter any numbers.\n");
  }
  else
  {
    printf("Here are the same numbers, except backwards:\n");
    
    while ( ! stackEmpty() )
    {
      /* Pop the last one pushed */
    
      number = popFromStack();
      
      /* And print it on the screen (stdout) */
      
      printf("%d\n", number);
    }
  }
     
  return 0;
}

stack2/Makefile

CC = gcc
CFLAGS = -Wall 
LIBS = -lm

BIN = bw2
OBJS = main2.o stack2.o

$(BIN):: $(OBJS)
	$(CC) $(CFLAGS) -o $(BIN) $(OBJS) $(LIBS)

main2.o:: stack2.h

stack2.o:: stack2.h

clean::
	/bin/rm -f *~ *.o

stack2/stack2.h

#ifndef stack_h
#define stack_h

#define MAX_STACK_SIZE		100

typedef struct 
{
  int stack[MAX_STACK_SIZE];
  int stackPointer;
}
Stack;

void initStack(Stack* s);
void pushOntoStack(Stack* s, int number);
int popFromStack(Stack* s);
int stackEmpty(Stack* s);

#endif

stack2/stack2.c

#include <stdio.h>
#include "stack2.h"

/*************************************************************************/

void initStack(Stack* s)
{
  s->stackPointer = 0;
}

/*************************************************************************/
 
void pushOntoStack(Stack* s, int number)
{
  s->stack[ s->stackPointer ] = number;
  (s->stackPointer)++;
}

/*************************************************************************/

int popFromStack(Stack* s)
{
  (s->stackPointer)--;
  return s->stack[ s->stackPointer ];
}

/*************************************************************************/

int stackEmpty(Stack* s)
{
  if (s->stackPointer > 0)	
    return 0;	/* false - the stack is not empty */
  else 
    return 1;	/* true */
}

/*************************************************************************/

stack2/main2.c

#include <stdio.h>

/* Include the header file for the stack module */

#include "stack2.h"

/* 
 * NAME:
 *	main()
 * INPUT:
 *	Integers can be typed in one at a time from the command line,
 * 	terminated by the value QUIT_VALUE (see code). 
 * OUTPUT:
 *	The input integers are then printed back to the user, except 
 *	in _reverse_ order.
 * IMPLEMENTATION:
 *	Each integer is pushed onto a stack. To print them in reverse,
 *	they are just popped of the stack, which gives them back in
 *	the opposite order to what they were put in. This is an example 
 *	of a LIFO (Last In, First Out) data structure.
 */

int main(int argc, char* argv[])
{
  Stack stk;			/* Allocate a new stack */
  const int QUIT_VALUE = -999;
  int number;
  
  /* Initialise the stack */
  
  initStack(&stk);
  
  /* Go into a loop, reading integers and adding them to the stack */

  printf("Please enter some numbers(%d to terminate):\n", QUIT_VALUE);

  while (1)
  {
    /* Read in an integer from the keyboard (stdin) */  
    
    scanf(" %d", &number);

    /* If this is the number used to finish inputting, leave the loop */
    
    if (number == QUIT_VALUE)
      break;
   
    /* Else, push the number onto the stack */
      
    pushOntoStack(&stk, number);
  }
  
  /* Now we want to print them backwards (if they entered any!) */
  
  if ( stackEmpty(&stk) )
  {
    printf("You did not enter any numbers.\n");
  }
  else
  {
    printf("Here are the same numbers, except backwards:\n");
    
    while ( ! stackEmpty(&stk) )
    {
      /* Pop the last one pushed */
    
      number = popFromStack(&stk);
      
      /* And print it on the screen (stdout) */
      
      printf("%d\n", number);
    }
  }
     
  return 0;
}


Torsten Seemann
1999-02-16