CHAPTER 19
ERRORS,
EXCEPTION HANDLING, AND SOFTWARE ENGINEERING
While
your program is running, you may encounter a problem that is unexpected.
This problem can bring down the whole system. For example, your program
may want to access a file that does not exist or, is in another
directory. What would happen if your program is computing the average of
a series of numbers, but the counter (denominator) is zero or becomes
zero, or what happens to your program when it requests more memory than
compiler can provide? Obviously these are unexpected situations that
rarely happen and if they occur in an unguarded program, they may cause
severe consequences. The strategy to handle errors is to provide a
systematic way of detecting them and take the necessary actions to fix
them and recover the program.
You
may have already taken proper measures to combat the errors in your
program simply by using an if-statement accompanied by a display
message. The introduction of the assert(
) function from header #include <cassert>
is more advanced and takes advantage of the system error messages and
aborts the program upon an unwanted situation. One might argue that the
assert( ) function will not be
able to recover from the bug. Rather than aborting the program, you can
take a different action when the bug occurs. A more sophisticated and
systematic approach for error handling is known as exception handling
that was introduced by ANSI-C++ standard and added to C++. It is called
an exception since it is unlikely to happen, but when it does happen it
may produce unwanted results. Exception handling consists of enclosing
the code that may result in an exception in a try
block and if an exception occurs it is thrown. The statement that
handles the exception is inside the catch
block. After an exception is thrown (raised), normal control flow will
suspend and the program will search for a match or a portion of the
program that can handle the exception. After the exception is handled
the program will either terminate or resume normal execution after the
corresponding catch block.
Use of
exception handling can separate error reporting from error handling and
as a result handling errors becomes systematic and readable.
In
conclusion, use of exception handling does not guarantee that errors
will not occur. There is no guaranteed solution in preventing errors.
One should consider having a fault tolerance system.
CATEGORIES OF BUGS
Programming bugs (errors) can be categorized into three kinds: compile
time, run time, and logical errors. Syntax or compile time errors are
known as grammatical errors that are detected by the compiler. Examples
of syntax errors are misspelling the keywords, omitting the necessary
punctuation such as a semicolon, or not balancing the quotation,
parenthesis, or statement pairs. Run-time errors are errors that occur
during the execution of the program when it attempts to perform an
operation that the system cannot perform (illegal). A popular example of
a run-time error is when a program tries to divide a number by zero.
Logical errors produce the wrong output, which is caused by applying
inappropriate algorithms; for example when subtraction is used instead
of addition. One analogy of a logical error would be driving east when
intending to go west, and the driver will never reach the desired
destination if given the wrong directions. Remember, in order for a
program to produce output, the program has passed the stage of syntax
and run-time error checking. Logical errors are the most difficult to
detect and they are often confused with run-time errors. If there is a
run-time error, the system will terminate or an exception will take
place. Linkage errors might result when linking two separately compiled
programs (functions). Duplication of variable names is an example of a
linkage error. There are different opinions as to what is considered a
logical error and what is not. Errors can be categorized with respect to
the compiler as follows: lexical errors, such errors resulting from
misspelling; syntactic errors, such as unbalanced quotations or
parentheses; semantic errors, such as applying an arithmetic operator to
incompatible variables (data types), and logical errors, such as calling
a recursive function without an exit (infinite).
SYNTAX
VERSUS SEMANTIC ERRORS
Another form of error that is often detected by a compiler is known as a
semantic (meaning) error. Syntax errors violate the language structure
rules while semantic errors are syntactically correct but meaningless.
An example of a semantic error is to perform arithmetic operations on 2
non-numeric variables, such as multiplication of 2 strings. Another
example of a semantic error is performing arithmetic operations on the
names of 2 functions (procedures) that do not return useful values. If
the compiler does not detect a semantic error, it becomes a run-time
error/logical error that is difficult to detect. For instance,
associating an else to the
wrong if statement, or applying
the wrong precedence of arithmetic operators.
A common semantic error
which is not detectable by compiler is using = when = = is the intended
meaning. The semantic errors are more about understanding the meaning of
the language constructs as opposed to problem solving. Logical errors
are referred to as problem solving errors than errors resulting from
language construct semantics.
SHOULD COMPILER ERROR MESSAGES BE
TRUSTED
A compiler’s job is to
detect any errors that violate the language structure and report the
error to the user. If there are no errors then it converts the program
to a low level language (object code) so that it can be executed.
Detecting the error and reporting it accurately isn’t an easy task for
compilers. Beginners put all their trust in the compiler because it is
seen an important program that interprets programs. Due to incorrect and
confusing error messages, dependence on the compiler’s messages
diminishes. With experience, error messages can be interpreted and found
useful. One may wonder why a compiler cannot report errors accurately
given all the technology we have. The following are some explanations:
the line number reported may not be accurate due to the fact that the
compiler has already proceeded to the next statement. Another reason
for inaccuracy in error reporting is that the statement that contains
the error has been eliminated, leading to more errors.
RUN-TIME ERROR - FILE DOES NOT EXIST:
SIMPLE ERROR CHECKING
The
following program demonstrates how error checking can be simply
performed by placing an if
statement with a report message. The program below displays a message if
the file does not exist and exits the execution rather than leaving it
to the system to crash.



DIVIDE BY ZERO: A SIMPLE ERROR HANDLING
PROGRAM
The program below
illustrates a simple error check for divide-by-zero. An if
statement determines if the denominator is zero and based on that a
message is displayed and the program is aborted.



OTHER RUN TIME ERRORS
In the preceding
programs, we demonstrated two run-time errors: when the file does not
exist and when the program divides by zero. Other run time errors are:
INSUFFICIENT MEMORY:
This error results when a program tries to allocate memory
dynamically, and there is not enough available memory such as:
phw=new int [100000];
INFINITE RECURSION:
Behind the scenes of a recursive function is the stack. On each call to
the function, values such as parameters and local variables are pushed
into the stack and on the return the values are popped from the stack.
If the recursive function never terminates, values will be pushed to the
stack with every call and at some point the stack will be exhausted
(overflowed) and crash will occur.
RUN-TIME ERROR:
READING PASSED END OF FILE
A common run time error
for novice programmers is when reading input data, especially from a
data file, trying to read passed the end of the file. For example, what
would happen if there are only 3 input data in the data file and you try
to read 5 data in your program? One would expect that only 3 values
would be read and the program would request the additional data however
this is not the case. The following program was run on Visual C++6.0,
GNU C++, and TURBO C++ 3.0. In all cases, the last data in the file
repeated for the remainder of the loop, even after the end of file was
reached. Imagine if a payroll system had a loop for writing checks and
the end of file was reached before the loop ended. In this case the last
person in the data file would keep receiving checks until the loop ends.
There are several solutions to this problem. The user can check the
cin.eof( ) to
break the loop, or check the status of the other input stream member
functions such as: bad( ), fail( ), or good( ).
Note that these functions return a Boolean value.
![Text Box: #include<fstream>
#include<iostream>
using namespace std;
int main(){
ifstream fin("employees.txt",ios::in);
char fname[20], lname[20];
for(int i=0; i<10; i++){
fin>>fname>>lname;
cout<<fname<<" "<<lname<<" ";
cout<<"1,000.00"<<endl;}//FOR
return 0;
}//MAIN](http://www.geocities.com/lonairl1/chap19_files/image009.gif)




C++
DEBUGGER: BREAKPOINTS, WATCHING VARIABLES, SINGLE STEPPING
Most of the C++
compilers (Visual C++, Borland C++, and CodeWarrior) come with an
Integrated Development Environment known as IDE, providing an
environment with many features including editing, compiling, and a
debugging program. With a debugger one can run the program and watch the
values or set breakpoints and observe how the values of the variables
change as stepping through the code. With the help of a debugger one can
detect and correct the bugs. Using a breakpoint in a program will enable
one to execute the program up to the breakpoint and see the code line by
line and inspect variable values. Every compiler has it’s own debugger
that might be a little different than others. If one can’t use the
debugger one can debug a program by inserting cout statements to
display the contents of the variable. For example one can insert a
cout statement inside a loop, if statement, or a function to
inspect the control flow and check the content of a variable. Comments
/* */ can be used to isolate the problem in the program. If a Unix C++
compiler (e.g. g++ , GNU ) is used the debugging information can be
obtained by typing the command man. On Dos/Windows environments
main compilers are from: Borland, Microsoft, and CodeWarrior. Among the
popular C++ compilers for the Macintosh environment are Symantec C++ and
CodeWarrior.
ASSERTION AS A DEBUGGING TOOL
In order for a program
to continue, the assertion must be true at the point where the
assert( ) function is placed. For an assertion to be true, the
parameter passed to the assert function must be true. If the assertion
fails (false), the execution will be aborted and the message generated
by the language will be displayed. The assert function can be used as a
debugging tool to trace a program at any given point in the program.
The assert routine
(macro) takes an expression (argument) that either evaluates to true or
false. The directive #include <cassert> must be included to use
assert( ). If there is no additional need for the assertion in
debugging the program, assert( ) can be disabled by simply having
a #define NDEBUG before the assert include header.