Torsten Seemann
School of Computer Science and Software Engineering,
Faculty of Information Technology, Monash University
Revised 16 February 1999
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.
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.
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.
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!
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.
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.
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.
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.
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.
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.
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.
-Wall option when compiling under Unix,
or turn on ``All Warnings'' on a DOS compiler. This will help to isolate
missing prototypes and other errors.
static keyword wherever possible to prevent illegal access
to variables or functions you wish to be private to a module.
#include a module's header file in the module's own source
code. This will help the compiler to ensure the prototypes in the .h
file match up with the real function definitions in the .c file.
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
#ifndef stack_h #define stack_h void initStack(void); void pushOntoStack(int number); int popFromStack(void); int stackEmpty(void); #endif
#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 */
}
/*************************************************************************/
#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;
}
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
#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
#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 */
}
/*************************************************************************/
#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;
}