CSC 076

Programming in C++

 

Summary of 4/30 Lecture

Exception Handling

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)


Polymorphism

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.

Inheritance

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.

wpe1.jpg (5607 bytes)


//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

Back to C++ Home Page