Data abstraction: ADTs

To quote Sebesta, "Abstraction is a weapon against the complexity of programming, its purpose is to simplify the programming process."

There are two kinds of abstraction we are concerned with:

Both process and data abstraction contribute to our ability to modularize code:

An abstract data type is a group of program units and data items, that only includes the data representation for one specific type of data and the subprograms that provide the operations for manipulating that data type.

One of the key design issues in programming languages is determining the level of support the language supplies for user-defined abstract data types.

Object-oriented languages are a natural extension of the ADT concept.

Formally, an abstract data type satisfies the following conditions:

The program units which use objects of the defined type are called clients of the type

ADTs provide all the benefits discussed earlier for abstractions:

ADTs also provide increased reliability by enforcing the use of defined operations to manipulate the data types: programmers are not free to access the data types directly, they can only be used in accordance with the access processes put in place by the ADT designer

Note that in most languages the programmer has the ability to simulate the use of ADTs, but not the ability to enforce them.

In C, for instance, the programmer may create a data type and a set of access routines which - if used - give the desired behavior. However, it is very difficult to prevent programmers from circumventing the access routines if they so desire.

Thus, under our formal definition, C has poor support for ADTs.

ADT Design issues:

ADTs in C++

C++ supports abstract data types through its class construct.

A class is defined with associated operations, or member functions, and data fields, or data members.

Once a class is defined and a name assigned to the class type, variables can be declared to be instances of that type.

For example, if a stack class is defined, then variables firststack and secondstack might be declared as two separate stack instances.

In implementation terms:

Example: a declaration for an array-based C++ stack of integers:

class stack {
   public:
       stack();                  // constructor: initializes stack
       ~stack();                 // destructor: cleans terminated stack
       void push(int n);         // push n onto stack
       void pop();               // remove top item from stack
       int top();                // return value of top item on stack
       int isempty();            // return 1 if stack empty, 0 otherwise
       int isfull();             // return 1 if stack full, 0 otherwise
       int getsize();            // return current number of elements on stack
   private:
       int  stackarray[MAXSIZE]; // stack contents
       int  elements;            // number of items currently on stack
};

stack::stack() 
// body of the stack constructor
{
   elements = 0;
   for (int i = 0; i < MAXSIZE; i++)
       stackarray[i] = 0;
}

stack::~stack()
// body of the stack destructor
{
   // no real cleanup necessary
}

void stack::push(int n)
// body of the stack push routine
{
   if (isfull() == 0) {
      stackarray[elements++] = n;
   }
}

// etc with the rest of the stack member functions

// Declaring and using stacks
void main()
{
   stack s1, s2;              // s1 and s2 are instances of stacks
 
   s1.push(1);                // push value 1 onto stack 1
   cout << s1.top() << endl;  // print the top value of stack 1
   s2.push(s1.top());         // push (a copy of) the top value 
                              //    from stack 1 onto stack 2
   // etc ...
} 
Note the following declaration and usage features for C++ classes:

C++ vs Java:

While Java and C++ are closely related in terms of ADTs, there are a few differences:

Parameterized ADTs in C++

Earlier we discussed generic functions in C++, using template functions to handle a variety of possible data types for parameters.

Templates are also useful in creating parameterized ADTs - for example stacks where a generic data type is used for the types of elements which can be pushed on the stack.

Here we modify the inline version of our stack as follows:

template <class Type>
class stack {
   public:
       stack() {
          elements = 0;
          stackarray_ptr = new Type [MAXSIZE];
          for (int i = 0; i < MAXSIZE; i++) stackarray_ptr[i] = 0;
       }
       ~stack() { 
          delete stackarray_ptr;
       }
       void push(int n) {
          if (isfull() == 0) stackarray_ptr[elements++] = n;
       }
       void pop() {
          if (isempty() == 0) elements--;
       }
       int top() {
          if (isempty() == 0) return(stackarray[elements-1]);
       }
       int isempty() {
          if (elements == 0) return(1);
          else return(0);
       }
       int isfull() {
          if (elements == MAXSIZE) return(1);
          else return(0);
       }
       int getsize() {
          return(elements);
       }
   private:
       Type *stackarray_ptr;     // stack contents
       int  elements;            // number of items currently on stack
};

Object-oriented programming

The goal of object-oriented programming is to model all systems as collections of objects which communicate by message passing.

This is ideally suited to modelling, simulating, or controlling real-world systems which can be regarded as sets of communicating entities, each with its own internal processes that are invoked as a result of communications with the outside world.

The central point to object-oriented programming languages is the ability to take abstractions of abstract data types:

True object oriented languages are (in a formal definition) required to support three key features:

ADTs were discussed in the previous section, so we now focus on the concepts of inheritance and dynamic binding.

Inheritance

Inheritance is the process by which one class can be created as a special category of another class - inheriting (although possibly redefining) the variables and methods of the original class.

One of the key practical goals of including inheritance in a language is to improve the rate of software reuse - if a "reasonable" set of underlying classes is defined then much of the work of creating specialized instances is reduced.

TERMINOLOGY:

Polymorphism and dynamic binding

The goal here is to allow parent classes to use variables of (limited?) generic data types, and define methods which act on those variables.

These generic variables should be able to reference any of the subclasses, and the methods may be overridden or customized by those subclasses to handle the different data types appropriately.

When the (generic) variable calls the (overridden) method the call is dynamically bound to the proper method in the proper class.

This facilitates long term development and maintenance of software systems, where all the possible (specific) data types may not be known at the time of initial development.

OO Design issues

Some of the design issues that must be addressed include:

OO in C++

C++ vs Java OO

Again, Java and C++ are very similar in terms of support for OO, but there are some differences:

C++ Generic Stacks Class

template <class Type>
class Stack {
   public:
      Stack(int MaxSize = 100); // create stack with size limit,
      ~Stack() { delete stackptr; } // delete stack space  
      void push(Type& data);    // push data element on stack
      void pop(){ if (size > 0) size--; } // delete top element  
      Type top();               // return copy of top stack element
      bool isempty(){ return(size == 0); } // is stack empty?
      bool isfull(){ return(size == maxsize); } // is stack full?
      int getsize(){ return(size); } // get current stack size
      int getmaxsize(){ return(maxsize); } // get maximum stack size
      Type peek(int depth); // peek into stack at depth from top
   private:
      int size;                 // current stack size
      int maxsize;              // maximum stack size
      Type *stackptr;           // ptr for array of elements
};

template <class Type>
Stack::Stack(int MaxSize) {
   maxsize = MaxSize;
   stackptr = new Type[MaxSize];
   size = 0;
}

template <class Type>
void Stack::push(Type& data) {
   if (size < maxsize)
       stackptr[size++] = data;
}

template <class Type>
Type Stack::peek(int depth) {
   if ((size > depth) && (depth >= 0))
      return(stackptr[size-(depth +1)]);
   else
      throw OutOfBounds(); // throw exception
}

template <class Type>
Type Stack::top() {
   if (size > 0)
      return(stackptr[size-1]);
   else
      throw OutOfBounds(); // throw exception
}

Cleanup: overloading operators in C++

I believe this was overlooked in the notes covered earlier, but the mechanism for actually performing operator overloading in C++ is to declare the operator as you would any other function, but using the operator symbol in place of the function name and preceding the symbol with the operator keyword.
<return type> operator <symbol> (<parameter_list>);

The C++ restrictions on operator overloading are as follows: