CS202 Programming

Systems

 

Lecture Notes #1

 

 

 

Building a C++ Foundation

with

Data Abstraction

(using classes)

 

 

 

Lecture and Reference


Abstract Data Types

Abstract Data Types (ADTs): Specifies a new data type which includes both data and operations that can be performed on that data.

An abstract data type (ADT) specifies a new type of data along with its corresponding operations. Client programs using abstract data types can be designed to access and manipulate an ADT's data solely in terms of its data type and associated operations. Clients need not know about the details of how the data is represented or how the operations are implemented. ADTs should be designed such that the code implementing them is reused for each instance of that data type.

Abstract data types are best understood by comparing them to the fundamental data types. A data type specifies the representation for a type of data together with the operations that can be performed on that data. For example, short and long are data types. We can perform various operations on instances of these data types, such as addition, subtraction, and multiplication. An abstract data type simply extends this concept by allowing us to specify our own data types where there can be zero or more instances of these data types in our programs.

 

1.1 Programming with User Defined Data Types

User Defined Data Types (UDT): Abstract data types that are built into the language itself and managed by the compiler.

Abstract data types implemented directly as part of the language using the class construct define user defined data types (UDTs). A class is a language feature that defines a grouping of similar types of attributes (or data) and operations. Think of a class as similar to a fundamental data type (e.g., an int), but created by the programmer. A class definition includes not only the specification of the data, but also the specification of the operations that can be performed on that data. This is significant in that we now have a mechanism to directly implement an abstraction.

The following sections introduce user defined types, step through the syntax and semantics of using the C++ class construct, and discuss the process of identifying user defined types.

 

1.1.1 C++ Constructs Supporting User Defined Types

The construct for creating user defined data types and abstractions in C++ is the class. At its simplest, a user defined data type consists of a new class assembled from fundamental data types, such as numbers and characters, along with their operations. In more complex systems a new class can contain other objects.

Just as we define objects of fundamental types, we can define objects of user defined data types. An object is an entity that comes into existence at some moment in time and has memory allocated to it. Therefore, think of an object of a class as existing in both time and space. It is a variable that gets allocated and deallocated. On the other hand, a class is an abstraction. A class allows a set of objects to be defined that have the same structure and behavior. The class specifies the behavior of such objects. This behavior is based on the operations that clients can perform on objects as well as the operations that these objects perform themselves. These operations are implemented as member functions.

The class construct replaces the purpose that the module served using modular abstraction. Now, we are free to use the C++ module in a new way to group related collections of classes. Classes that are logically related should be grouped into the same module. Therefore, we expose only those features that other modules must see. It allows us to organize and structure even more complex programs to ensure reliability and to be able to control future modification. Using the class construct, we are able to encapsulate both data and behavior. Modularity now allows us to group logically related classes.

User defined data types can be encapsulated with their associated operations in a class definition. A class' definition includes a class interface as well as a class implementation. The interface to a class supports the idea of data abstraction while keeping the implementation of the operations hidden from the client. For example, the interface to the sorted list abstraction can be separate from the implementation of the operations. The class interface consists of the declarations of all of the operations that correspond to that particular class. It also includes other classes, objects, data structures, and/or constants that are needed to support that interface.

Within a class interface, we can specify whether or not the data and operations can be seen and/or accessed, making C++ a very flexible language. Data and operations that are declared to be public are visible to all clients. Data and operations that are declared to be private cannot be accessed by the client. Think of a class' member functions and private data as being able to work together; no other parts of the program can interfere or alter the value of such private data. Public member functions defined as part of a class can access that class' private data. Therefore, think of public member functions as the interface between private data and the rest of the program.

 

1.1.2 Identifying User Defined Data Types

Identifying classes and objects is one of the hardest parts of data abstraction. As we will see, it is also one of the hardest parts of object-oriented programming. So where do we begin? The best place is to investigate the application's environment. Look for key components and common patterns of interaction among these components. Once these are found and outlined, try to group related classes together. Notice, we don't start by looking at the modules (i.e., don't start with a module to do the I/O and a separate module to do the computations). Instead, start with the key components of the problem's environment, which then get turned into our key abstractions that are realized as classes.

Once we have outlined the necessary classes, they may need to be refined. This is especially necessary when operations are diverse. If one class needs to perform too many different types of operations depending on the "state" of the object (i.e., the values of the data for an instance of the class), then the design is not right. If some classes do the same operations, refine the design to group these classes together.

But what about adding operations? How many operations should be defined for a particular class? Where do we stop? To begin with, come up with operations that represent the minimal number of operations and then add other operations as needed. Determine whether operations are being added because of necessity versus convenience. Only include those for convenience that are really important. Of course, we may later need to revise the design to meet performance criteria. But to begin with, we recommend only including unnecessary operations on the basis of performance efficiency. This can also be done as an iterative refinement, down the road.

One of the goals of data abstraction is to strive for class reuse. Therefore, it is important not to constrain the implementation to a single strategy. The wrong design may create abstractions that are too specialized, causing more work for the programmer and more work to redesign in the future. By following these suggestions, we have taken the first step toward object-oriented programming.

 

 

1.2 Designing the User Defined Types

The key construct for implementing user defined data types is the C++ class. A class is composed of two types of members: data members and member functions. The data members correspond to the abstract data type's data. The member functions correspond to the abstract data type's operations. The member functions apply to each instance of the class (i.e., objects of the class) in a way similar to accessing members of a structure. We can declare these members as either public or private. To prevent access of the data members by clients, we place the data members in the private section. This causes the compiler to enforce the rules of data hiding. Using this approach, only other member functions of the class can access the private data. To allow clients of an abstract data type access to the operations, we place the member functions in the public section.

Without classes, every operation would need an extra argument (a pointer to the structure containing the list head and current pointer) to support multiple, simultaneous lists. Memory for the list head and current pointer would be dynamically allocated when an instance of a list was created. With the class construct, this is transparent to the programmer.

With user defined types, the compiler automatically allocates memory when we define an instance (or object) of a class. When an object is created, memory is allocated for all of the associated data members. Operations can then be invoked by associating the object's name (or reference) with the appropriate member function. For example, instead of writing add(emp_list,ptr,type), we can write emp_list.add(ptr,type). An explicit argument is no longer needed to indicate which instance is being used because C++ implicitly supplies a pointer to the object for each member function.  Therefore, each reference to a member function is associated with an object and each object has its own copy of the data defined by the class. References to data members within member functions are to unique data members belonging to the object that invoked the member function. The effect is exactly the same as when we passed a pointer to the data. The only difference is that the compiler takes care of the details for us and protects our objects from unauthorized access.

 

1.2.1 Implementing Employee and Manager User Defined Data Types

For each class definition, there should be a class interface and a class implementation. A class interface is represented in a header file (.h) and the cor­responding implementation in an implementation file (.cpp). The following demonstrates the class interface for our employee and manager abstract data types.

 

//employee.h class interface (Ex0702)

class employee { //employee class

  public:

    void read();       //read from stdin to employee object

    void write();      //write employee object to stdout

    long get_salary(); //get salary

    int get_years();   //get years of employment

  private:

    char name[32];     //employee name (last, first)

    long salary;       //employee salary (dollars)

    int years;         //years employed by firm

};

 

//manager.h class interface (Ex0702)

class manager { //manager class

  public:

    void read();       //read from stdin to manager object

    void write();      //write manager object to stdout

    long get_salary(); //get salary

    int get_years();   //get years of employment

  private:

    char name[32];     //manager name (last, first)

    long salary;       //manager salary (dollars)

    int years;         //years employed by firm

    int group_size;    //size of group managed

};

 

The class implementation is where the definition of all member functions (i.e., the implementation of the operations) is placed. It is necessary to place each of the member functions within the scope of the corresponding class (called class scope). Otherwise, C++ assumes that we are dealing with a function that is not a member of a class. To place a function within the scope of a class, we must use the scope resolution operator (::). These member functions can then directly access private data and operations that are not otherwise accessible from non-member functions.

The following demonstrates the class implementation for our employee and manager abstract data types. Notice that the employee member functions are preceded with employee:: and the manager member functions are preceded with manager::. Data members of that class can be directly referenced within the implementation of the member functions.

 

//employee.cpp class implementation (Ex0702)

#include <iostream>

using namespace std;

#include "employee.h"

 

void employee::read() {

  char delim;

  cin >>delim;

  cin.getline(name, 32, ':');

  cin >>salary >>delim;

  cin >>years;

  cin.get(delim);

}

 

void employee::write() {

  cout <<"\nEmployee's name:  "  <<name;

  cout <<"\nSalary:           $" <<salary;

  cout <<"\nYears employed:   "  <<years <<endl;

}

 

long employee::get_salary() {

  return (salary);

}

 

int employee::get_years() {

  return (years);

}

 

 

//manager.cpp class implementation (Ex0702)

#include <iostream>

using namespace std;

#include "manager.h"

 

void manager::read() {

  char delim;

  cin >>delim;

  cin.getline(name, 32, ':');

  cin >>salary >>delim;

  cin >>years >>delim;

  cin >>group_size;

  cin.get(delim);

}

 

void manager::write() {

  cout <<"\nManager's name:   "  <<name;

  cout <<"\nGroup size:       "  <<group_size;

  cout <<"\nSalary:           $" <<salary;

  cout <<"\nYears employed:   "  <<years <<endl;

}

 

long manager::get_salary() {

  return (salary);

}

 

int manager::get_years() {

  return (years);

}

 

1.2.2 Implementing the List User Defined Data Types

For unsorted and sorted list abstract data types, the class interface contains the specification of the data and function prototypes. The class implementation contains the implementation of the functions, placed within the scope of the class.

When defining the list class, the functionality to “create” a list is performed by the constructors. Constructors are special member functions that are automatically invoked when an object of this type is created and are used to initialize a new instance. Their only purpose is to initialize the data members. For our list classes, the constructors are named unsorted and sorted (the same names as the classes). They can have no return type and must have the same name as the class.

The functionality to deallocate all dynamic memory (i.e., a destroy-all function) is performed in our classes with destructors. Destructors are special member functions that are automatically invoked when the lifetime of an object of this type is over. Like constructors, destructors also have no return type and have the same name as their class except that the name is preceded by a tilda (~). In the following class definitions, the ~unsorted and ~sorted destructors destroy the lists. Their role is to deallocate any memory dynamically allocated by other member functions in the class. The compiler automatically invokes these functions and also takes care of deleting memory allocated for the data members.

The following demonstrates the class interface for the unsorted and sorted lists:                  

 

//unsorted.h class interface (Ex0702)

 

class unsorted {           //unsorted list class

  public:

    unsorted();            //constructor

    ~unsorted();           //destructor

    void add(void*, char); //add object

    void* next(char &);    //get next object

  private:

    struct node {          //linked list node structure

      char data_type;      //type of object in *data_ptr

      void* data_ptr;      //pointer to object

      node* link;          //pointer to next node in list

    };

    node* list_head;       //list head

    node* cur_ptr;         //current node for next

};

 

//sorted.h class interface (Ex0702)

 

typedef long KEY_TYPE; //type of search key for sorted list

 

class sorted {          //sorted list class

  public:

    sorted();           //constructor

    ~sorted();          //destructor

    void insert(void*, char, KEY_TYPE); //insert object

    void* next(char &); //get next object

  private:

    struct node {       //linked list node structure

      KEY_TYPE key;     //search key in *data_ptr

      char data_type;   //type of object in *data_ptr

      void* data_ptr;   //pointer to object

      node* link;       //pointer to next node in list

    };

    node* list_head;    //list head

    node* cur_ptr;      //current node for next

};

 

With data abstraction we have all of the simplicity of modular abstraction, yet we retain all of the benefits of data abstraction. This is the beauty of C++. We simultaneously gain simplicity and increased functionality.

 

//unsorted.cpp class implementation (Ex0702)

#include "unsorted.h"

 

unsorted::unsorted() {

  cur_ptr = list_head = 0;

}

 

unsorted::~unsorted() {

  node* node_ptr;

  while (node_ptr = list_head) {

    list_head = node_ptr->link;

    delete node_ptr;

  }

}

 

void unsorted::add(void* data_ptr, char data_type) {

  node* node_ptr = new node;

  node_ptr->data_type = data_type;

  node_ptr->data_ptr = data_ptr;

  node_ptr->link = list_head;

  list_head = node_ptr;

}

 

void* unsorted::next(char &type) {

  cur_ptr = cur_ptr ? cur_ptr->link : list_head;

  type = cur_ptr ? cur_ptr->data_type : ' ';

  return (cur_ptr ? cur_ptr->data_ptr : 0);

}

 

 

//sorted.cpp class implementation (Ex0702)

#include "sorted.h"

 

sorted::sorted() {

  cur_ptr = list_head = 0;

}

 

sorted::~sorted() {

  node* node_ptr;

  while (node_ptr = list_head) {

    list_head = node_ptr->link;

    delete node_ptr;

  }

}

 

void sorted::insert(void* data_ptr, char data_type,

                    KEY_TYPE key) {

  node* node_ptr = new node;

  node_ptr->data_type = data_type;

  node_ptr->data_ptr = data_ptr;

  node_ptr->key = key;

 

  node* prev_ptr = 0;

  node* next_ptr = list_head;

  while(next_ptr && (key > next_ptr->key)) {

    prev_ptr = next_ptr;

    next_ptr = next_ptr->link;

  }

  node_ptr->link = next_ptr;

  if (prev_ptr)

    prev_ptr->link = node_ptr;

  else

    list_head = node_ptr;

}

 

void* sorted::next(char &type) {

  cur_ptr = cur_ptr ? cur_ptr->link : list_head;

  type = cur_ptr ? cur_ptr->data_type : ' ';

  return (cur_ptr ? cur_ptr->data_ptr : 0);

}

 

1.5.4 The Client Program

Now that our user defined data types have been created, they are available for access by client programs. Just as before, only the source code for the header files is made available. The details of the implementation can be hidden (in object modules or libraries).

Since the abstract data types are implemented using classes, clients must define objects they wish to use. An object is created by specifying its type (the name of its class) followed by an identifier (the name of the object). For example, our client program defines three different objects: one for an unsorted list of employees (emp_list), another for a sorted list by salary (sal_list), and a third for a sorted list by seniority (sen_list). Memory for these objects is allocated by the compiler. Once allocated, the constructor for that object is automatically invoked. And, just as a regular variable disappears when its lifetime is over, so does an object. Just before the memory is deallocated, the destructor for that object is invoked. The destructor's function is to deallocate any dynamic memory allocated for that object. This is essential since C++ does not automatically deallocate dynamically allocated memory. In the following client program, the destructor is automatically invoked for emp_list, sal_list, and sen_list at the end of the main function.

Objects can also be dynamically allocated using new. This is done the same as if we were dynamically allocating memory for a fundamental data type. Since all memory that is allocated dynamically must be explicitly deallocated, we must delete the memory allocated for an object by using delete. In the following client program, new is used to allocate employee and manager objects and delete is used to deallocate those objects.

The most significant change in our client program occurs when we invoke an operation for an object. Instead of passing the object to a function, we access an operation through the object. Our focus is now completely on objects. We invoke operations on objects using one of the member access operators (the . operator for direct access and the -> operator for indirect access). These operators apply to classes in the same way that they apply to structures. Both operators are demonstrated in the following code.

 

//main.cpp (Ex0702)

#include <iostream>

using namespace std;

#include "employee.h"

#include "manager.h"

#include "unsorted.h"

#include "sorted.h"

 

//read and save employees and managers in unsorted list

void init_unsorted_list(unsorted &emp_list) {

  char type; //specifies type of data

  void* ptr; //pointer to employee or manager

 

  while (cin >>type) {

    switch (type) {

      case 'e': ptr = new employee;

                static_cast<employee*>(ptr)->read();

                break;

      case 'm': ptr = new manager;

                static_cast<manager*>(ptr)->read();

                break;

      default:  char skip;

                while(cin.get(skip) && (skip != '\n'))

                  ;

                continue;

    }

    emp_list.add(ptr, type);

  }

}

 

//get data from unsorted list and insert into sorted lists

void init_sorted_lists(unsorted &emp_list,sorted &sal_list,

                       sorted &sen_list) {

  char type; //specifies type of data

  void* ptr; //pointer to employee or manager

 

  while (ptr = emp_list.next(type))

    switch (type) {

      case 'e': sal_list.insert(ptr, 'e',

                static_cast<employee*>(ptr)->get_salary());

                sen_list.insert(ptr, 'e',

                static_cast<employee*>(ptr)->get_years());

                break;

      case 'm': sal_list.insert(ptr, 'm',

                static_cast<manager*>(ptr)->get_salary());

                sen_list.insert(ptr, 'm',

                static_cast<manager*>(ptr)->get_years());

                break;

    }

}

 

//display employees and managers from the sorted list

void write_sorted_list(sorted &list) {

  char type; //specifies type of data

  void* ptr; //pointer to employee or manager

 

  while (ptr = list.next(type))

    switch (type) {

      case 'e': static_cast<employee*>(ptr)->write();

                break;

      case 'm': static_cast<manager*>(ptr)->write();

                break;

    }

}

 

//deallocate all dynamically allocated objects

void term_unsorted_list(unsorted &emp_list) {

  char type; //specifies type of data

  void* ptr; //pointer to employee or manager

 

  while (ptr = emp_list.next(type))

    switch (type) {

      case 'e': delete static_cast<employee*>(ptr);

                break;

      case 'm': delete static_cast<manager*>(ptr);

                break;

    }

}

 

int main() {

  unsorted emp_list; //unsorted list

  init_unsorted_list(emp_list);

 

  sorted sal_list;   //sorted list

  sorted sen_list;   //sorted list

  init_sorted_lists(emp_list, sal_list, sen_list);

 

  cout <<"\nEmployees Sorted By Salary\n";

  write_sorted_list(sal_list);

 

  cout <<"\nEmployees Sorted By Seniority\n";

  write_sorted_list(sen_list);

 

  term_unsorted_list(emp_list);

  return (0);

}