18. Stacks¶
18.1. Abstract data types¶
The data types we have looked at so far are all concrete, in the sense that we
have completely specified how they are implemented. For example, the Card
class represents a card using two integers. As we discussed in the
Card objects section, there are alternative implementations, one of
which we explored in Enumerations.
An abstract data type, or ADT, specifies a set of operations (or functions) and the semantics of the operations (what they do), but it does not specify the implementations of the operations.
Why is that useful?
It simplifies the task of specifying an algorithm if you can denote the operations you need without having to think at the same time about how the operations are performed.
Since there are usually many ways to implement an ADT, it might be useful to write an algorithm that can be used with any of the possible implementations.
Well-known ADTs, like the Stack ADT in this chapter, are often implemented in standard libriaries so they can be written once and used by many programmers.
The operations on ADTs provide a common high-level language for specifying and talking about algorithms.
When we talk about ADTs, we often distinguish the code that uses the ADT, called the client code, from the code that implements the ADT, called provider code because it provides a standard set of services. The collection of classes and functions through which the client code accesses the provider codes is called the application programming interface (API).
18.2. The Stack ADT¶
In this chapter we will look at one common ADT, the stack. A stack is a collection. Other collections we have seen include vectors and lists.
An ADT is defined by the operations you can perform on it. Stacks can perform only the following operations:
- new
Create a new, empty stack.
- push
Add a new item to the stack.
- pop
Remove and return an item from the stack. The item that is returned is always the last one that was added.
- top
Return the value at the top of the stack without removing it.
- empty
Check whether the stack is empty.
A stack is sometimes called a “last in, first out,” or LIFO data structure, because the last item added is the first to be removed.
Stacks have numerous uses in computer science, including in the run-time stack used by operating systems to manage function calls. We will explore some of these in the exercises.
18.3. Implemeting stacks with linked lists¶
We will look at a few different implementations of the stack ADT, beginning with one that we have already written, without even knowing it.
Take a look back at the LinkedList
class we wrote in the previous chapter,
then look at the operations defined for the stack ADT. If it occurs to you
that the push operation looks a lot like what the insert_at_front function
does, and pop similarly looks like remove_from_front, then you are doing
well. In fact, with just a little tweaking, our LinkedList
class could
perform all the functions we want from stacks.
How should we do this “tweaking”? We could just write a new Stack
class,
burrowing most of the logic we used to create LinkList
s and rewriting it,
but this would violate the DRY principle that we introduced in
Our own find function.
Our desire to implement the stack ADT utilizing our existing LinkedList
class provides a nice opportunity to use inheritence, to which we were
introducted in the Inheritance of chapter 13.
Here is the entire source code for a stack object that inherits from
LinkedList
residing in a file named Stack.h
:
#include "LinkedList.h"
template <class T>
struct Stack : public LinkedList<T>
{
void push(T item) {
LinkedList<T>::insert_at_front(item);
}
T pop() {
return LinkedList<T>::remove_from_front();
}
bool empty() const {
return (LinkedList<T>::length() == 0);
}
T top() {
if (LinkedList<T>::head == nullptr)
throw runtime_error("Can't return top item of empty stack!");
return LinkedList<T>::head->cargo;
}
};
To use it, you need to have the LinkedList.h
file provided in
Appendix A: Code Source in the same location as Stack.h
.
Notice that there is no constructor defined for our stack. Instead, we are
using the implicitely declared default constructor that C++
gives us, which in this case calls the no argument contructor of the base
object, our linked list, creating a linked list with instance variables
num_nodes
and head
.
Note
We have been making most of our objects public
by using struct
instead of class
. Be forewarned that you may be told by more
experienced C++ programmers that this is unsafe and a bad practice. C++
supports access specifiers to provide controlled
access from one part of the code to another. Implementing access control is
cumbersome and would have greatly complicated both our code examples and
their explainations. Since our goal is to present “big ideas” here as
accessibly as possible, we intentionally avoided getting stuck “in the
weeds” with this topic. If your intention is to become a C++ programer,
you will definitely need to learn all about it.
Modifier member functions push
and pop
just call insert_at_front
and remove_from_front
respectively, giving their already implemented
functionality our new stack API.
The LinkedList
object doesn’t have an exact equivalent of a Boolean
empty
function, but it does have the accessor function length
. A stack
is empty when the length of its list of items is 0.
Finally top
returns the cargo
of the head node. We can do this without
the compiler complaining because we made everything in our Node
objects
public.
Here is a small program in a file named test_stack.cpp
that we can use to
test our Stack
implementation:
#include <iostream>
#include "Stack.h"
using namespace std;
int main()
{
Stack<int> stack;
cout << "Calling stack.empty() gives: " << stack.empty() << endl;
for (int i = 1; i <= 5; i++) {
cout << "Pushing " << i * i << "..." << endl;
stack.push(i * i);
}
cout << "Calling stack.empty() now gives: " << stack.empty() << endl;
cout << "The top of the stack contains: " << stack.top() << endl;
while (stack.empty() == false) {
cout << "Popping " << stack.pop() << "..." << endl;
}
cout << "Calling stack.empty() after while loop gives: ";
cout << stack.empty() << endl;
return 0;
}
Running it yields:
Calling stack.empty() gives: 1
Pushing 1...
Pushing 4...
Pushing 9...
Pushing 16...
Pushing 25...
Calling stack.empty() now gives: 0
The top of the stack contains: 25
Popping 25...
Popping 16...
Popping 9...
Popping 4...
Popping 1...
Calling stack.empty() after while loop gives: 1
As we have been doing, you will be asked to write automated tests for it in the exercises.
18.4. Exceptions¶
The last of our stack API functions, top, presents us with a problem similar
to the one we saw in Preconditions. This function should only be called
on a non-empty stack. In fact, non-emptiness could be considered a
precondition for top
. So what should happen when we try to call top
on an empty stack?
The top
function is defined to return a value of type T
. It’s not at
all clear what top
should do with an empty stack, and if we don’t do
anything to handle the problem, trying to access the cargo
attribute of
a NULL
pointer to a Node
will result in a runtime error. As we
mentioned way back in Runtime errors, these are also called
exceptions, and C++ has
a standard way of handling them.
The three keywords C++ uses to handle exceptions are try
, catch
, and
throw
. The way to handle code which could cause an exception to occur is
to put the code in a try-block and then to write exception-handlers which
are declared with the keyword catch
.
To do this for our top
function, we would write something like this:
T top() {
try {
if (LinkedList<T>::head == nullptr)
throw runtime_error("Can't return top item of empty stack!");
return LinkedList<T>::head->cargo;
}
catch (exception& e) {
// Do something sensible here to handle the exception
}
}
Since catch
would have to return a value of type T
, and since it is not
at all clear how to do that, we will instead keep our previous verstion of
top
that calls throw
after checking if head
is nullptr
. Calling
top
on an empty stack will still cause the program to crash, but at least
it will report the helpful measage, “Can’t return top item of empty stack!”, as
it crashes.
18.5. Implementing stacks by wrapping a list¶
Instead of having our Stack
inherit from LinkedList
, we could have it
wrap LinkedList
by
having a linked list as its private data member.
#include "LinkedList.h"
template <class T>
class Stack
{
LinkedList<T> items;
public:
void push(T item) {
items.insert_at_front(item);
}
T pop() {
if (empty())
throw runtime_error("Can't pop from empty stack!");
return items.remove_from_front();
}
bool empty() const {
return items.length() == 0;
}
T top() {
if (empty())
throw runtime_error("Can't return top item of empty stack!");
T top_item = items.remove_from_front();
items.insert_at_front(top_item);
return top_item;
}
};
This version uses the class
keyword instead of struct
, and makes the
wrapped link list a private data member. LinkedList
could also be changed
to a class with its num_nodes
and head
data members private. Doing that
prevents access to the cargo
of the head node from within our stack
code. The top
function of this version has been written to use only list
API calls. The front element is removed, returning its value, and then placed
back on the front of the list.
18.6. Postfix expressions¶
In most programming languages, mathematical expressions are written with the operator between the two operands, as in \(1 + 2\). This format is called infix. An alternative format used by some scientific calculators is called postfix. In postfix, the operator follows the operands, as in \(1\;2\;+\).
One advantage of postfix over infix is that it eliminates the need for parenthesis in expressions like \((4 + 5) * 3\), since the postfix expression for this is unambiguously written as \(4\;5 + 3\;*\).
Another advantage of postfix for programmers (and calculator designers) is that there is a natural way to evaluate a postfix expression using a stack.
Starting at the beginning of the expression, get one term (operator or operand) at a time.
If the term is an operand, push it on the stack.
If the term is an operator, pop two operands off the stack, perform the operation on them, and push the result back on the stack.
When we get to the end of the expression, there should be exactly one operand on the stack. That operand is the result.
18.7. Parsing¶
In order to implement the algorithm from the previous section, we need to be able to traverse a string and break it into operands and operators. This process is an example of parsing.
18.8. Implementing APIs¶
One of the fundamental goals of an API is to separate the interests of the provider, who writes the code that implements the API, and the client, who uses the API. The provider only has to worry about whether the implementation is correct – that calls made using the API will do what they are supposed to do – not about how it will be used.
Conversely, the client assumes that the implementation works as specified and doesn’t worry about the details. When you use C++’s standard library, you have the luxury of thinking exclusively as a client.
When you implement an API, on the other hand, you also have to write the client code to test it. In that case, you sometimes have to think carefully about which role you are playing at a given time.
In the next few sections we will switch gears and look at another way of implementing the Stack ADT using a vector.
18.9. Array implementation of the Stack ADT¶
Our stack data for this implementation will be stored in the simple C style array which we were introduced to in the Arrays section of the Pointers and arrays chapter.
#ifndef STACK_H
#define STACK_H
#define MAX_SIZE 128
#include <stdexcept>
template <class T>
class Stack {
int top_item;
T items[MAX_SIZE];
public:
Stack() {
top_item = -1;
}
void push(const T& value) {
if (top_item >= MAX_SIZE - 1) {
throw std::overflow_error("Stack overflow");
}
items[++top_item] = value;
}
T pop() {
if (empty()) {
throw std::underflow_error("Stack underflow");
}
return items[top_item--];
}
const T& top() const {
if (empty()) {
throw std::underflow_error("Stack is empty");
}
return items[top_item];
}
bool empty() const {
return top_item == -1;
}
size_t size() const {
return top_item + 1;
}
};
#endif // STACK_H
The second line uses the preprocessor directive #define
to define a
constant value (128 in this case) named MAX_SIZE
. The preprocessor will
replace all occurances of MAX_SIZE
with the literal value 128
before
the code is compiled.
18.10. The stack in the standard template library¶
A standard library is a collection of programming resources (pre-defined constants, functions, classes, etc.) that can be treated by programmers as part of the language, since they come “standard” with it.
We have been using the C++ standard library since the first chapter of this
book, since every time we write #include <iostream>
and use cout
we
are using the standard library.
18.11. Glossary¶
- abstract data type (ADT)¶
A data type (usually a collection of objects) that is defined by a set of operations, but that can be implemented in a variety of ways.
- client¶
A program that uses an ADT (or the person who wrote the program).
- provider¶
The code that implements an ADT (or the person who wrote it).
- application programming interface (API)¶
The classes and functions through which an interface is provided from one piece of software, the provider, to another piece of software, the client.
- infix¶
A way of writing mathematical expressions with the operators between the operands.
- postfix¶
A way of writing mathematical expressions with the operators after the operands.
- delimiter¶
A character that is used to separate tokens, like the punctuation in a natural language.