13.3. 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 baseclass and derived-class objects, and
how those pointers can be used to invoke member functions that manipulate those
objects. In Section
13.3.4, 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 baseclass functionality—i.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 functionality—when 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 baseclass function as virtual. We then assign the address of a derived-class object to
the base-class pointer and use that pointer to invoke derived-class
functionality—precisely 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
12—a 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.1–13.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
straightforward—we 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.1–13.2), which we discussed in Chapter
12, is used to represent employees who are paid a
percentage of their sales. Class BasePlusCommissionEmployee (Figs. 13.3–13.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 32–35 of Fig. 13.4) redefines class
CommissionEmployee's earnings member function (lines 79–82 of
Fig. 13.2) to include the object's base salary. Class
BasePlusCommissionEmployee's print member function (lines
38–46 of Fig. 13.4)
redefines class CommissionEmployee's print member function
(lines 85–92 of Fig.
13.2) to display the same information as the print function in
class CommissionEmployee, as well as the employee's base salary.
Fig. 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 <string> // 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
|
Fig. 13.2. CommissionEmployee class
implementation file.
1 // Fig. 13.2: CommissionEmployee.cpp 2 // Class CommissionEmployee member-function definitions. 3 #include <iostream> 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 << "\nsocial security number: " << getSocialSecurityNumber() 90 << "\ngross sales: " << getGrossSales() 91 << "\ncommission rate: " << getCommissionRate(); 92 } // end function print
|
Fig. 13.3. BasePlusCommissionEmployee class
header file.
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 <string> // 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
|
Fig. 13.4. BasePlusCommissionEmployee class
implementation file.
1 // Fig. 13.4: BasePlusCommissionEmployee.cpp 2 // Class BasePlusCommissionEmployee member-function definitions. 3 #include <iostream> 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 << "\nbase salary: " << getBaseSalary(); 46 } // end function print
|
In Fig. 13.5, lines 19–20 create a
CommissionEmployee object and line 23 creates a pointer to a
CommissionEmployee object; lines 26–27 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. This "crossover" is allowed 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'll see that this is crucial to implementing
polymorphic behavior—the key topic of this chapter.
Fig. 13.5. Assigning
addresses of base-class and derived-class objects to base-class and
derived-class pointers.
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 <iostream> 5 using std::cout; 6 using std::endl; 7 using std::fixed; 8 9 #include <iomanip> 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:\n\n"; 37 commissionEmployee.print(); // invokes base-class print 38 cout << "\n\n"; 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 << "\n\n\nCalling print with base-class pointer to " 44 << "\nbase-class object invokes base-class print function:\n\n"; 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 << "\n\n\nCalling print with derived-class pointer to " 50 << "\nderived-class object invokes derived-class " 51 << "print function:\n\n"; 52 basePlusCommissionEmployeePtr->print(); // invokes derived-class print 53 54 // aim base-class pointer at derived-class object and print 55 commissionEmployeePtr = &basePlusCommissionEmployee; 56 cout << "\n\n\nCalling print with base-class pointer to " 57 << "derived-class object\ninvokes base-class print " 58 << "function on that derived-class object:\n\n"; 59 commissionEmployeePtr->print(); // invokes base-class print 60 cout << endl; 61 return 0; 62 } // end main
|
Print base-class and derived-class objects:
commission employee: Sue Jones social security number: 222-22-2222 gross sales: 10000.00 commission rate: 0.06
base-salaried commission employee: Bob Lewis social security number: 333-33-3333 gross sales: 5000.00 commission rate: 0.04 base salary: 300.00
Calling print with base-class pointer to base-class object invokes base-class print function:
commission employee: Sue Jones social security number: 222-22-2222 gross sales: 10000.00 commission rate: 0.06
Calling print with derived-class pointer to derived-class object invokes derived-class print function:
base-salaried commission employee: Bob Lewis social security number: 333-33-3333 gross sales: 5000.00 commission rate: 0.04 base salary: 300.00
Calling print with base-class pointer to derived-class object invokes base-class print function on that derived-class object:
commission employee: Bob Lewis social security number: 333-33-3333 gross sales: 5000.00 commission rate: 0.04
|
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.1–13.4.] Lines 8–9 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.
Fig. 13.6. Aiming a derived-class
pointer at a base-class object.
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:
|
Error E2034 Fig13_06\fig13_06.cpp 14: Cannot convert 'CommissionEmployee *' to 'BasePlusCommissionEmployee *' in function main()
|
GNU C++ compiler error messages:
|
fig13_06.cpp:14: error: invalid conversion from 'CommissionEmployee*' to 'BasePlusCommissionEmployee*'
|
Microsoft Visual C++ 2005 compiler error messages:
|
C:\cppfp_examples\ch13\Fig13_06\fig13_06.cpp(14) : error C2440: '=' : cannot convert from 'CommissionEmployee *__w64 ' to 'BasePlusCommissionEmployee *' Cast from base to derived requires dynamic_cast or static_cast
|
13.3.3. Derived-Class
Member-Function Calls via Base-Class Pointers
Off a base-class pointer, the
compiler allows us to invoke only base-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.1–13.4.] Line 9 creates
commissionEmployeePtr—a pointer to a CommissionEmployee
object—and lines 10–11 create a BasePlusCommissionEmployee object. Line
14 aims commissionEmployeePtr at derived-class object
basePlusCommissionEmployee. Recall from Section 13.3.1 that this is
allowed, because a
BasePlusCommissionEmployee is a
CommissionEmployee (in the sense that a
BasePlusCommissionEmployee object contains all the functionality of a
CommissionEmployee object). Lines 18–22 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 26–27 we attempt to invoke
BasePlusCommissionEmployee member functions getBaseSalary and
setBaseSalary. The compiler generates errors on
both of these calls, because they are not made to member functions of base-class
CommissionEmployee. The handle can be used to
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.)
Fig. 13.7. Attempting to invoke
derived-class-only functions via a base-class pointer.
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 (allowed) 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 (disallowed) 26 double baseSalary = commissionEmployeePtr->getBaseSalary(); 27 commissionEmployeePtr->setBaseSalary( 500 ); 28 return 0; 29 } // end main
|
Borland C++ command-line compiler error messages:
|
Error E2316 Fig13_07\fig13_07.cpp 26: 'getBaseSalary' is not a member of 'CommissionEmployee' in function main() Error E2316 Fig13_07\fig13_07.cpp 27: 'setBaseSalary' is not a member of 'CommissionEmployee' in function main()
|
Microsoft Visual C++ 2005 compiler error messages:
|
C:\cppfp_examples\ch13\Fig13_07\fig13_07.cpp(26) : error C2039: 'getBaseSalary' : is not a member of 'CommissionEmployee' C:\cppfp_examples\ch13\Fig13_07\CommissionEmployee.h(10) : see declaration of 'CommissionEmployee' C:\cppfp_examples\ch13\Fig13_07\fig13_07.cpp(27) : error C2039: 'setBaseSalary' : is not a member of 'CommissionEmployee' C:\cppfp_examples\ch13\Fig13_07\CommissionEmployee.h(10) : see declaration of 'CommissionEmployee'
|
GNU C++ compiler error messages:
|
fig13_07.cpp:26: error: `getBaseSalary' undeclared (first use this function) fig13_07.cpp:26: error: (Each undeclared identifier is reported only once for each function it appears in.) fig13_07.cpp:27: error: `setBaseSalary' undeclared (first use this function)
|
The 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 pointer—a 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 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'll 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. This 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 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 derived 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 a program invokes a virtual
function through a baseclass 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 type—not 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 object—this is not
polymorphic behavior. Thus, dynamic binding with virtual functions occurs only off pointer (and, as we'll soon
see, reference) handles.
Now let's see how virtual functions can enable
polymorphic behavior in our employee hierarchy. Figures 13.8–13.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 30–31 of Fig. 13.8 and lines 21–22 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.
Fig. 13.8. CommissionEmployee class header file
declares earnings and print functions as
virtual.
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 <string> // 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
|
Fig. 13.9. BasePlusCommissionEmployee class
header file declares earnings and print functions as
virtual.
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 <string> // 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 46–57
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. 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 message—print, in this case—sent
(off a base-class pointer) to a variety of objects related by inheritance to
that base class, takes on many forms—this is polymorphic behavior.
Fig. 13.10. Demonstrating polymorphism by invoking a
derived-class virtual function via a base-class pointer to a
derived-class object.
1 // Fig. 13.10: fig13_10.cpp 2 // Introducing polymorphism, virtual functions and dynamic binding. 3 #include <iostream> 4 using std::cout; 5 using std::endl; 6 using std::fixed; 7 8 #include <iomanip> 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 << "\nobjects with static binding\n\n"; 37 commissionEmployee.print(); // static binding 38 cout << "\n\n"; 39 basePlusCommissionEmployee.print(); // static binding 40 41 // output objects using dynamic binding 42 cout << "\n\n\nInvoking print function on base-class and " 43 << "derived-class \nobjects with dynamic binding"; 44 45 // aim base-class pointer at base-class object and print 46 commissionEmployeePtr = &commissionEmployee; 47 cout << "\n\nCalling virtual function print with base-class pointer" 48 << "\nto base-class object invokes base-class " 49 << "print function:\n\n"; 50 commissionEmployeePtr->print(); // invokes base-class print 51 52 // aim derived-class pointer at derived-class object and print 53 basePlusCommissionEmployeePtr = &basePlusCommissionEmployee; 54 cout << "\n\nCalling virtual function print with derived-class " 55 << "pointer\nto derived-class object invokes derived-class " 56 << "print function:\n\n"; 57 basePlusCommissionEmployeePtr->print(); // invokes derived-class print 58 59 // aim base-class pointer at derived-class object and print 60 commissionEmployeePtr = &basePlusCommissionEmployee; 61 cout << "\n\nCalling virtual function print with base-class pointer" 62 << "\nto derived-class object invokes derived-class " 63 << "print function:\n\n"; 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
|
Invoking print function on base-class and derived-class objects with static binding
commission employee: Sue Jones social security number: 222-22-2222 gross sales: 10000.00 commission rate: 0.06
base-salaried commission employee: Bob Lewis social security number: 333-33-3333 gross sales: 5000.00 commission rate: 0.04 base salary: 300.00
Invoking print function on base-class and derived-class objects with dynamic binding
Calling virtual function print with base-class pointer to base-class object invokes base-class print function:
commission employee: Sue Jones social security number: 222-22-2222 gross sales: 10000.00 commission rate: 0.06
Calling virtual function print with derived-class pointer to derived-class object invokes derived-class print function:
base-salaried commission employee: Bob Lewis social security number: 333-33-3333 gross sales: 5000.00 commission rate: 0.04 base salary: 300.00
Calling virtual function print with base-class pointer to derived-class object invokes derived-class print function:
base-salaried commission employee: Bob Lewis social security number: 333-33-3333 gross sales: 5000.00 commission rate: 0.04 base salary: 300.00
|
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 were 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 were derived-class objects—the
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 cast—such 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 straightforward—calls made off the base-class pointer
simply invoke base-class functionality.
Aiming a derived-class pointer at a
derived-class object is straightforward—calls 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
you attempt to refer to a derived-class-only member through the base-class
pointer, the compiler reports an error. To avoid this error, you 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. This
technique, called downcasting, is a potentially dangerous operation—Section
13.8 demonstrates how to safely use downcasting. If
a virtual function is defined in the base
and derived classes (either by inheritance or overriding), and if that function
is invoked on a derived-class object via a base-class pointer, then the
derived-class version of that function is called. This is an example of the
polymorphic behavior that occurs only with virtual
functions.
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. |
No comments:
Post a Comment