Abstract Data Types and C++ Implementation
Reading: Dietel 6.1-6.14
Abstract Data Types (ADTs)
- A conceptual specification of a data type
- Includes description of object, description of operations on it
- No specification of how to implement the object, operations
- Could have many different implementations of the same ADT
Programmers implement the ADT; can see internal variables, function definitions, etc.
Users instantiate and work with the ADT; can see only the external operations / behavior
Example: Stack
Contains various values of like type
May have maximum size
Values of appropriate type can be added to or removed from the stack
Push operation adds a value to the stack
Pop operation removes the last added value from the stack (LIFO)
Is_Empty operation tells whether stack contains any values
Is_Full operation tells whether stack is full (if applicable)
It is an error to Pop from an empty stack
Data Abstraction
- Separate design information (what is to be done) from implementation information (how it is to be done)
- Control and limit dependence between portions of code
- Make information available only to those who need it
- Only implementor should see function definitions for operations
- Possibly different programmers will implement various operations; each should only see their own
- Users of the data type should only be able to view or modify instantiations through accessor/mutator functions (not directly, as with struct's)
Methods of Abstraction
Procedural Abstractiontypedef struct
{
int cur_pos ;
int array [ 100 ] ; // Can have at most 100 elements in stack
} stack;
void Init ( stack* s )
{
s -> cur_pos = 0;
}
void Push ( stack* s, int x )
{
if ( s -> cur_pos == 100 )
cerr << "Stack full!";
else
s -> array [ s -> cur_pos ++ ] = x;
}
int Pop (stack* s)
{
if ( s -> cur_pos == 0 )
{ cerr << "Stack empty!"; return -1; }
else
return s -> array [ -- s -> cur_pos ];
}
void main()
{
stack s1, s2; int x,y;
Init ( &s1 ); Init( &s2 );
Push ( &s1, 7 ); Push ( &s2, 9 ); // Good...
x = Pop ( &s2 ); y = Pop ( &s1 );
x = s1.array[187]; s2.array[503] = 6; // ... and bad
s2.cur_pos = -3;
}
Modular (File) AbstractionFile 1 (precompiled):
< typedef from above >
static stack s;
void Init ( void )
{
s.cur_pos = 0;
}
void Push ( int x )
{
if ( s.cur_pos == 100 )
cerr << "Can't push onto a full stack!";
else
s.array [ s.cur_pos ++ ] = x;
}
int Pop ( void )
{
if ( s.cur_pos == 0 )
{ cerr << "Can't pop from an empty stack!"; return -1; }
else
return s.array [ -- s.cur_pos ];
}
File 2:
void main ( )
{
Init ( );
Push ( 8 ); // We can't access the data directly without knowing the typedef
Push ( 10 );
int x = Pop ( );
}
Object-Oriented Programmingclass stack
{
private: // Only stack member functions can access this data
// Can also have private member functions
int array[100];
int cur_pos ;
public: // Data and functions accessible by any part of code
void Push ( int x ) // No need to pass in stack parameter to stack functions
{
if ( Is_Full() ) // Can call another function on this object if necessary
cerr << "Can't push on a full stack!";
else
array [ cur_pos ++ ] = x; // No need to use . to access fields
}
int Pop ( void )
{
if ( Is_Empty() )
{ cerr << "Can't pop from an empty stack!"; return -1; }
else
return array [-- cur_pos ] ;
}
int Is_Empty ( void ) const // Function does not change stack on which it is called
{
return ( cur_pos == 0 );
}
int Is_Full ( void) const // Could make Is_Full return false for a growing stack
{
return ( cur_pos == 100 );
}
stack ( ) // Constructor function: initialize object as necessary
{
cur_pos = 0;
}
}
void main()
{
stack s1, s2; // Constructor function called implicitly for each
s1.Push(8); s2.Push(27); // Note: we cannot access the stack's array or cur_pos directly
// Compiler prevents erroneous stack usage
}
Inheritance
- Allow related data types to share relevant code
- example: Square is-a Rectangle is-a Polygon is-a Shape
- no need to re-code common features
- can implement operations differently; a square's area is computed more simply than a rectangle's, etc.