Defensive Programming Techniques

 

The topic is an overview of defensive programming techniques with a view to making your testing and debugging much easier.

 

PREAMBLE

The best way to achieve working programs is to write the programs very carefully in the first place.

 

DEFENSIVE PROGRAMMING

Only a naive programmer (or a truly brilliant one) believes that his or her program will work first time and be totally correct. You will be able to locate those errors much more rapidly if you adopt some simple programming strategies. Although these strategies may slow you down a bit, that is probably a good thing in any case!

(Programming examples follow in later sections.)

 

CHECKING RETURN CODES FROM LIBRARY FUNCTIONS

The memory allocation functions (malloc, calloc, realloc) are particularly important for return code checking. Always write your code like this:

		p = calloc(numElements, ELEMENTSIZE);
		if (p == NULL) {
		    fprintf(stderr, "malloc failed!\n");  abort();
		}

The functions for accessing files are also very important (because they can fail for reasons quite outside the program's control). Therefore, use code like this to open a file:

		fp = fopen("mynewfile", "w");
		if (fp == NULL) {
		    perror("mynewfile");  abort();
		}

and it is a good idea to also check that writes to files succeed ...

		if (fprintf(fp, "%d", n) < 0) {
		    perror("mynewfile");  abort();
		}

It is conceivable that disk storage is becomes exhausted and causes the write to file, but other error reasons are also possible. [The perror() function, used above, is explained next.]

 

USING THE PERROR MACRO

Many of the file handling functions in the C library store an error reason code in an integer variable named errno when they discover an error situation. Therefore, if a function returns an error status code, you can usually check the value of errno to find out more about what went wrong.

The following is plausible coding:

	extern int errno;
	...
	fp = fopen("mynewfile", "w");
	if (fp == NULL) {
	    fprintf(stderr, "mynewfile: error code = %d\n", errno);
	    abort();
	}

However, textual error messages are much more useful than numeric error codes. The perror() function simply looks up the error reason code in a table and outputs the error description on the standard error stream, prefixed with the text provided by you. This text is typically the name of the file that we are having trouble using. In the code above, we should replace the fprintf call with

	perror("mynewfile");

Note that if you want your program to continue execution after reporting the error (rather than calling abort), you may have to clear an error indication associated with a file before you carry on. E.g.

	clearerr(fp);

must be performed before your program attempts some other action on the file referenced by fp.

 

USING THE ASSERT MACRO

The <assert.h> header file defines a macro named assert. It can be used as in the example below.

 

CHECKING ARRAY INDEXES IN C

No standard C compiler generates code to check that array subscripts re within range (though some C development environments, such as CodeCenter, do provide that capability). If you want such checking in a C program, you must do it yourself. [This is a good reason to consider switching to the C++ language, because redefining the array index operator to perform subscript range checking is easy.]

The easiest approach in C is to define macros for accessing the arrays.

For example, suppose that our program declares this array:

	arr[100];

and there are subsequent uses of the array, such as

	arr[i] = arr[j]+1;

We could add the following function to our program:

	void checkIndex( int index, int size ) {
	    if (index < 0 || index >= size) {
		fprintf(stderr, "index out of range\n");
		abort();
	    }
	}

and expand the array declaration by adding a macro definition alongside:

	int arr[100];
	#define ARR(x)	arr[checkIndex((x),100),(x)]

Now, our uses of the array can be written like this:

	ARR(i) = ARR(j) + 1;

You would probably like some way of disabling the array checking after the program has been debugged and tested. We can make disabling easy by writing the macro definition like this:

	#ifdef ARRAY_DEBUG
	#define ARR(x)	arr[checkIndex((x),100),(x)]
	#else
	#define ARR(x)	arr[x]
	#endif

 

CHECKING POINTERS IN C

Pointer variables with undefined values, or pointers that have become invalid because they reference memory that has been de-allocated, can lead to very mysterious errors. Almost any imaginable error symptom can appear if your program writes to memory via a bad pointer value.

Again, it is easy in C++ to extend the meaning of the prefix * (dereference) operator so that the pointer is checked before it is used. In C, we should define a macro. Here is a simple example.

If the program contains a declaration like this:

          LISTELEMENT	*np;

and a use like this:

          np->name = "Mr Smith";

we can add the following function and macro definition to our program:

	void checkPointer(void *p) {
	    if (p == NULL) {
		fprintf(stderr,
		    "Attempt to dereference NULL pointer\n");
		abort();
	    }
	}
 
	#define PCHECK(x)	(checkPointer(x),(x))

and our example use of the pointer should be coded like this:

		PCHECK(np)->name = "Mr Smith";

Note that all pointer variables should be initialized to NULL when they are declared, and pointers should be reset to NULL after a call to free(), for this checking strategy to be most effective.

 

COUNTING LOOP ITERATIONS

Infinite loops can be aggravating to debug. Remember that you can interrupt a non-terminating program with the Quit signal -- generated by typing ^\ (control-backslash) -- and that will force a core dump which can be analyzed with dbx. But you will find the program much easier to debug if it contains no indefinite loops.

If you have a loop like:

	while( some_condition ) {
	    ...
	}

you can convert it into the following form:

	#define MAX_ITERATIONS	100000
	int limit;
 
	...
	limit = 0;
	while( some_condition ) {
	    if (++limit >= MAX_ITERATIONS) {
		fprintf(stderr, "Infinite loop at line %d\n",
		    __LINE__);  abort();
	    }
	    ...
	}

[See below for an explanation of __LINE__.]

 

MISCELLANEOUS

ANSI C compilers provide two built-in macros, __LINE__ and __FILE__. (These names begin and end with two underscores.) The __LINE__ macro expands to the current line number in the current file; the __FILE__ macro expands to the name of the file being processed. You can use these macros to make your error messages more informative.

(The assert macro, as defined in <assert.h>, will print the file name and line number along with the text of the assertion test that has failed.)