Tonight we examined the use of exceptions. C++ incorporates exception handling in a way very similar to Java. We'll use the intStack class of 4/4 to illustrate. The two potential error generating circumstances are attempting to push on a "full" stack, and attempting to pop an "empty" stack. To monitor these hazards, we create two exception classes as follows:
//Header File: intStack.h
#ifndef INTSTACK_H
#define INTSTACK_H
class popOnEmpty
{
};
class pushOnFull
{
};
const int maxSize = 2;
class intStack
{
private:
int top;
int s[maxSize];
public:
intStack();
void push(const int);
int pop();
bool empty() const;
bool full() const;
};
#endif
//Implementation File: intStack.cpp
#include "intStack.h"
intStack::intStack()
{
top=-1;
}
void intStack::push(const int number)
{
if (full())
throw pushOnFull();
s[++top]=number;
}
int intStack::pop()
{
if (empty())
throw popOnEmpty();
return (s[top--]);
}
bool intStack::empty() const
{
return (top==-1);
}
bool intStack::full() const
{
return (top==maxSize-1);
}
The following driver has been designed to throw both exceptions.
//File: intStackDriver.cpp
#include <iostream>
#include "intStack.h"
using namespace std;
int main()
{
intStack s;
try
{
s.push(25);
s.push(12);
s.push(8);
s.push(10);
}
catch (pushOnFull)
{
cerr << "Pushing onto full
stack..." << endl;
}
try
{
cout << s.pop() << endl;
}
catch (popOnEmpty)
{
cerr << "Popping an empty
stack..." << endl;
}
try
{
s.push(34);
s.push(45);
}
catch (pushOnFull)
{
cerr << "Pushing onto full
stack..." << endl;
}
try
{
cout << s.pop() << endl;
cout << s.pop() << endl;
cout << s.pop() << endl;
cout << s.pop() << endl;
}
catch (popOnEmpty)
{
cerr << "Popping an empty
stack..." << endl;
}
return 0;
}
gives the following output:
Pushing onto full stack...
12
Pushing onto full stack...
34
25
Popping an empty stack...
Notice the use of the standard error stream, cerr. This is by default attached to the display, like cout, but without buffering.
It's acceptable to place one try-catch block around the whole program, but the output will differ. (NOTE: It is NOT ACCEPTABLE in Visual 6.0) Upon encountering an exception, the compiler will exit the block and continue. In the following version, after encountering the overflow exception, the compiler exits the program.
//File: intStackDriver2.cpp
#include <iostream>
#include "intStack.h"
using namespace std;
int main()
try
{
intStack s;
s.push(25);
s.push(12);
s.push(8);
s.push(10);
cout << s.pop() << endl;
s.push(34);
s.push(45);
cout << s.pop() << endl;
cout << s.pop() << endl;
cout << s.pop() << endl;
cout << s.pop() << endl;
return 0;
}
catch (pushOnFull)
{
cout << "Pushing onto full stack..." << endl;
}
catch (popOnEmpty)
{
cout << "Popping an empty stack..." << endl;
}
produces the following output:
Pushing onto full stack...
It's sometimes convenient to elaborate on the exception class. The following adds more detail to the popOnEmpty class so that more information can be retrieved if an exception is generated.
//Header File: intStack3.h
#ifndef INTSTACK3_H
#define INTSTACK3_H
#include <string>
using namespace std;
class popOnEmpty
{
private:
string message;
public:
popOnEmpty():message("Popping an empty
stack...")
{}
string what()
{
return
message;
}
};
class pushOnFull
{
};
const int maxSize = 10;
class intStack
{
private:
int top;
int s[maxSize];
public:
intStack();
void push(const int);
int pop();
bool empty() const;
bool full() const;
};
#endif
//File: intStackDriver3.cpp
#include <iostream>
#include "intStack3.h"
using namespace std;
int main()
try
{
intStack s;
s.push(25);
s.push(12);
cout << s.pop() << endl;
s.push(34);
s.push(45);
cout << s.pop() << endl;
cout << s.pop() << endl;
cout << s.pop() << endl;
cout << s.pop() << endl;
return 0;
}
catch (pushOnFull)
{
cout << "Pushing a full stack..." << endl;
}
catch (popOnEmpty e) //Note the use of an object
here
{
cout << e.what() << endl;
}
producing the following output:
12
45
34
25
Popping an empty stack...
Finally, we put all of the pieces together, including exception specifications in the
push() and pop() signatures.
//Header File: intStack4.h
#ifndef INTSTACK4_H
#define INTSTACK4_H
#include <string>
using namespace std;
class popOnEmpty
{
private:
string message;
public:
popOnEmpty():message("Popping an empty
stack...")
{}
string what()
{
return
message;
}
};
class pushOnFull
{
private:
string message;
int value;
public:
pushOnFull(int
i):message("Pushing a full stack..."), value(i)
{}
string what()
{
return
message;
}
int valueOf()
{
return
value;
}
};
const int maxSize = 2;
class intStack
{
private:
int top;
int s[maxSize];
public:
intStack();
void push(const int) throw
(pushOnFull);
int pop() throw (popOnEmpty);
bool empty() const;
bool full() const;
};
#endif
//Implementation File: intStack4.cpp
#include "intStack4.h"
intStack::intStack()
{
top=-1;
}
void intStack::push(const int number) throw
(pushOnFull)
{
if (full())
throw pushOnFull(number);
s[++top]=number;
}
int intStack::pop() throw (popOnEmpty)
{
if (empty())
throw popOnEmpty();
return (s[top--]);
}
...........
//File: intStackDriver4.cpp
#include <iostream>
#include "intStack4.h"
using namespace std;
int main()
try
{
intStack s;
s.push(25);
s.push(12);
s.push(8);
cout << s.pop() << endl;
s.push(34);
s.push(45);
cout << s.pop() << endl;
cout << s.pop() << endl;
cout << s.pop() << endl;
cout << s.pop() << endl;
return 0;
}
catch (pushOnFull e)
{
cout << e.what() << " Value attempted: " <<
e.valueOf() << endl;
}
catch (popOnEmpty e)
{
cout << e.what() << endl;
}
Pushing a full stack... Value attempted: 8
Finally, we discovered a programmer's tool for spotting trouble.
//File: assertTest.cpp
#include <iostream>
#include <cassert>
using namespace std;
int main()
{
cout << "Give me a number: ";
double number1;
cin >> number1;
cout << "Give me another number: ";
double number2;
cin >> number2;
assert (number2!=0);
double answer = number1/number2;
cout << "answer = " << answer << endl;
return 0;
}
Give me a number: 3
Give me another number: 0
Assertion failed at assertTest.cpp line 15: number2
Exiting due to signal SIGABRT
Raised at eip=000131ba
eax=018adec4 ebx=00000120 ecx=00000000 edx=0001e870 esi=00000054 edi=00027774
ebp=018adf70 esp=018adec0 program=E:\MYDOCU~1\DRBILL~1\SPRING~1\CSC076\CLASS_~1\
4-30\A.EXE
cs: sel=00a7 base=83493000 limit=ffa39fff
ds: sel=00af base=83493000 limit=ffa39fff
es: sel=00af base=83493000 limit=ffa39fff
fs: sel=0087 base=0001e870 limit=0000ffff
gs: sel=00bf base=00000000 limit=0010ffff
ss: sel=00af base=83493000 limit=ffa39fff
App stack: [018ae000..0182e000] Exceptn stack: [00027650..00025710]
Call frame traceback EIPs:
0x000130d8
0x000131ba
0x00010bc4
0x0000162e
0x00010762
The assert macro is useful to the programmer in the debugging stage, but is not usually used in the production version.
Lab Exercise. Write a program that asks the user for two numbers (double). Then proceed to divide one by the other and display the result on the screen. Write a divideByZero exception class that handles an attempt to divide by zero.
You may want to incorporate a function divide(x1, x2) into your program. Feel free to check your solution with mine. (divide.cpp and divideByZero.h)
We have already encountered polymorphism ("many forms") in static form when we discussed overloaded functions and operators. For example, several functions, all named 'swap', could be created, each with differing parameter types.
void swap (int &, int
&);
void swap (double &, double &);
void swap (char &, char &);
The compiler determines which of these functions to execute depending upon the type of the actual parameters. The invocation
int a, b;
.............
swap (a, b); <------ invoke swap
causes the first of the defined swap functions to be executed, since the actual parameters are of type int. The number and type of a function's parameters are referred to as the function's signature. Note that the return type of the function is not part of its signature.
What we have described is a static form of polymorphism. That is, the type of a and b are known at compile time and so the appropriate version of the overloaded swap function can be determined then. We saw an extension of this idea when we created function templates. This was certainly more efficient than overloading the function since it was not necessary to duplicate the swap code over and over again just because the type of the parameter changed. However, it is still static; that is, the types of the actual parameters are know at compile time.
With the concepts of inheritance and virtual functions, we will see that polymorphism can be made dynamic. That is, the program will determine which version of the function to execute at run time.
The object-oriented term inheritance reflects the relationship between classes. In the same way that we inherit genes from our ancestors, derived classes inherit data members and member functions from their parent or base class. In the following geometrical hierarchy, Circle 'is a' Shape and Rectangle 'is a' Shape. Object-oriented design emphasizes the building of these kinds of class relationships as opposed to writing procedures which act on independently defined data structures. Since the derived classes inherit the members of the base class, it's clear that code reusability and maintainability, both primary objectives of software engineering, can be enhanced.
//Header File: Shape0.h
#ifndef SHAPE0_H
#define SHAPE0_H
#include <string>
using namespace std;
class Shape
{
public:
//Constructors
Shape();
Shape(string);
//Destructor
~Shape(){};
//Accessor
string name() const;
//Facilitator
double area() const;
private:
string m_name;
};
//Derived class Rectangle definition
class Rectangle : public Shape //Shape
is the base class for Rectangle
{
public:
//Constructor
Rectangle (double, double, double,
double, string);
//Destructor
~Rectangle() {};
//Facilitators
double width() const;
double height() const;
double perimeter() const;
double area() const;
private:
//upper left hand corner coordinates
double m_xul, m_yul;
//lower right hand corner coordinates
double m_xlr, m_ylr;
};
//Derived class Circle definition
class Circle : public Shape //Shape is
the base class for Circle
{
public:
//Constructor
Circle (double, double, double, string);
//Destructor
~Circle() {};
//Accessors
double radius() const;
void center(double &, double
&) const;
//Facilitators
double circumference() const;
double area() const;
private:
//radius
double m_rad;
//coordinates of center
double m_x, m_y;
};
#endif
Observe the notation
class Circle : public Shape
The colon is used to indicate the base class for Circle. The access specifier public in this context indicates the accessibility of Shape members in the derived class Circle. In particular, public inheritance allows access to all public members of Shape, while denying access to the private data member m_name. However, access is easily obtained through the use of the public member function, name().
The details of implementation can be found in
//File: Shape0.cpp
#include <iostream>
#include <string>
#include <cmath> //contains PI
#include "shape0.h"
using namespace std;
//Shape Implementation
Shape::Shape():m_name("Basic Shape")
{};
Shape::Shape(string name)
{
m_name = name;
}
string Shape::name() const
{
return m_name;
}
double Shape::area() const
{
return 0.0;
}
//Rectangle Implementation
Rectangle::Rectangle (double Xul, double Yul, double
Xlr, double Ylr, string name)
: Shape (name)
{
m_xul = Xul;
m_yul = Yul;
m_xlr = Xlr;
m_ylr = Ylr;
}
double Rectangle::width() const
{
return m_xlr - m_xul;
}
double Rectangle::height() const
{
return m_yul - m_ylr;
}
double Rectangle::perimeter() const
{
return 2.0 * (width() + height());
}
double Rectangle::area() const
{
return width() * height();
}
//Circle Implementation
Circle::Circle(double xcoord, double ycoord, double
radius, string name)
: Shape (name)
{
m_x = xcoord; m_y = ycoord; m_rad = radius;
}
double Circle::radius() const
{
return m_rad;
}
void Circle::center(double &x, double
&y) const
{
x = m_x;
y = m_y;
}
double Circle::circumference() const
{
return(2.0 * PI * m_rad);
}
double Circle::area() const
{
return (PI * m_rad * m_rad);
}
//File: Shape_driver0.cpp
#include <iostream>
#include <string>
#include "Shape0.h"
using namespace std;
int main()
{
Shape sh;
Circle c(0.0, 0.0, 1.0, "MyCircle");
Rectangle r(0.0, 1.0, 1.0, 0.0, "MyRectangle");
cout << sh.area() << endl;
cout << sh.name() << endl;
cout << c.area() << endl;
cout << c.name() << endl;
cout << r.area() << endl;
cout << r.name() << endl;
cout << endl;
return 0;
}
0
Basic Shape
3.14159
MyCircle
<===== OUTPUT
1
MyRectangle