CS 112 Lab 7: Stacks and Exceptions

Objectives:

  1. Build a dynamically-allocated array-based Stack class.
  2. Build a simple Exception class hierarchy.
  3. Practice throwing and catching exceptions.

Introduction

Today's exercise has two parts:

  1. In Part I, we will build a simple Stack class that stores its items in a dynamically allocated array.
  2. In Part II, we will see how to define, throw, catch, and handle exceptions.

Getting Started

Begin by creating a new project for this week's files. Download tester.cpp, StackTester.h, StackTester.cpp, Stack.h, and Stack.cpp. Import them into your project, open the files, and take a moment to look them over, to become familiar with their contents.

Part I: The Stack Class

As you can see, this Stack class contains three instance variables:

   unsigned myCapacity;
   unsigned myTop;
   Item*    myArray;
We will be storing the address of a dynamically allocated array in myArray. We will be storing the size of that array in myCapacity. And we will be storing the index of the top value in that array in myTop.

As we have seen in class, the primary Stack operations are:

To let you focus on these Stack operations, the Stack class already contains prototypes and definitions for:

Take a few minutes to look these over. Look carefully at the constructor, and note the initial values each member is given. Then take a minute to look over StackTester::testConstructor(), to see the tests it contains. Note that part of it is commented out. Leave this part for now; we will be uncommenting it in Part II.

Run the project, and verify that everything works correctly at the outset.

The isEmpty() Method

Our constructor gives us the ability to define Stack objects. Since these Stacks are initially empty, the first operation we will build is the isEmpty() method.

In StackTester.cpp, uncomment the call to and definition of the testIsEmpty() method. Compile and run the test. You should see an error message indicating that isEmpty() does not exist.

In Stack.h, add a prototype for isEmpty() to the class. Recompile and rerun. You should now see a linking error, since isEmpty() has been prototyped but not defined.

In Stack.cpp, add a definition for isEmpty() that will pass testIsEmpty(). This definition should be as simple as possible (e.g., a single line). If you get stuck, here is a hint, but don't use it unless you have to.

Recompile and rerun your test. Continue when you pass the test.

The push() and isFull() Methods

Before we can test the other operations, we need to be able to add items to a Stack, so we now turn our attention to the push() method. Since pushing items onto a stack can fill it, we will also build the isFull() method.

In StackTester.cpp, uncomment the call to and definition of the testPushAndIsFull() method. Compile and run the test. You should see error messages indicating that push() and isFull() do not exist.

In Stack.h, add a prototype for isFull() to the class. Recompile. You should now see just the error for push(). Add a prototype for push(); then recompile. You should now get linking errors, since isFull() and push() have been declared but not defined.

In Stack.cpp, add a definition for isFull(). This definition should be as simple as possible (e.g., a single line).

Save/compile, and verify that the linking error for isFull() goes away. (The errors for push() should still be there.)

Define the push() method. Save/recompile, and verify that what you have written compiles without errors.

Run the tests. If you pass the tests, congratulations! If not, go back and fix your push() and/or isFull() method. If you get stuck, here is a hint for push(), and here is a hint for isFull(), but only use them if you have to.

Continue when your methods pass all the tests.

The getTop() Method

Now that we are able to put items onto our Stack, we are ready to build the getTop() operation.

In StackTester.cpp, uncomment the call to and definition of the testGetTop() method. Compile and run the test. You should see an error message indicating that getTop() does not exist.

In Stack.h, add a prototype for getTop() to the class. Recompile and rerun. You should now see a linking error, since getTop() has been prototyped but not defined.

In Stack.cpp, add a definition for getTop() that will pass testGetTop(). For now, this definition should be as simple as possible (e.g., a single line). If you get stuck, here is a hint, but don't use it unless you have to.

Recompile and rerun your test. Continue when you pass the test.

The pop() Method

Our sole remaining operation is the pop() method, that removes and returns the top value from the Stack.

In StackTester.cpp, uncomment the call to and definition of the testPop() method. Compile and run the test. You should see an error message indicating that pop() does not exist.

In Stack.h, add a prototype for pop() to the class. Recompile and rerun. You should now see a linking error, since pop() has been prototyped but not defined.

In Stack.cpp, add a definition for pop() that will pass testPop(). For now, this definition should be as simple as possible (e.g., a single line). If you get stuck, here is a hint, but don't use it unless you have to.

Recompile and rerun your test. Continue when you pass the test.

Part II: Exceptions

We now have a working class, but it is lacking any error-handling capability. For example, what happens if a person pops an empty stack, or pushes an item onto a full stack? In Part II of today's exercise, we introduce the C++ exception mechanism, which provides a graceful way of handling such problems.

An exception is an out-of-the-ordinary occurrence. As we shall see C++ lets us create classes that represent exceptions.

In C++, a method that wants to indicate that something exceptional has occurred can throw an exception back to the caller of the method. The pattern for the throw statement is:

   throw NameOfException(arguments);
Back in the caller, the call to that method can be "wrapped" in a try-catch block, whose form is as follows:
   try {
      // risky code -- might throw an exception
   } catch ( NameOfException exceptionParameter ) {
      // handler code -- to be executed if the exception occurs
   }
Such a block tries to perform the "risky code". If it works, control skips past the catch block to whatever follows it. But if the "risky code" throws the exception named in the catch, then control is transferred to the "handler" code, which does whatever is appropriate.

Catching Exceptions

In StackTester.cpp, uncomment the try-catch block in method testGetTop():

   try {
      s1.getTop();
      cerr << "\ngetTop() worked on empty Stack" << flush;
      exit(1);
   } catch (StackException se) {
      cout << " 1 " << flush;
   } 
This block tries to send the getTop() message to an empty stack, and then catches the exception that should be thrown. If the exception is not thrown, the statements that follow the getTop() message will execute, terminating the program. If the execption is thrown, control transfers from the statement invoking getTop() to the catch block, which displays the exception. The next statement then displays the " 1 " that indicates our test has succeeded. (An alternative, simpler approach is to just display the " 1 " in the catch block.)

Save/recompile your project. You should get several errors, because StackException has not been defined. Verify that that is the cause of your errors, then continue.

The StackException Class

One way to provide exceptions for our Stack class is to define a separate StackException class that our Stack class can use when something goes wrong. The file: StackException.h, contains such a class:

 
       class StackException {
       public:
          StackException(const string& whereThrown,
                         const string& message) { 
              myLocation = whereThrown; 
              myMessage = message; 
          }

          string asString() const {
              return "*** StackException in " +
                      myLocation + ": " + myMessage; 
          }

       private:
          string myLocation;
          string myMessage;
       };  // StackException

Save and import a copy of this file into your project, and personalize its opening comment.

The class has two instance variables:

The StackException constructor initializes these members. For example, if at the beginning of the pop() method, we find that the stack is empty, we can build an exception like this:

   StackException("pop()", "stack is empty")
If at the beginning of the the push() method, we find that the stack is full, we can build an exception like this:
   StackException("push()", "stack is full")

The asString() method presents a string representation of these values, accompanied by some descriptive labels.

At the bottom of StackException.h, there is a definition of operator<< that can be used to display a StackException.

At the beginning of Stack.h, uncomment the #include directive that includes StackException.h.

After making these changes, save/recompile your project. If all is well, your project should compile correctly. If not, find and correct the error(s).

When your project compiles correctly, run it. You should see execution halt in the testGetTop() method. The problem is that our testGetTop() method now expects a StackException to be thrown when we send the getTop() message to an empty Stack. Continue, and we will see how to accomplish this.

Throwing Exceptions

Now that our Stack class contains a StackException class by which we can describe unusual circumstances, we can use that class in our Stack methods. As mentioned previously, a method that detects an unusual circumstance can throw an exception. For example, we might revise our getTop() method to make it throw a StackException when the Stack is empty as follows:

   Item Stack::getTop() const {
      if (myTop > 0) {
         return myArray[myTop - 1];
      } else {
         throw StackException("getTop()", "stack underflow");
      }
   }
Recompile and rerun your project. With these changes, what you have written should pass all the tests. Continue when it does so.

The Constructor

Uncomment the try-catch block in the testConstructor() method. Then save/compile your project. Run it, and verify that it fails inside the try block.

Using what we did with getTop() as a model, modify the Stack constructor so that when its precondition is false, the constructor throws a StackException with the message Stack(size): size must be positive!. Continue when your project passes all tests.

The pop() Method

Uncomment the try-catch blocks in the testPop() method. Then save/compile your project. Run it, and verify that it fails inside the try block.

Using what we did with getTop() as a model, modify pop() so that it throws a StackException with the message stack is empty!. In case you get stuck, here is a hint, but don't use it unless you need to. Continue when your project passes all tests.

The push() Method

Our final operation is the push() operation. Uncomment the try-catch blocks in testPushAndIsFull(). Then save/recompile, and verify that your project no longer passes the test.

Using what we did previously as a model, modify push() so that it throws a StackException with the message stack is full!. In case you get stuck, here is a hint, but don't use it unless you need to. Continue when your project passes all tests.

The Other Operations

Now that you have tested the stack operations, uncomment the remaining test-methods and verify that the other Stack operations pass their tests.

Congratulations! You now have a full-featured dynamic (array-based) Stack class at your disposal!

Turn In

Copy your lab folder to a new /home/cs/112/current/yourUserName/lab7 folder.

If time permits, you may begin working on this week's project.


CS > 112 > Labs > 07


This page maintained by Joel Adams.