Relationships Among Objects in an Inheritance Hierarchy
Section 12.4 created an employee class hierarchy, in which class BasePlusCommissionEmployee inherited from class CommissionEmployee. The Chapter 12 examples manipulated CommissionEmployee and BasePlusCommissionEmployee objects by using the objects' names to invoke their member functions. We now examine the relationships among classes in a hierarchy more closely. The next several sections present a series of examples that demonstrate how base-class and derived-class pointers can be aimed at base-class and derived-class objects, and how those pointers can be used to invoke member functions that manipulate those objects. Toward the end of this section, we demonstrate how to get polymorphic behavior from base-class pointers aimed at derived-class objects.
In Section 13.3.1, we assign the address of a derived-class object to a base-class pointer, then show that invoking a function via the base-class pointer invokes the base-class functionalityi.e., the type of the handle determines which function is called. In Section 13.3.2, we assign the address of a base-class object to a derived-class pointer, which results in a compilation error. We discuss the error message and investigate why the compiler does not allow such an assignment. In Section 13.3.3, we assign the address of a derived-class object to a base-class pointer, then examine how the base-class pointer can be used to invoke only the base-class functionalitywhen we attempt to invoke derived-class member functions through the base-class pointer, compilation errors occur. Finally, in Section 13.3.4, we introduce virtual functions and polymorphism by declaring a base-class function as virtual. We then assign a derived-class object to the base-class pointer and use that pointer to invoke derived-class functionalityprecisely the capability we need to achieve polymorphic behavior.
A key concept in these examples is to demonstrate that an object of a derived class can be treated as an object of its base class. This enables various interesting manipulations. For example, a program can create an array of base-class pointers that point to objects of many derived-class types. Despite the fact that the derived-class objects are of different types, the compiler allows this because each derived-class object is an object of its base class. However, we cannot treat a base-class object as an object of any of its derived classes. For example, a CommissionEmployee is not a BasePlusCommissionEmployee in the hierarchy defined in Chapter 12a CommissionEmployee does not have a baseSalary data member and does not have member functions setBaseSalary and getBaseSalary. The is-a relationship applies only from a derived class to its direct and indirect base classes.
13.3.1. Invoking Base-Class Functions from Derived-Class Objects
The example in Figs. 13.113.5 demonstrates three ways to aim base-class pointers and derived-class pointers at base-class objects and derived-class objects. The first two are straightforwardwe aim a base-class pointer at a base-class object (and invoke base-class functionality), and we aim a derived-class pointer at a derived-class object (and invoke derived-class functionality). Then, we demonstrate the relationship between derived classes and base classes (i.e., the is-a relationship of inheritance) by aiming a base-class pointer at a derived-class object (and showing that the base-class functionality is indeed available in the derived-class object).
Class CommissionEmployee (Figs. 13.113.2), which we discussed in Chapter 12, is used to represent employees who are paid a percentage of their sales. Class BasePlusCommissionEmployee (Figs. 13.313.4), which we also discussed in Chapter 12, is used to represent employees who receive a base salary plus a percentage of their sales. Each BasePlusCommissionEmployee object is a CommissionEmployee that also has a base salary. Class BasePlusCommissionEmployee's earnings member function (lines 3235 of Fig. 13.4) redefines class CommissionEmployee's earnings member function (lines 7982 of Fig. 13.2) to include the object's base salary. Class BasePlusCommissionEmployee's print member function (lines 3846 of Fig. 13.4) redefines class CommissionEmployee's print member function (lines 8592 of Fig. 13.2) to display the same information as the print function in class CommissionEmployee, as well as the employee's base salary.
Figure 13.1. CommissionEmployee class header file.
1 // Fig. 13.1: CommissionEmployee.h 2 // CommissionEmployee class definition represents a commission employee. 3 #ifndef COMMISSION_H 4 #define COMMISSION_H 5 6 #include // C++ standard string class 7 using std::string; 8 9 class CommissionEmployee 10 { 11 public: 12 CommissionEmployee( const string &, const string &, const string &, 13 double = 0.0, double = 0.0 ); 14 15 void setFirstName( const string & ); // set first name 16 string getFirstName() const; // return first name 17 18 void setLastName( const string & ); // set last name 19 string getLastName() const; // return last name 20 21 void setSocialSecurityNumber( const string & ); // set SSN 22 string getSocialSecurityNumber() const; // return SSN 23 24 void setGrossSales( double ); // set gross sales amount 25 double getGrossSales() const; // return gross sales amount 26 27 void setCommissionRate( double ); // set commission rate 28 double getCommissionRate() const; // return commission rate 29 30 double earnings() const; // calculate earnings 31 void print() const; // print CommissionEmployee object 32 private: 33 string firstName; 34 string lastName; 35 string socialSecurityNumber; 36 double grossSales; // gross weekly sales 37 double commissionRate; // commission percentage 38 }; // end class CommissionEmployee 39 40 #endif |
Figure 13.2. CommissionEmployee class implementation file.
(This item is displayed on pages 692 - 693 in the print version)
1 // Fig. 13.2: CommissionEmployee.cpp 2 // Class CommissionEmployee member-function definitions. 3 #include 4 using std::cout; 5 6 #include "CommissionEmployee.h" // CommissionEmployee class definition 7 8 // constructor 9 CommissionEmployee::CommissionEmployee( 10 const string &first, const string &last, const string &ssn, 11 double sales, double rate ) 12 : firstName( first ), lastName( last ), socialSecurityNumber( ssn ) 13 { 14 setGrossSales( sales ); // validate and store gross sales 15 setCommissionRate( rate ); // validate and store commission rate 16 } // end CommissionEmployee constructor 17 18 // set first name 19 void CommissionEmployee::setFirstName( const string &first ) 20 { 21 firstName = first; // should validate 22 } // end function setFirstName 23 24 // return first name 25 string CommissionEmployee::getFirstName() const 26 { 27 return firstName; 28 } // end function getFirstName 29 30 // set last name 31 void CommissionEmployee::setLastName( const string &last ) 32 { 33 lastName = last; // should validate 34 } // end function setLastName 35 36 // return last name 37 string CommissionEmployee::getLastName() const 38 { 39 return lastName; 40 } // end function getLastName 41 42 // set social security number 43 void CommissionEmployee::setSocialSecurityNumber( const string &ssn ) 44 { 45 socialSecurityNumber = ssn; // should validate 46 } // end function setSocialSecurityNumber 47 48 // return social security number 49 string CommissionEmployee::getSocialSecurityNumber() const 50 { 51 return socialSecurityNumber; 52 } // end function getSocialSecurityNumber 53 54 // set gross sales amount 55 void CommissionEmployee::setGrossSales( double sales ) 56 { 57 grossSales = ( sales < 0.0 ) ? 0.0 : sales; 58 } // end function setGrossSales 59 60 // return gross sales amount 61 double CommissionEmployee::getGrossSales() const 62 { 63 return grossSales; 64 } // end function getGrossSales 65 66 // set commission rate 67 void CommissionEmployee::setCommissionRate( double rate ) 68 { 69 commissionRate = ( rate > 0.0 && rate < 1.0 ) ? rate : 0.0; 70 } // end function setCommissionRate 71 72 // return commission rate 73 double CommissionEmployee::getCommissionRate() const 74 { 75 return commissionRate; 76 } // end function getCommissionRate 77 78 // calculate earnings 79 double CommissionEmployee::earnings() const 80 { 81 return getCommissionRate() * getGrossSales(); 82 } // end function earnings 83 84 // print CommissionEmployee object 85 void CommissionEmployee::print() const 86 { 87 cout << "commission employee: " 88 << getFirstName() << ' ' << getLastName() 89 << " social security number: " << getSocialSecurityNumber() 90 << " gross sales: " << getGrossSales() 91 << " commission rate: " << getCommissionRate(); 92 } // end function print |
Figure 13.3. BasePlusCommissionEmployee class header file.
(This item is displayed on pages 693 - 694 in the print version)
1 // Fig. 13.3: BasePlusCommissionEmployee.h 2 // BasePlusCommissionEmployee class derived from class 3 // CommissionEmployee. 4 #ifndef BASEPLUS_H 5 #define BASEPLUS_H 6 7 #include // C++ standard string class 8 using std::string; 9 10 #include "CommissionEmployee.h" // CommissionEmployee class declaration 11 12 class BasePlusCommissionEmployee : public CommissionEmployee 13 { 14 public: 15 BasePlusCommissionEmployee( const string &, const string &, 16 const string &, double = 0.0, double = 0.0, double = 0.0 ); 17 18 void setBaseSalary( double ); // set base salary 19 double getBaseSalary() const; // return base salary 20 21 double earnings() const; // calculate earnings 22 void print() const; // print BasePlusCommissionEmployee object 23 private: 24 double baseSalary; // base salary 25 }; // end class BasePlusCommissionEmployee 26 27 #endif |
Figure 13.4. BasePlusCommissionEmployee class implementation file.
(This item is displayed on pages 694 - 695 in the print version)
1 // Fig. 13.4: BasePlusCommissionEmployee.cpp 2 // Class BasePlusCommissionEmployee member-function definitions. 3 #include 4 using std::cout; 5 6 // BasePlusCommissionEmployee class definition 7 #include "BasePlusCommissionEmployee.h" 8 9 // constructor 10 BasePlusCommissionEmployee::BasePlusCommissionEmployee( 11 const string &first, const string &last, const string &ssn, 12 double sales, double rate, double salary ) 13 // explicitly call base-class constructor 14 : CommissionEmployee( first, last, ssn, sales, rate ) 15 { 16 setBaseSalary( salary ); // validate and store base salary 17 } // end BasePlusCommissionEmployee constructor 18 19 // set base salary 20 void BasePlusCommissionEmployee::setBaseSalary( double salary ) 21 { 22 baseSalary = ( salary < 0.0 ) ? 0.0 : salary; 23 } // end function setBaseSalary 24 25 // return base salary 26 double BasePlusCommissionEmployee::getBaseSalary() const 27 { 28 return baseSalary; 29 } // end function getBaseSalary 30 31 // calculate earnings 32 double BasePlusCommissionEmployee::earnings() const 33 { 34 return getBaseSalary() + CommissionEmployee::earnings(); 35 } // end function earnings 36 37 // print BasePlusCommissionEmployee object 38 void BasePlusCommissionEmployee::print() const 39 { 40 cout << "base-salaried "; 41 42 // invoke CommissionEmployee's print function 43 CommissionEmployee::print(); 44 45 cout << " base salary: " << getBaseSalary(); 46 } // end function print |
In Fig. 13.5, lines 1920 create a CommissionEmployee object and line 23 creates a pointer to a CommissionEmployee object; lines 2627 create a BasePlusCommissionEmployee object and line 30 creates a pointer to a BasePlusCommissionEmployee object. Lines 37 and 39 use each object's name (CommissionEmployee and BasePlusCommissionEmployee, respectively) to invoke each object's print member function. Line 42 assigns the address of base-class object CommissionEmployee to base-class pointer CommissionEmployeePtr, which line 45 uses to invoke member function print on that CommissionEmployee object. This invokes the version of print defined in base class CommissionEmployee. Similarly, line 48 assigns the address of derived-class object BasePlusCommissionEmployee to derived-class pointer BasePlusCommissionEmployeePtr, which line 52 uses to invoke member function print on that BasePlusCommissionEmployee object. This invokes the version of print defined in derived class BasePlusCommissionEmployee. Line 55 then assigns the address of derived-class object BasePlusCommissionEmployee to base-class pointer CommissionEmployeePtr, which line 59 uses to invoke member function print. The C++ compiler allows this "crossover" because an object of a derived class is an object of its base class. Note that despite the fact that the base class CommissionEmployee pointer points to a derived class BasePlusCommissionEmployee object, the base class CommissionEmployee's print member function is invoked (rather than BasePlusCommissionEmployee's print function). The output of each print member-function invocation in this program reveals that the invoked functionality depends on the type of the handle (i.e., the pointer or reference type) used to invoke the function, not the type of the object to which the handle points. In Section 13.3.4, when we introduce virtual functions, we demonstrate that it is possible to invoke the object type's functionality, rather than invoke the handle type's functionality. We will see that this is crucial to implementing polymorphic behaviorthe key topic of this chapter.
Figure 13.5. Assigning addresses of base-class and derived-class objects to base-class and derived-class pointers.
(This item is displayed on pages 695 - 697 in the print version)
1 // Fig. 13.5: fig13_05.cpp 2 // Aiming base-class and derived-class pointers at base-class 3 // and derived-class objects, respectively. 4 #include 5 using std::cout; 6 using std::endl; 7 using std::fixed; 8 9 #include 10 using std::setprecision; 11 12 // include class definitions 13 #include "CommissionEmployee.h" 14 #include "BasePlusCommissionEmployee.h" 15 16 int main() 17 { 18 // create base-class object 19 CommissionEmployee commissionEmployee( 20 "Sue", "Jones", "222-22-2222", 10000, .06 ); 21 22 // create base-class pointer 23 CommissionEmployee *commissionEmployeePtr = 0; 24 25 // create derived-class object 26 BasePlusCommissionEmployee basePlusCommissionEmployee( 27 "Bob", "Lewis", "333-33-3333", 5000, .04, 300 ); 28 29 // create derived-class pointer 30 BasePlusCommissionEmployee *basePlusCommissionEmployeePtr = 0; 31 32 // set floating-point output formatting 33 cout << fixed << setprecision( 2 ); 34 35 // output objects commissionEmployee and basePlusCommissionEmployee 36 cout << "Print base-class and derived-class objects: "; 37 commissionEmployee.print(); // invokes base-class print 38 cout << " "; 39 basePlusCommissionEmployee.print(); // invokes derived-class print 40 41 // aim base-class pointer at base-class object and print 42 commissionEmployeePtr = &commissionEmployee; // perfectly natural 43 cout << " Calling print with base-class pointer to " 44 << " base-class object invokes base-class print function: "; 45 commissionEmployeePtr->print(); // invokes base-class print 46 47 // aim derived-class pointer at derived-class object and print 48 basePlusCommissionEmployeePtr = &basePlusCommissionEmployee; // natural 49 cout << " Calling print with derived-class pointer to " 50 << " derived-class object invokes derived-class " 51 << "print function: "; 52 basePlusCommissionEmployeePtr->print(); // invokes derived-class print 53 54 // aim base-class pointer at derived-class object and print 55 commissionEmployeePtr = &basePlusCommissionEmployee; 56 cout << " Calling print with base-class pointer to " 57 << "derived-class object invokes base-class print " 58 << "function on that derived-class object: "; 59 commissionEmployeePtr->print(); // invokes base-class print 60 cout << endl; 61 return 0; 62 } // end main
|
13.3.2. Aiming Derived-Class Pointers at Base-Class Objects
In Section 13.3.1, we assigned the address of a derived-class object to a base-class pointer and explained that the C++ compiler allows this assignment, because a derived-class object is a base-class object. We take the opposite approach in Fig. 13.6, as we aim a derived-class pointer at a base-class object. [Note: This program uses classes CommissionEmployee and BasePlusCommissionEmployee of Figs. 13.113.4] Lines 89 of Fig. 13.6 create a CommissionEmployee object, and line 10 creates a BasePlusCommissionEmployee pointer. Line 14 attempts to assign the address of base-class object commissionEmployee to derived-class pointer basePlusCommissionEmployeePtr, but the C++ compiler generates an error. The compiler prevents this assignment, because a CommissionEmployee is not a BasePlusCommissionEmployee. Consider the consequences if the compiler were to allow this assignment. Through a BasePlusCommissionEmployee pointer, we can invoke every BasePlusCommissionEmployee member function, including setBaseSalary, for the object to which the pointer points (i.e., the base-class object commissionEmployee). However, the CommissionEmployee object does not provide a setBaseSalary member function, nor does it provide a baseSalary data member to set. This could lead to problems, because member function setBaseSalary would assume that there is a baseSalary data member to set at its "usual location" in a BasePlusCommissionEmployee object. This memory does not belong to the CommissionEmployee, object so member function setBaseSalary might overwrite other important data in memory, possibly data that belongs to a different object.
Figure 13.6. Aiming a derived-class pointer at a base-class object.
(This item is displayed on pages 698 - 699 in the print version)
1 // Fig. 13.6: fig13_06.cpp 2 // Aiming a derived-class pointer at a base-class object. 3 #include "CommissionEmployee.h" 4 #include "BasePlusCommissionEmployee.h" 5 6 int main() 7 { 8 CommissionEmployee commissionEmployee( 9 "Sue", "Jones", "222-22-2222", 10000, .06 ); 10 BasePlusCommissionEmployee *basePlusCommissionEmployeePtr = 0; 11 12 // aim derived-class pointer at base-class object 13 // Error: a CommissionEmployee is not a BasePlusCommissionEmployee 14 basePlusCommissionEmployeePtr = &commissionEmployee; 15 return 0; 16 } // end main Borland C++ command-line compiler error messages:
GNU C++ compiler error messages:
Microsoft Visual C++.NET compiler error messages:
|
13.3.3. Derived-Class Member-Function Calls via Base-Class Pointers
Off a base-class pointer, the compiler allows us to invoke only bases-class member functions. Thus, if a base-class pointer is aimed at a derived-class object, and an attempt is made to access a derived-class-only member function, a compilation error will occur.
Figure 13.7 shows the consequences of attempting to invoke a derived-class member function off a base-class pointer. [Note: We are again using classes CommissionEmployee and BasePlusCommissionEmployee of Figs. 13.113.4] Line 9 creates commissionEmployeePtra pointer to a CommissionEmployee objectand lines 1011 create a BasePlusCommissionEmployee object. Line 14 aims commissionEmployeePtr at derived-class object basePlusCommissionEmployee. Recall from Section 13.3.1 that the C++ compiler allows this, because a BasePlusCommissionEmployee is a CommissionEmployee (in the sense that a BasePlusCommissionEmployee object contains all the functionality of a CommissionEmployee object). Lines 1822 invoke base-class member functions getFirstName, getLastName, getSocialSecurityNumber, getGrossSales and getCommissionRate off the base-class pointer. All of these calls are legitimate, because BasePlusCommissionEmployee inherits these member functions from CommissionEmployee. We know that commissionEmployeePtr is aimed at a BasePlusCommissionEmployee object, so in lines 2627 we attempt to invoke BasePlusCommissionEmployee member functions getBaseSalary and setBaseSalary. The C++ compiler generates errors on both of these lines, because these are not member functions of base-class CommissionEmployee. The handle can invoke only those functions that are members of that handle's associated class type. (In this case, off a CommissionEmployee *, we can invoke only CommissionEmployee member functions setFirstName, getFirstName, setLastName, getLastName, setSocialSecurityNumber, getSocialSecurityNumber, setGrossSales, getGrossSales, setCommissionRate, getCommissionRate, earnings and print.)
Figure 13.7. Attempting to invoke derived-class-only functions via a base-class pointer.
(This item is displayed on pages 699 - 700 in the print version)
1 // Fig. 13.7: fig13_07.cpp 2 // Attempting to invoke derived-class-only member functions 3 // through a base-class pointer. 4 #include "CommissionEmployee.h" 5 #include "BasePlusCommissionEmployee.h" 6 7 int main() 8 { 9 CommissionEmployee *commissionEmployeePtr = 0; // base class 10 BasePlusCommissionEmployee basePlusCommissionEmployee( 11 "Bob", "Lewis", "333-33-3333", 5000, .04, 300 ); // derived class 12 13 // aim base-class pointer at derived-class object 14 commissionEmployeePtr = &basePlusCommissionEmployee; 15 16 // invoke base-class member functions on derived-class 17 // object through base-class pointer 18 string firstName = commissionEmployeePtr->getFirstName(); 19 string lastName = commissionEmployeePtr->getLastName(); 20 string ssn = commissionEmployeePtr->getSocialSecurityNumber(); 21 double grossSales = commissionEmployeePtr->getGrossSales(); 22 double commissionRate = commissionEmployeePtr->getCommissionRate(); 23 24 // attempt to invoke derived-class-only member functions 25 // on derived-class object through base-class pointer 26 double baseSalary = commissionEmployeePtr->getBaseSalary(); 27 commissionEmployeePtr->setBaseSalary( 500 ); 28 return 0; 29 } // end main Borland C++ command-line compiler error messages:
Microsoft Visual C++.NET compiler error messages:
GNU C++ compiler error messages:
|
It turns out that the C++ compiler does allow access to derived-class-only members from a base-class pointer that is aimed at a derived-class object if we explicitly cast the base-class pointer to a derived-class pointera technique known as downcasting. As you learned in Section 13.3.1, it is possible to aim a base-class pointer at a derived-class object. However, as we demonstrated in Fig. 13.7, a base-class pointer can be used to invoke only the functions declared in the base class. Downcasting allows a program to perform a derived-class-specific operation on a derived-class object pointed to by a base-class pointer. After a downcast, the program can invoke derived-class functions that are not in the base class. We will show you a concrete example of downcasting in Section 13.8.
Software Engineering Observation 13.3
If the address of a derived-class object has been assigned to a pointer of one of its direct or indirect base classes, it is acceptable to cast that base-class pointer back to a pointer of the derived-class type. In fact, this must be done to send that derived-class object messages that do not appear in the base class. |
13.3.4. Virtual Functions
In Section 13.3.1, we aimed a base-class CommissionEmployee pointer at a derived-class BasePlusCommissionEmployee object, then invoked member function print through that pointer. Recall that the type of the handle determines which class's functionality to invoke. In that case, the CommissionEmployee pointer invoked the CommissionEmployee member function print on the BasePlusCommissionEmployee object, even though the pointer was aimed at a BasePlusCommissionEmployee object that has its own customized print function. With virtual functions, the type of the object being pointed to, not the type of the handle, determines which version of a virtual function to invoke.
First, we consider why virtual functions are useful. Suppose that a set of shape classes such as Circle, triangle, Rectangle and Square are all derived from base class Shape. Each of these classes might be endowed with the ability to draw itself via a member function draw. Although each class has its own draw function, the function for each shape is quite different. In a program that draws a set of shapes, it would be useful to be able to treat all the shapes generically as objects of the base class Shape. Then, to draw any shape, we could simply use a base-class Shape pointer to invoke function draw and let the program determine dynamically (i.e., at runtime) which derived-class draw function to use, based on the type of the object to which the base-class Shape pointer points at any given time.
To enable this kind of behavior, we declare draw in the base class as a virtual function, and we override draw in each of the derived classes to draw the appropriate shape. From an implementation perspective, overriding a function is no different than redefining one (which is the approach we have been using until now). An overridden function in a derived class has the same signature and return type (i.e., prototype) as the function it overrides in its base class. If we do not declare the base-class function as virtual, we can redefine that function. By contrast, if we declare the base-class function as virtual, we can override that function to enable polymorphic behavior. We declare a virtual function by preceding the function's prototype with the keyword virtual in the base class. For example,
virtual void draw() const;
would appear in base class Shape. The preceding prototype declares that function draw is a virtual function that takes no arguments and returns nothing. The function is declared const because a draw function typically would not make changes to the Shape object on which it is invoked. Virtual functions do not necessarily have to be const functions.
Software Engineering Observation 13.4
Once a function is declared virtual, it remains virtual all the way down the inheritance hierarchy from that point, even if that function is not explicitly declared virtual when a class overrides it. |
Good Programming Practice 13.1
Even though certain functions are implicitly virtual because of a declaration made higher in the class hierarchy, explicitly declare these functions virtual at every level of the hierarchy to promote program clarity. |
Error-Prevention Tip 13.1
When a programmer browses a class hierarchy to locate a class to reuse, it is possible that a function in that class will exhibit virtual function behavior even though it is not explicitly declared virtual. This happens when the class inherits a virtual function from its base class, and it can lead to subtle logic errors. Such errors can be avoided by explicitly declaring all virtual functions virtual tHRoughout the inheritance hierarchy. |
Software Engineering Observation 13.5
When a derived class chooses not to override a virtual function from its base class, the derived class simply inherits its base class's virtual function implementation. |
If the program invokes a virtual function through a base-class pointer to a derived-class object (e.g., shapePtr->draw ()), the program will choose the correct derived-class draw function dynamically (i.e., at execution time) based on the object typenot the pointer type. Choosing the appropriate function to call at execution time (rather than at compile time) is known as dynamic binding or late binding.
When a virtual function is called by referencing a specific object by name and using the dot member-selection operator (e.g., squareObject.draw()), the function invocation is resolved at compile time (this is called static binding) and the virtual function that is called is the one defined for (or inherited by) the class of that particular objectthis is not polymorphic behavior. Thus, dynamic binding with virtual functions occurs only off pointer (and, as we will soon see, reference) handles.
Now let's see how virtual functions can enable polymorphic behavior in our employee hierarchy. Figures 13.813.9 are the header files for classes CommissionEmployee and BasePlusCommissionEmployee, respectively. Note that the only difference between these files and those of Fig. 13.1 and Fig. 13.3 is that we specify each class's earnings and print member functions as virtual (lines 3031 of Fig. 13.8 and lines 2122 of Fig. 13.9). Because functions earnings and print are virtual in class CommissionEmployee, class BasePlusCommissionEmployee's earnings and print functions override class CommissionEmployee's. Now, if we aim a base-class CommissionEmployee pointer at a derived-class BasePlusCommissionEmployee object, and the program uses that pointer to call either function earnings or print, the BasePlusCommissionEmployee object's corresponding function will be invoked. There were no changes to the member-function implementations of classes CommissionEmployee and BasePlusCommissionEmployee, so we reuse the versions of Fig. 13.2 and Fig. 13.4.
Figure 13.8. CommissionEmployee class header file declares earnings and print functions as virtual.
(This item is displayed on page 703 in the print version)
1 // Fig. 13.8: CommissionEmployee.h 2 // CommissionEmployee class definition represents a commission employee. 3 #ifndef COMMISSION_H 4 #define COMMISSION_H 5 6 #include // C++ standard string class 7 using std::string; 8 9 class CommissionEmployee 10 { 11 public: 12 CommissionEmployee( const string &, const string &, const string &, 13 double = 0.0, double = 0.0 ); 14 15 void setFirstName( const string & ); // set first name 16 string getFirstName() const; // return first name 17 18 void setLastName( const string & ); // set last name 19 string getLastName() const; // return last name 20 21 void setSocialSecurityNumber( const string & ); // set SSN 22 string getSocialSecurityNumber() const; // return SSN 23 24 void setGrossSales( double ); // set gross sales amount 25 double getGrossSales() const; // return gross sales amount 26 27 void setCommissionRate( double ); // set commission rate 28 double getCommissionRate() const; // return commission rate 29 30 virtual double earnings() const; // calculate earnings 31 virtual void print() const; // print CommissionEmployee object 32 private: 33 string firstName; 34 string lastName; 35 string socialSecurityNumber; 36 double grossSales; // gross weekly sales 37 double commissionRate; // commission percentage 38 }; // end class CommissionEmployee 39 40 #endif |
Figure 13.9. BasePlusCommissionEmployee class header file declares earnings and print functions as virtual.
(This item is displayed on pages 703 - 704 in the print version)
1 // Fig. 13.9: BasePlusCommissionEmployee.h 2 // BasePlusCommissionEmployee class derived from class 3 // CommissionEmployee. 4 #ifndef BASEPLUS_H 5 #define BASEPLUS_H 6 7 #include // C++ standard string class 8 using std::string; 9 10 #include "CommissionEmployee.h" // CommissionEmployee class declaration 11 12 class BasePlusCommissionEmployee : public CommissionEmployee 13 { 14 public: 15 BasePlusCommissionEmployee( const string &, const string &, 16 const string &, double = 0.0, double = 0.0, double = 0.0 ); 17 18 void setBaseSalary( double ); // set base salary 19 double getBaseSalary() const; // return base salary 20 21 virtual double earnings() const; // calculate earnings 22 virtual void print() const; // print BasePlusCommissionEmployee object 23 private: 24 double baseSalary; // base salary 25 }; // end class BasePlusCommissionEmployee 26 27 #endif |
We modified Fig. 13.5 to create the program of Fig. 13.10. Lines 4657 demonstrate again that a CommissionEmployee pointer aimed at a CommissionEmployee object can be used to invoke CommissionEmployee functionality, and a BasePlusCommissionEmployee pointer aimed at a BasePlusCommissionEmployee object can be used to invoke BasePlusCommissionEmployee functionality. Line 60 aims base-class pointer commissionEmployeePtr at derived-class object basePlusCommissionEmployee. Note that when line 67 invokes member function print off the base-class pointer, the derived-class BasePlusCommissionEmployee's print member function is invoked, so line 67 outputs different text than line 59 does in Fig. 13.5 (when member function print was not declared virtual). We see that declaring a member function virtual causes the program to dynamically determine which function to invoke based on the type of object to which the handle points, rather than on the type of the handle. The decision about which function to call is an example of polymorphism. Note again that when commissionEmployeePtr points to a CommissionEmployee object (line 46), class CommissionEmployee's print function is invoked, and when CommissionEmployeePtr points to a BasePlusCommissionEmployee object, class BasePlusCommissionEmployee's print function is invoked. Thus, the same messageprint, in this casesent (off a base-class pointer) to a variety of objects related by inheritance to that base class, takes on many formsthis is polymorphic behavior.
Figure 13.10. Demonstrating polymorphism by invoking a derived-class virtual function via a base-class pointer to a derived-class object.
(This item is displayed on pages 704 - 706 in the print version)
1 // Fig. 13.10: fig13_10.cpp 2 // Introducing polymorphism, virtual functions and dynamic binding. 3 #include 4 using std::cout; 5 using std::endl; 6 using std::fixed; 7 8 #include 9 using std::setprecision; 10 11 // include class definitions 12 #include "CommissionEmployee.h" 13 #include "BasePlusCommissionEmployee.h" 14 15 int main() 16 { 17 // create base-class object 18 CommissionEmployee commissionEmployee( 19 "Sue", "Jones", "222-22-2222", 10000, .06 ); 20 21 // create base-class pointer 22 CommissionEmployee *commissionEmployeePtr = 0; 23 24 // create derived-class object 25 BasePlusCommissionEmployee basePlusCommissionEmployee( 26 "Bob", "Lewis", "333-33-3333", 5000, .04, 300 ); 27 28 // create derived-class pointer 29 BasePlusCommissionEmployee *basePlusCommissionEmployeePtr = 0; 30 31 // set floating-point output formatting 32 cout << fixed << setprecision( 2 ); 33 34 // output objects using static binding 35 cout << "Invoking print function on base-class and derived-class " 36 << " objects with static binding "; 37 commissionEmployee.print(); // static binding 38 cout << " "; 39 basePlusCommissionEmployee.print(); // static binding 40 41 // output objects using dynamic binding 42 cout << " Invoking print function on base-class and " 43 << "derived-class objects with dynamic binding"; 44 45 // aim base-class pointer at base-class object and print 46 commissionEmployeePtr = &commissionEmployee; 47 cout << " Calling virtual function print with base-class pointer" 48 << " to base-class object invokes base-class " 49 << "print function: "; 50 commissionEmployeePtr->print(); // invokes base-class print 51 52 // aim derived-class pointer at derived-class object and print 53 basePlusCommissionEmployeePtr = &basePlusCommissionEmployee; 54 cout << " Calling virtual function print with derived-class " 55 << "pointer to derived-class object invokes derived-class " 56 << "print function: "; 57 basePlusCommissionEmployeePtr->print(); // invokes derived-class print 58 59 // aim base-class pointer at derived-class object and print 60 commissionEmployeePtr = &basePlusCommissionEmployee; 61 cout << " Calling virtual function print with base-class pointer" 62 << " to derived-class object invokes derived-class " 63 << "print function: "; 64 65 // polymorphism; invokes BasePlusCommissionEmployee's print; 66 // base-class pointer to derived-class object 67 commissionEmployeePtr->print(); 68 cout << endl; 69 return 0; 70 } // end main
|
13.3.5. Summary of the Allowed Assignments Between Base-Class and Derived-Class Objects and Pointers
Now that you have seen a complete application that processes diverse objects polymorphically, we summarize what you can and cannot do with base-class and derived-class objects and pointers. Although a derived-class object also is a base-class object, the two objects are nevertheless different. As discussed previously, derived-class objects can be treated as if they are base-class objects. This is a logical relationship, because the derived class contains all the members of the base class. However, base-class objects cannot be treated as if they are derived-class objectsthe derived class can have additional derived-class-only members. For this reason, aiming a derived-class pointer at a base-class object is not allowed without an explicit castsuch an assignment would leave the derived-class-only members undefined on the base-class object. The cast relieves the compiler of the responsibility of issuing an error message. In a sense, by using the cast you are saying, "I know that what I'm doing is dangerous and I take full responsibility for my actions."
In the current section and in Chapter 12, we have discussed four ways to aim base-class pointers and derived-class pointers at base-class objects and derived-class objects:
- Aiming a base-class pointer at a base-class object is straightforwardcalls made off the base-class pointer simply invoke base-class functionality.
- Aiming a derived-class pointer at a derived-class object is straightforwardcalls made off the derived-class pointer simply invoke derived-class functionality.
- Aiming a base-class pointer at a derived-class object is safe, because the derived-class object is an object of its base class. However, this pointer can be used to invoke only base-class member functions. If the programmer attempts to refer to a derived-class-only member through the base-class pointer, the compiler reports an error. To avoid this error, the programmer must cast the base-class pointer to a derived-class pointer. The derived-class pointer can then be used to invoke the derived-class object's complete functionality. However, this techniquecalled downcastingis a potentially dangerous operation. Section 13.8 demonstrates how to safely use downcasting.
- Aiming a derived-class pointer at a base-class object generates a compilation error. The is-a relationship applies only from a derived class to its direct and indirect base classes, and not vice versa. A base-class object does not contain the derived-class-only members that can be invoked off a derived-class pointer.
Common Programming Error 13.1
After aiming a base-class pointer at a derived-class object, attempting to reference derived-class-only members with the base-class pointer is a compilation error. |
Common Programming Error 13.2
Treating a base-class object as a derived-class object can cause errors. |