Debugging, a Guide
By
Thomas E. Zammikiel
Copyright 2003
The second part can't be done until after the first is done.
Testing only finds errors; it does nothing about correcting them.
Compilers report syntax errors when the source code doesn't adhere to the rules of the
language. This is supposed to be a document about debugging, why care about syntax
errors? If your source code doesn't even compile correctly, you won't be able to debug at
all.
It also gives me the opportunity to give this piece of advice: Only pay attention to the
error messages in the beginning of the compiler's list. The error message's usefulness to
you, in fixing the syntax errors, usually decrease the farther down the list the error
message occurs.
An infinite loop occurs when the terminating condition of a while loop is never false. They can usually be detected when a program "hangs". A program that hangs doesn't ever terminate.
Here is a Java code fragment of an infinite loop for example:
int ind = 0;
int [] temp = {1, 3, 5, 7 };
while (ind < temp.length) {
// other statements with no assignment to the variable "ind"
}
If the variable "ind" is never greater or equal to temp.length the loop will never terminate.
The correct way to code such a loop is as follows:
int ind = 0;
int[] temp = {1, 3, 5, 7};
while (ind < temp.length) {
// Here are other statements that usually don't assign anything to the variable "ind"
// Followed by a statement that increments "ind".
ind ++; // increment the counter
}
Once on an interview for a teaching position, I coded a while loop and forgot not only to increment the counter at the end of the while loop, but I forgot the statement in its entirety. This happened after years of programming! It can happen to anyone. (I didn't get that particular job)
To stop the program you will have to halt the program by using the method supplied by your operating system. On windows system's you should be able to type CTRL-BREAK.
In the infinite loop example above, you can detect it for certain by adding a print statement to the source code, save the source code, recompiling the program, and re- executing the program. From now on when I say add a line to the source code I mean that it will have to be added, saved, recompiled and the program re-executed. If any other meaning is intended it will be expressed explicitly.
Here is a Java Code Fragment example:
int ind = 0;
int[] temp = {1 3 5 7 }
While (ind < temp.length) {
System.out.println( "ind =" + ind);
}
The output to your terminal will be a sequence of lines as follows:
ind=0
ind=0
ind=0
ind=0
That never stops.
The use of the print statement is often called logging because one doesn't have to write to the terminal. The programmer could redirect the output to a file. More then one error could be written to that file. That is called logging the errors. Often a date and timestamp is added to the error messages. Also, the name of the class and method in which the error occurred is often added.
It wouldn't be prudent to have error messages from a infinite loop redirected to a file, you will eventually run out of disk space. You can mitigate this by having a limit to how many lines you write to a file, after which you delete the file and start writing at the beginning of the file again.
Often the programmer doesn't want the error messages used to debug to appear in production code. A way to do this is to have a variable flag called debug which is set to true when one wants the debugging messages and false when one does not. This is usually encapsulated in a method such as:
Logger.outdebug(String errorMessage);
if (debug) {
System.out.println(errorMessage);
}
for (int i = 0; i< temp.length;i++) {
if (i > 2) { // Here's the condition that must be met before outdebug is invoked.
Logger.outdebug("i is now=" + i);
}
Many times, miscounting causes an error. Usually by being off by one. For example there are five numbers from zero to four, inclusive, not four. They are 0, 1 ,2, 3, and 4. There are five of them. The same thing is true for ranges that don't start at zero. For example there are five numbers from six to ten, inclusive. They are 6 , 7 , 8 , 9 and 10. That is why you will often see code fragments that look like:
tempSize = (lastIndex – firstIndex ) + 1;
Using that last example, (10 – 6) + 1 equals 5.
Different languages index arrays at different starting points. The languages c, c++, and Java all start indexes at zero. Smalltalk indexes start at one. Programmers that know and program in more then one language often get the notion confused.
The following code fragment assumes a one start index when a zero index should be used. It also uses a boolean conditional that is usually used correctly with a zero index, incorrectly used with a one index start. These two errors together can cause a programmer endless grief. It will return an incorrect sum, but otherwise will execute correctly. (No out of bounds error will be raised on the array, since "ind" never passes the value three) As you may have guessed this can be difficult to debug, because your only indication will be an incorrect numerical result.
sum = 0;
int[] temp = { 3, 4, 5, 6};
for (int ind = 1; ind < temp.length; ind++) {
sum = sum + temp[ind];
}
return sum;
The correct code is as follows:
sum = 0;
ind [] temp = {3 4 5 6 };
for (int ind = 0; ind < temp.length; ind++) {
sum =sum + temp[ind];
}
return sum;
The c language uses sentinels to end strings. This can cause you problems. The number of characters need to store a string is one more then the string length, because the sentinel character, referred to as /0 must be stored also. The c language does no bounds checking on strings, so the following code will compile and run without reporting errors,
Char first[29] = "This will not copy correctly"; Char second[25]; /* because this is too small */ Strcpy(second,first);
But the problem is that the actual space taken up by the characters and sentinel is 29 in variable first while only storage for 25 characters is allotted in variable second. The strcpy function will write past the end of second into memory as if there was storage allocated for the 26 , 27 , 28, and 29 th index of the array second. There is no bounds checking of arrays in c. Those storage locations could be used by other variables in the program, which will now have most likely incorrect values.
Usually typographical errors (from now on I'll call them typos) are caught by the compiler as syntax errors, but there is one kind of typo that is insidious. That's when you type in a different variable then the one you meant but one that has a definition in your program.
Programmers are human and cannot concentrate perfectly for indefinite periods of time. The mind can wander and often does. So the obviously wrong code below can be written, even if one knows that the area of a circle is pi time the radius squared and not the diameter. Especially if there are variables for both diameter and radius are defined in your program
Incorrect:
rea := Math.pi * (diameter^2);
correct:
area = Math.pi * (radius ^2);
Another example follows:
This error often happens when programmer's mixes the ideas of height and width with x and y axis of geometry
Height naturally fits with the y axis and width naturally with the x axis.
So this incorrect code:
height = top.x – bot.x; width = top.y – bot.y;
should be:
height = top.y - bot.y; width = top.x – bot.x ;
In c it is possible to assign values to variables inside the Boolean condition in if statements.
The following c code fragment has two incorrect results from one error:
if (test = 5)
printf("It was a five\n");
else
printf("It wasn't a five\n");
The code will assign 5 to the variable test, and it will print out: It wasn't a five", neither of which is intended.
The correct c code fragment is:
if (test == 5)
printf("It was a five\n");
else
printf("It wasn't a five\n");
The c code fragment with the error assigns the value of 5 to test, instead of checking the value of test to see if it equals 5. This is because the assignment operator in c is "=" and the Boolean test of equality is the operator "==" (Two equal sign, one immediately after the other).
This happens in the language Smalltalk also, but in reverse sense. The assignment operator in Smalltalk is the same as in Pascal, it is ":=". The equality operator is "=" the opposite from the Boolean equality ("==") operator in c. So it's not hard to make the mistake and write. test = 5. in Smalltalk. This statement compares test to 5, returning a value of true, which is thrown away, because there is no assignment. Most likely, not what was intended.
The switch statement is used when multiple flows of control are needed, and they are much cleaner to read then their nested if statement cousins. They may also be more efficient. The flow of control is decided by the value of the switch expression, which is right after the switch keyword in Java. If the value of the expression, for example, is five, the flow of control will go to the statements following the Case 5: tag of the switch statement. If the statement has no Case 5: tag, flow of control will go to the default: tag If there is no default: tag either, then none of the statements of the switch statement are executed.
There is a problem with this construct, after all the code for a particular tag is executed, flow of control is not automatically transferred to the end of the switch statement after the next tag occurs. This only happens if there is a break statement, otherwise, the code "falls through" to the next tag and executes that code. This continues until there is a break statement or the end of the switch statement occurs. Rarely is this the behavior what was intended.
The following switch code fragment is incorrect, it's missing a break statement after the case of 1.
switch (x) {
case 5: System.out.println("The piece is a Rook");
Break;
case 1: System.out.println("The piece is a Pawn");
case 9: System.out.println("The piece is a Queen");
Break;
default: System.out.println("The square is empty");
}
So When the programmer wanted the only the line:
The piece is a Pawn.
to display, what actually was displayed was:
The piece is a Pawn
The piece is a Queen
The correct code fragment is as follows:
switch (x) {
case 5: System.out.println("The piece is a Rook");
Break;
case 1: System.out.println("The piece is a Pawn");
Break; // correct break statement was added here
case 9: System.out.println("The piece is a Queen");
Break;
default: System.out.println("The square is empty");
}
Zero, one, and many.
When writing code that access database records, I have found it advisable to write tests
that show how my code performs when there are no records of that type (The zero case),
when there is only one record. (The one case). And when there are many records of that
type (The many case). If you know that your code performs correctly in all these cases,
you can have greater confidence that your code performs without bugs in those sections
of code.
Repeatability.
If you cannot repeat a programs error, it will be extremely difficult to debug that
program. It's not impossible (see desk checking below), but I assure you repeatability
makes the task of debugging much more amenable to fixing the error.
Assignment semantics.
The assignment operator has different ways of working in Object Oriented Langauges
versus procedural languages. For example in the language c
A = 5 ;
B = A;
B = B + 1;
Will result in A having the value 5 and B having the value 6 while the similar code in
Smalltalk will have a different result.
A = 5.
B = A.
B = B + 1.
Here the variable B has the value 6 and so does A! That's because A and B refer to the
same object.
If you wanted the same result as the c code you would have to write.
A = 5.
B= A copy.
B = B + 1.
Hybrid languages (Languages that have procedural and object oriented features) such as
java have an added complexity. I'll use Java as an example. Java has primitive types,
which are not objects.
So the code:
Int a;
Int b;
A = 5;
B = A;
B = B + 1;
Println( "A=" + A + " B=" + B);
Will print A=5 B=6
It does this because primitive variables in Java use procedural assignment semantics
while object variables use object assignment semantics.
It pays to keep the distinction in mind.
Desk Checking.
Desk checking is the process of debugging by simulating the execution of the program by
using the source listing, a desk, some paper, and a pencil. You write down each variable
and record it's state each time as assignment is made. You interpret the program one line
at a time as if you were the computer executing it. (This is a useful skill to have when the
power goes out).
Errors can be found this way but it is time consuming. Sometimes for stubborn errors it is
quite effective, however. Symbolic debugger programs automate desk checking by the
use of breakpoints and the ability to display the value of variables at those breakpoints.
Request for comments and suggestions.
If you have any comments, corrections, or suggestions for material to be added to this
document please send an E-mail to the following address.Tom@puzzware.com
Please be advised any e-mail and any content, you send to me, will become my property and be
used as I see fit. If you do not agree to those terms, don't e-mail me.
I am, however eager to, hear from you, and will add material as time goes by.