ABSTRACT AND CONCRETE CLASSES ■ 569 ᮀ Concrete or Abstract? If a class comprises pure virtual methods, you cannot create objects of this class type. Example: Coworker worker("Black , Michael"); The compiler will issue an error message here, as the Coworker class contains the pure virtual method income(). This avoids calling a method for worker that still needs to be defined. A class that does not allow you to create any objects is referred to as an abstract class. In contrast, a class that allows you to create objects is referred to as a concrete class. ᮀ Deriving Abstract Classes When a class is derived from an abstract class, it inherits all the methods the base class contains, particularly the pure virtual methods. If all of these pure virtual methods are implemented in the derived class, you can then create an object of the derived class type. This means you need to implement the income() method in the Laborer class shown opposite. Since the hourly wage and the number of hours worked are both defined for blue-collar workers, it is possible to implement that method. Example: double Laborer::income() { return ( wages * hr ); } A class derived from a concrete class can again contain pure virtual methods, due to additional definitions in the derived class. In other words, an abstract class can be derived from a concrete class. An abstract class does not necessarily need to contain pure virtual functions. If the class contains a protected constructor, objects of the class type cannot be created. The constructor can only be called then by methods in derived classes. A constructor of this type normally acts as base initializer, when an object of a derived class type is cre- ated. A class with pure virtual methods is an abstract class. ✓ NOTE A class derived from a class containing pure virtual methods is a concrete class, if it contains a definition for each pure virtual function. ✓ NOTE 570 ■ CHAPTER 26 ABSTRACT CLASSES // coworker.h: Extending the headerfile. // class Employee : public Coworker { private: double salary; // Pay per month public: Employee( const string& s="", double sa = 0.0) : Coworker(s), salary(g){ } double getSalary() const { return salary; } void setSalary( double sa){ salary = sa; } void display() const; double income()const { return salary; } Employee& operator=( const Coworker& ); Employee& operator=( const Employee& ); }; // coworker_t.cpp : Using the Coworker classes. // #include "coworker.h" int main() { Coworker* felPtr[2]; felPtr[0] = new Laborer("Young, Neil",45., 40); felPtr[1] = new Employee("Smith, Eve", 3850.0); for( int i = 0; i < 2; ++i) { felPtr[i]->display(); cout << "\nThe income of " << felPtr[i]->getName() << " : " << felPtr[i]->income() << endl; } delete felPtr[0]; delete felPtr[1]; return 0; } ■ POINTERS AND REFERENCES TO ABSTRACT CLASSES The derived class Employee Sample program POINTERS AND REFERENCES TO ABSTRACT CLASSES ■ 571 Although you cannot define objects for abstract classes, you can declare pointers and ref- erences to abstract classes. Example: Coworker *felPtr, &coRef; The pointer felPtr is a base class pointer that can address objects belonging to derived concrete classes. The reference coRef can also address objects of this type. ᮀ References to Abstract Base Classes References to base classes are often used as parameters in functions. The copy construc- tor in the Coworker class is just one of them. Example: Coworker( const Coworker& ); The copy constructor expects an object belonging to a derived class, since the base class is abstract. The assignment in the Coworker class has a reference as a parameter and returns a reference to the abstract class. ᮀ Pointers to Abstract Base Classes Pointers to base classes are generally used to address dynamically allocated objects. If the base class is abstract, you can only allocate memory for objects belonging to derived, con- crete classes. Example: Coworker* felPtr; felPtr = new Laborer("Young, Neil",45.,40); cout << felPtr->income(); Since the income()method is virtual, a corresponding function found in the derived class Laborer is executed. ᮀ Polymorphic Interface Defining pure virtual methods also determines interfaces for general operations, although the interfaces still need to implemented in the derived classes. If a derived class contains its own definition of a virtual method, this version will also be executed if an object is referenced by a base class pointer or reference. Abstract classes are therefore also referred to as polymorphic interfaces to derived classes. The opposite page shows the definition of the Employee class, which was also derived from the abstract class Coworker. The operator functions for the assignments are discussed and implemented in the following section. 572 ■ CHAPTER 26 ABSTRACT CLASSES // Virtual Assignment in the base class Coworker& operator=(const Coworker & m) { if( this != &m ) // No self assignment name = m.name; return *this; } Redefining the virtual operator function operator=(), which returns a reference to the derived class, is not yet supported by all compilers. In this case the return type must be a reference to the base class Coworker. ✓ NOTE // Redefinition: virtual Employee& operator=(const Coworker& m) { if( this != &m ) // No self assignment { Coworker::operator=( m ); salary = 0.0; } return *this; } // Standard Assignment: not virtual Employee& operator=(const Employee& a) { if( this != &a ) { Coworker::operator=( a ); salary = a.salary; } return *this; } ■ VIRTUAL ASSIGNMENT Assignment for class Coworker Assignments for class Employee VIRTUAL ASSIGNMENT ■ 573 ᮀ Virtual Operator Functions Operator functions implemented as methods can also be virtual. In this case, you can ensure that the right version of an operator function will be executed when using a pointer or reference to a base class to address a derived class object. One example of this is the operator function for an assignment. If the function decla- ration is not virtual, and if the function is called via a base class pointer, only the base data of the object is overwritten. Any additional data members of the derived class remain unchanged. ᮀ Using Virtual Assignments The assignment was declared virtual in the Coworker base class. The derived classes Laborer and Employee both contain their own versions. Thus, in the following Example: void cpy(Coworker& a,const Coworker& b) { a = b; } the assignment of the Employee class is executed, if an object of this class type is the first argument passed to it. If the object is a Laborer type, the assignment of the Laborer class is performed. In the case of the cpy()function, you can therefore assign two objects of any class, including classes derived at a later stage, without having to modify the function itself! However, you definitely need to define a version of the assignment for each derived class. ᮀ Redefining the Standard Assignment When you define a new version for a virtual method in a derived class, this implies using the signature of the original method. Since the standard assignment of a derived class has a signature of its own, it is not virtual. The standard assignment for the Laborer class has the following prototype: Example: Laborer& operator=(const Laborer&); The type const Laborer& is different from the const Coworker& type of the parameter in the virtual operator function of the base class. The standard assignment thus masks the virtual assignment in the base class. This gives rise to two issues: ■ the virtual operator function for the assignment must be defined for every derived class ■ to ensure that the standard assignment is also available, the standard assignment must also be redefined in every derived class. 574 ■ CHAPTER 26 ABSTRACT CLASSES // cell.h: Defining the classes Cell, BaseEl, and DerivedEl // #ifndef _CELL_ #define _CELL_ #include <string> #include <iostream> using namespace std; class Cell { private: Cell* next; protected: Cell(Cell* suc = NULL ){ next = suc; } public: virtual ~Cell(){ } // Access methods to be declared here. virtual void display() const = 0; }; class BaseEl : public Cell { private: string name; public: BaseEl( Cell* suc = NULL, const string& s = "") : Cell(suc), name(s){} // Access methods would be declared here. void display() const; }; class DerivedEl : public BaseEl { private: string rem; public: DerivedEl(Cell* suc = NULL,const string& s="", const string& b="") : BaseEl(suc, s), rem(b) { } // Access methods would be declared here. void display() const; }; #endif ■ APPLICATION: INHOMOGENEOUS LISTS The abstract base class Cell and derived classes APPLICATION: INHOMOGENEOUS LISTS ■ 575 1st list element Info Info Pointer 2nd list element 3rd list element Info Pointer Pointer ᮀ Terminology Let’s look into implementing an application that uses an inhomogeneous list. An inho- mogeneous list is a linear list whose elements can be of different types. If the data you need to store consists of objects in a class hierarchy, one list element could contain an object belonging to the base class, whereas another could contain an object of a derived class. Due to implicit type conversions in class hierarchies, you can use the base class point- ers to manage the list elements, that is, you can manage the elements in a linked list. The following graphic illustrates this scenario: ᮀ Representing List Elements To separate the management of list elements from the information contained in the list, we have defined an abstract class called Cell as a base class for all list elements. The class contains a pointer of type Cell* as the data member used to link list elements. Since Cell type objects are not be created, the constructor in the Cell class has a protected declaration. The Cell class does not contain any data that might need to be output. However, each class derived from Cell contains data that need to be displayed. For this reason, Cell contains a declaration of the pure virtual method display(), which can be mod- ified for multiple derivations. The classes BaseEl and DerivedEl, which are derived from Cell, represent list elements used for storing information. To keep things simple, the BaseEl class contains only a name, and the DerivedEl class additionally contains a comment. The public declaration section contains a constructor and access method declarations. In addition, a suitable version of the display() method is defined. Both classes are thus concrete classes. 576 ■ CHAPTER 26 ABSTRACT CLASSES Info Info Pointer New element: Info Pointer Pointer Info // List.h: Defining the class InhomList // #ifndef _LIST_H #define _LIST_H #include "cell.h" class InhomList { private: Cell* first; protected: Cell* getPrev(const string& s); void insertAfter(const string& s,Cell* prev); void insertAfter(const string& s, const string& b,Cell* prev); public: // Constructor, Destructor etc void insert(const string& n); void insert(const string& n, const string& b); void displayAll() const; }; #endif void InhomList::insertAfter(const string& s, Cell* prev) { if( prev == NULL ) // Insert at the beginning, first = new BaseEl( first, s); else // middle, or end. { Cell* p = new BaseEl(prev->getNext(), s); prev->setNext(p); } } ■ IMPLEMENTING AN INHOMOGENEOUS LIST Defining class InhomList Inserting a list element in the middle Definition of insertAfter() version IMPLEMENTING AN INHOMOGENEOUS LIST ■ 577 ᮀ The InhomList Class The inhomogeneous list must allow sorted insertion of list elements. It is no longer suffi- cient to append elements to the end of the list; instead, the list must allow insertion at any given point. A pointer to the first element in the list is all you need for this task. You can then use the pointer to the next list element to access any given list element. The definition of the InhomList class is shown opposite. A pointer to Cell has been declared as a data member. The constructor has very little to do. It simply sets the base class pointer to NULL, thus creating an empty list. The list will be sorted by name. When inserting a new element into the list, the inser- tion point—that is the position of the element that will precede the new element—must first be located. In our example, we first locate the immediate lexicographical predeces- sor. The getPrev() method, shown opposite, performs the search and returns either the position of the predecessor or NULL if there is no predecessor. In this case, the new list element is inserted as the first element in the list. ᮀ Inserting a New List Element After finding the insertion position you can call the insertAfter() method that allo- cates memory for a new list element and inserts the element into the list. There are two cases that need to be looked at: 1. If the new element has to be inserted at the start of the list, what was originally the first element now becomes the second. The new element becomes the first element. The first pointer thus needs updating. 2. If the new element is inserted at any other position in the list, the first pointer is not affected. Instead, you have to modify two pointers. The pointer in the pre- ceding list element must be pointed at the new element and the pointer in the new element must be pointed at what was formerly the successor of the preceding element. This situation also applies in cases where the successor was a NULL pointer, in other words, when the new element is appended to the list. Since the list contains objects of the BaseEl and DerivedEl types, the insertAfter() method has been overloaded with two versions. They differ only in different calls to the new operator. The insert() method was overloaded for the same reason. Both versions first call the getPrev() method and the corresponding version of the insertAfter() method. exercise 578 ■ CHAPTER 26 ABSTRACT CLASSES class InhomList { private: Cell* first; protected: Cell* getPrev(const string& s); Cell* getPos(const string& s); void insertAfter(const string& s, Cell* prev); void insertAfter(const string& s,const string& b, Cell* prev); void erasePos(Cell* pos); public: InhomList(){ first = NULL; } InhomList(const InhomList& src); ~InhomList(); InhomList& operator=( const InhomList& src); void insert(const string& n); void insert(const string& n, const string& b); void erase(const string& n); void displayAll() const; }; ■ EXERCISE The complete class InhomList . abstract. The assignment in the Coworker class has a reference as a parameter and returns a reference to the abstract class. ᮀ Pointers to Abstract Base Classes Pointers to base classes are generally. from a concrete class can again contain pure virtual methods, due to additional definitions in the derived class. In other words, an abstract class can be derived from a concrete class. An abstract. defined. A class that does not allow you to create any objects is referred to as an abstract class. In contrast, a class that allows you to create objects is referred to as a concrete class. ᮀ Deriving