UP ONE LEVEL: ENGG 333 Course Handouts

C Program Modules in ENGG 333
Class Handout for ENGG 333, Fall 1996

Author: Steve Norman
Date of first publication: 20 October 1995
Minor changes made for Fall 1996
Paper copies handed out Friday, October 11
Last modified: Wed Oct 9 17:20:17 MDT 1996

Contents


Introduction

In Lab Assignments 1 through 4 each program you worked with was written in a single .c file. Starting with Lab[5], you will modify or build programs that are collections of modules, along with a main program file.

For the purposes of this course a module is defined to be either:

Our definition of module is a definition that many programmers, books, and courses would agree with, but you should keep in mind that other definitions of the word are in common use. For instance, some programmers unfortunately use the word module to refer to a single function.

The header file defines the interface to the module. It contains the prototypes for the functions the module exports (makes available) to other modules and to the main program file. It may also export #define directives and type declarations if such things are needed to use the exported functions.

The implementation file contains definitions for the functions listed in the header file, plus whatever code is necessary to support these function definitions. Typically, the supporting code will consist of #define directives, type declarations for use only within the implementation file, declarations of static global variables, and prototypes and definitions of static functions. All of this supporting code is private to the module. As far as the main program and other modules are concerned, it is hidden information.

If you have worked with Turbo Pascal unit files, it may be helpful to note that a single Turbo Pascal unit file plays a very similar role to that played by the pair of files that make up a C module. The interface section of the unit is like the header file of the C module, and the implementation section of the unit is like the implementation file of the C module.

[back to top of document]


Vending Machine Analogy

A software module can be compared to a vending machine that sells canned soft drinks.

The interface to a module is like the front panel of the vending machine. The front panel allows the customer to perform a limited number of operations: to check whether the machine has run out of the customer's preferred kind of drink; to insert coins; to retrieve change; and to press a button to select which kind of drink the machine will deliver.

The implementation of a module is like the inside of the vending machine, which contains drinks, a system for storing and dispensing drinks, refrigeration equipment, money, a system to sort coins, count money and dispense change, and so on. What all of this stuff looks like and how it works is hidden from the customer. The customer trusts the machine to respond correctly to coin insertion and drink selection without knowing exactly how that will happen. This is a good example of information hiding.

The benefit of information hiding to the vending machine customer is simple but important:

The machine makes it easy to buy a drink.
The customer doesn't have to worry about details of money counting or motors and gears or temperature control.

Information hiding has many benefits for the vending machine owner. Here are two important ones:

Protection of system integrity. Because of the limited number of controls on the front panel it is hard for customers to inadvertently (or maliciously) foul up the operation of the machine. There is no way a customer can cause the cans of Diet Glorp to be replaced with cans of Sugary Glorp, there is no way a customer can change the price of a drink to 15 cents, and so on.

Easy modification of internals. Customers do not rely on knowledge of the insides of the machine when they buy drinks. If the owner changes the insides of the machine, for example improving its energy efficiency or reconfiguring drink storage to make more room for the more popular flavours of drink, the customers, unaware of the internal changes, will continue to use the machine the way they always have.

The above discussion of vending machines should be easy to understand, but transferring the ideas back to software design may be difficult. It's fairly easy to think about the different physical parts of a vending machine, even if (like me) you don't really know what those parts look like. It's a bit harder to think of a computer program as a collection of components, because the components are conceptual instead of physical.

Another problem students have in learning to think in terms of modules is that students in programming courses are usually asked to play the roles of designer, builder and user all at the same time. When you're writing function prototypes for a C header file you should have a radically different point of view from the point of view you have later when you're writing function definitions in an implementation file. Looking at one project from multiple points of view is a difficult skill that takes time to learn. It's worth learning not just for software development but for all branches of engineering and management.

[back to top of document]


static Functions and Global Variables

The C keyword static has two unrelated meanings. This section discusses the meaning of static applied to functions and global variables.

A static function may be called only within the .c file where it is defined. In other words, it is private to a single implementation file. The role of a static function is to be an ``helper'' to one or more of the functions exported by a module. The keyword static must appear both in the function prototype and the function definition of a static function. Since a static function is not exported by a module, its prototype should appear in the implementation file, not the header file.

Access to a static global variable is shared by all of the functions in the .c file where the static global variable is declared. Functions in other .c files do not have access. Such global variables are required whenever a module manages data that must be ``remembered'' between calls to its exported functions. Because it is private to an implementation file, a static global variable is much less evil than a global variable that can be scribbled on by every single function in a program.

When applied to local variables, static has quite a different meaning. See Sections 8.1, 8.3, and 8.6 of your textbook for details on the different meanings of static.

[back to top of document]


Header File Organization

Here is how you should lay out module header files:
top-of-file comment

#ifndef transmogrified file name
#define transmogrified file name

#include directives (if necessary)

exported #define directives (if necessary)

exported struct and typedef declarations (if necessary)

prototypes and function interface comments for functions exported by the module

#endif /* transmogrified file name */

The rules for the top-of-file comment are the same as for Labs 2-4: include the name of the file, your name, U of C ID number and lab section number, and the lab assignment number and exercise letter.

Function interface comments should be written in terms of restrictions on argument values and in terms of services provided by the module. Since information about how the functions work is supposed to be hidden, do not describe the behaviour of the function by referring to information in the implementation file.

The transmogrified file name is an identifier based on the name of the header file, altered to look like a preprocessor name. For example, fred.h would generate FRED_H as a transmogrified file name. The purpose of the #ifndef - #endif wrapper for the header file is to allow for repeat inclusion of header files. This protects against silly errors like

#include "bozo.h"
#include "bozo.h"
but that is not the main point. The main point is to protect against subtle errors like
#include "clown.h"
#include "bozo.h"
where the programmer is unaware that #include "clown.h" appears within the file bozo.h. Writing header files this way in ENGG 333 exercises may seem unnecessarily elaborate, but it's a very good habit to adopt if you plan to work with larger C programs in the future.

Remember that #define directives and type declarations should be exported only if they must be exported to support the functions exported by the module.

[back to top of document]


Implementation File Organization

Here is how you should lay out module implementation files:
top-of-file comment

#include directives (must include own header file)

private #define directives (if necessary)

private struct and typedef declarations (if necessary)

prototypes for static functions (if necessary)

declarations of static global variables (if necessary)

function definitions

Again: the rules for the top-of-file comment are the same as for Labs 2-4.

A module implementation file must include its own header file to ensure that function prototypes in the header file are consistent with function definitions in the implementation file.

Formal function interface comments are not required for static functions because formal documentation is unnecessary for functions that are not exported by a module. However, you are encouraged to write a function interface comment whenever it would be effective in explaining the role of a static function.

[back to top of document]


Main Program File Organization

Here is how you should lay out module implementation files:
top-of-file comment

#include directives (must include at least one module header file)

private #define directives (if necessary)

private struct and typedef declarations (if necessary)

prototypes for static functions (if necessary)

declarations of static global variables (if necessary)

function definition for main

definitions for static functions (if necessary)

One more time: the rules for the top-of-file comment are the same as for Labs 2-4.

[back to top of document]


Example Source Code

This section describes some example source code that demonstrates how to write and use a module. If you're reading this with Mosaic or some other Web browser, use the links below to view the code.
  • module header file: boo.h
  • module implementation file: boo.c
  • main program file: testboo.c
  • If you're reading the paper version distributed in class, you will find the source code printed on the last two pages of the handout. The source is also available on the DECstations in the directory /courses/engg333/boomod

    The point of the example is not to show why modules are good, but simply to show how C modules are organized. (Most readers will agree that the program is rather silly.) The benefits of modules are not really apparent in a tiny program such as this. Upcoming lab exercises will let you work with or build more useful modules.

    Header file: The module exports two functions: say_boo and change_boo_volume, so there are two function prototypes in the header file. There are also some #define directives. The #define directives appear in the header file and not the implementation file because they are needed by callers of the change_boo_volume function.

    Implementation file: In addition to the say_boo and change_boo_volume functions, the implementation file contains some helper functions. The helper functions are private to the implementation, so they are declared to be static. There is a static global variable called volume_setting, which both say_boo and change_boo_volume need access to.

    Defect detection code: The use of assert in the definition of change_boo_volume and the code belonging to the default case in the switch statement in say_boo are examples of what could be called ``defect detection code'' - code that helps programmers find program flaws. These two pieces of code both cause the program to crash in ways that offer programmers considerable information about what went wrong. They are not intended to deal with user errors, since the messages they generate will be meaningless to a typical user. The use of abort rather than exit is advantageous for programmers who wish to examine programs with a debugger like gdb.

    Main program file: There's nothing that needs to be said about this file. Compile and run the program if you're not sure what it does.

    [back to top of document]


    A Module Should Be Cohesive

    One property of a well-designed module is strong cohesion. A cohesive module is one which provides a well-organized collection of closely related services. If a module provides many seemingly unrelated services, it has not been designed with cohesion in mind.

    The example program, although trivial, nevertheless provides an example of a cohesive module. The services listed in the header file boo.h are clearly closely related.

    The standard library header file <string.h> is an example of a cohesive interface: all of the services provided are operations on character strings.

    On the other hand the <stdlib.h> header file is not a very cohesive interface. It contains prototypes for the program termination functions abort and exit, for the dynamic memory manager functions malloc and free, for pseudorandom number functions, and for many other miscellaneous library functions.

    (Note: I have avoided using the word module when referring to the library, because the source and object code organization of libraries is generally much more complex than that of the modules you will create or modify in ENGG 333.)

    [back to top of document]


    Coupling Between Modules Should Be Loose

    If the main program or a module uses services from another module, the two pieces of software are said to be coupled. Coupling is unavoidable; if there were no coupling, you would have a collection of programs instead of a single program.

    However, certain kinds of coupling are much better than others. Sharing constants and interacting by means of function calls are both acceptable forms of coupling.

    There are at least two forms of coupling that should generally be avoided. Creating global variables that all modules can modify introduces a nasty form of coupling. So does creating functions that return pointers to the private data of modules. There are two main reasons why these forms of coupling are bad:

    (Modules do not really have lives of their own, although in some large projects it may seem that way. What I mean by the evolution of a module is the sequence of modifications made by members of a software development team.)

    [back to top of document]