CS320 Lecture: Inheritance in C++; Inherited and 9/20/96
Overridden Methods; Protected Members; revised 9/24/97
Polymorphism in C++: Static vs Dynamic
Binding; Virtual Methods; Pure
Virtual Methods and Abstract Classes
Operator Overloading
Materials: Transparencies: transaction.h - Transaction,WithdrawlTransaction
transaction.cc - chooseTransaction +
WithdrawlTransaction::
session.h
Piece class hierarchy and testers
Ditto with virtual on Piece::kind()
Ditto with Piece::kind() pure virtual
Stroustrup Design/Evolution p. 75
money.h
money.cc
excerpts from bank.cc - operators on Money
C++ Reference Manual 13.4
Executables: POLY1.EXE
POLY2.EXE
I. Inheritance in C++
- ----------- -- ---
A. At the start of the course, we said that the distinctive features of OO
could be remembered by the acronym "PIE".
1. What does this stand for?
ASK
2. So far, we have talked about how encapsulation is implemented in C++
by using classes with public and private members.
3. We now turn to inheritance and polymorphism
B. The basic C++ syntax for implementing inheritance is simple: a class
explicitly names the base class (if any) of the class being declared
TRANSPARENCY - transaction.h
declaration of WithdrawlTransaction
1. Note the colon between the class name and the base class
specifier
2. Note the use of the reserved word "public".
a. This specifies that all the public members of the base class
become public members of the derived class. (Private members
of the base class remain private, of course).
b. It is also possible to use the words "protected" and "private"
here. For example, if private were used, all the members of the
base class would become private members of the derived class.
c. The key idea is that this can be used to REDUCE the accessibility
of members, but NOT to increase it.
d. In practice, one rarely sees anything but "public" here.
C. In C++, it is possible for a class to have more than one base class.
This is called MULTIPLE INHERITANCE, and is specified by listing the
base classes separated by commas.
1. Example:
class foo : public bar, public baz
...
2. We save further discussion of multiple inheritance for later in the
course.
D. When a C++ class is derived from another class, the following happens
1. It inherits all of the instance variables, to which it can add
instance variables of its own.
Example - TRANSPARENCY - transaction.h
declarations of Transaction, WithdrawlTransaction
NOTE: COMMENTS HAVE BEEN EDITED OUT TO FIT ON ONE PAGE
Each WithdrawlTransaction object has the following fields:
Inherited from Transaction:
_session
_atm
_bank
_serialNumber
_newBalance
_availableBalance
(_lastSerialNumberAssigned is a class variable shared by all
intances of Transaction and its subclasses)
Added by WithdrawlTransaction:
_fromAccount
_amount
2. It responds to all of the messages its base class responds to,
to which it can add messages of its own. In the case of messages
inherited from the base class, it can either use the base class's
methods or OVERRIDE the base class method with one of its own.
Example - TRANSPARENCY - transaction.h
declarations of Transaction, WithdrawlTransaction
A WithdrawlTransaction object handles the following messages:
doTransactionUseCase - using inherited method
getTransactionSpecificsFromCustomer - overridden
sendToBank - overridden
finishApprovedTransaction - overridden
a. Note that overridden methods are explicitly declared in the
interface of the derived class, while inherited methods are
not.
b. In this case, WithdrawlTransaction does not add any additional
messages to those inherited from Transaction.
c. Note: the static method chooseTransaction is a class method and so
is not associated with a specific object
3. Constructors and destructors are handled specially.
a. The constructor method for a derived class automatically calls
the constructor method for its base class BEFORE its own code
is executed. The code to do this is generated by the compiler.
If the base class constructor requires parameters, they must be
provided by listing the base class constructor with its parameters
in the initializer list of the constructor.
Example: TRANSPARENCY: transaction.cc
WithdrawlTransaction::WithdrawlTransaction()
i. Note how the session, atm, and bank parameters to
WithdrawlTransaction() are passed on to the Transaction()
constructor
ii. In addition to copying these into the _session, _atm, and _bank
fields, the Transaction constructor will also initialize
_serialNumber
b. In like manner, the destructor method for a derived class
automatically calls the destructor method for its base class
AFTER its own code is executed. Nothing explicit need be
supplied, since destructors never take parameters.
c. Note the order - construction occurs in the order base class,
derived class; destruction in the reverse order. This is always
the case in C++ - destructors are called in the reverse order of
the corresponding constructors (LIFO).
E. In our discussion of encapsulation, we mentioned that class members can
have either public or private access. When we use inheritance, a third
access becomes relevant: protected.
1. As your recall, a private member is accessible only from the methods
of the class in which it is declared, and a public member is
accessible everywhere.
2. A protected member is accessible only to the methods of the class in
which it is declared AND ITS SUBCLASSES.
Example: TRANSPARENCY - transaction.h
a. The data members _session, _atm, _bank, _serialNumber, _balance, and
_newBalance are accessible both to the methods of Transaction and
to the methods of WithdrawlTransaction (and other subclasses).
b. The data member _lastSerialNumberAssigned is accessible only to the
methods of Transaction. This is what we want, because the only
time it is used is as part of the process of initializing
_serialNumber in the Transaction constructor.
(Note: the protection on this item and the fact that it is static
are totally independent issues).
II. Polymorphism in C++
-- ------------ -- ---
A. The final piece of the OO "PIE" we need to deal with is Polymorphism.
1. The essential idea behind polymorphism is that different objects
can respond to the same message by using different methods.
2. Actually, as we shall see, polymorphism takes a variety of shapes.
(No pun intended :-) ).
B. One form of polymorphism is based on the fact that whenever a method is
invoked it is always associated with some specific class; thus, two
or more classes can have methods with the same nate without causing
any confusion.
1. Example: In our ATM example, the class both ATM and CardReader have
methods called:
ejectCard()
retainCard()
and both ATM and CashDispenser have a method called
dispenseCash()
and both ATM and EnvelopeAcceptor have a method called
acceptEnvelope()
The ATM version of these methods is implemented by calling the
corresponding method of one of its components, perhaps in conjunction
with displaying some message on the Display.
2. Methods with names like "get", "put", "insert", "remove", "lookup"
etc. tend to occur in many classes.
3. This kind of polymorphism is handled by the compiler, as follows:
a. Each method invocation is associated with a specific class by
virtue of either being applied to a specific object or by the
explicit use of CLASSNAME::
b. The compiler determines the correct method to use by looking up
the name in its table of methods for the appropriate class.
C. Another form of polymorphism is based on the types of the parameters to
a function or method.
1. In Pascal, we got used to the idea that a given identifier could
have only one declaration in any given context.
2. In C++ - as in many other languages - an identifier can name more than
one function or method, provided that each has a distinguishable
parameter list.
a. Example: We might define two versions of a function to
calculate a square root - e.g.
int sqrt(int x);
float sqrt(float x);
i. If we invoke sqrt(2), we get the int version (which returns 1)
ii. If we invoke sqrt(2.0), we get the float version (which returns
1.414...
b. Example: In our chess game example, we might have three different
constructors for class Game:
Game(); // Initiate a game - challenge someone
Game(const char * challenger);// Respond to a challenge from someone
Game(GameFile & file); // Resume a saved game
c. Example: In a game playing program where a human plays against
the machine (rather than another human), we might want a player
who has made a mistake to restart the game at some earlier point,
or even at the beginning. We might have two restart methods:
void restart(); // Start over from beginning
void restart(int noMoves); // Backup a specified number of moves
3. C++ allows any number of functions or methods in a class to have the
same name, as long as each has a distinct SIGNATURE.
a. The signature of a function is constructed from the types of its
parameters - e.g. the signature of
int foo(int x, float y)
is (int, float)
b. The signature of a method is constructed in the same way,
including the implicit first parameter - e.g. the signature of a
method of class Game declared as
void restart(int noMoves)
is (Game *, int)
c. Note that the return value type - if any - is NOT a part of the
signature. That is, one could not declare two functions or
methods with the same name and parameter types, differing only
in the type of the return value - so:
int foo(int x);
float foo(int x);
would NOT be allowed.
4. We say that an identifier that names two or more different functions
or methods in this way is OVERLOADED.
5. Overloading is handled by the compiler as follows:
a. When a function or method is called, the compiler determines the
signature of the needed method from the actual parameters used.
b. It then chooses the correct version of an overloaded function or
method by matching the signature of the method and the call.
c. Note that if there is no method whose signature exactly matches the
call, the compiler applies certain automatic type conversions and
tries again to find a match.
Example: suppose we had methods declared
foo(int i) ...
foo(float f) ...
foo(double d) ...
and the calls:
foo(1);
foo(1.5);
foo(6.02d23);
These would generate calls to the integer, float, and
double methods, respectively.
However, if we had only the double method, all three
would use it, because the compiler automatically converts
an int to a double if necessary to satisfy the declared
argument type of a function.
(NOTE: the full set of rules for such conversions is
complex and can lead to ambiguities in some cases)
D. Another form of polymorphism arises when inheritance is used.
1. Recall that the relationship between a derived class (subclass) and its
base class is called "is a". Any object that is an instance of a
derived class is also, in a very real sense, an instance of the base
class(es) of from which the subclass is derived.
a. It has all the instance variables of the base class (plus, perhaps
more).
b. It responds to all the messages the base class responds to (plus,
perhaps more) - though it may do so in a different way.
2. In C++ and other OO languages, a consequence of this fact is that an
object of a derived class can often be used where an object of the
base class is expected.
a. Example: TRANSPARENCY - transaction.cc
method chooseTransaction is declared to return a value of type
Transaction * - i.e. a pointer to a Transaction. But what it
actually returns is a pointer to a particular subclass of
Transaction: WithdrawlTransaction or DepositTransaction or ...
b. Example: TRANSPARENCY - session.h
field _currentTransaction is declared to be a pointer to a
Transaction - but what it will actually contain is a pointer to
a WithdrawlTransaction or DepositTransaction or ...
c. In particular, the following general rule holds: a variable that
is declared to hold a POINTER or a REFERENCE to an object of
some class can always receive as its value a pointer or reference
(as the case may be) to an object of ANY SUBCLASS of the declared
type (as well, of course, as the class itself).
Example: a variable declared Transaction & or Transaction * can
receive as its value a reference / pointer (as the case
may be) to a Transaction object, a WithdrawlTransaction
object, a DepositTransaction object ...
d. Note, though, that a variable declared to be of a class type (not
pointer or reference to a class type) can only be assigned an
object of its exact class
Example: If we have the declaration
Transaction t;
We CANNOT store a WithdrawlTransaction into t.
(The reason for this is that, in general, a subclass object can
be bigger than an object of its base class due to added
fields).
3. Thus, we can have polymorphic references and pointers in C++, which
in turn allows us to create POLYMORPHIC CONTAINERS (classes that can
contain objects of more than one type)
a. Example: Consider implementing a chess game. We have six basic
types of piece (King, Queen, Bishop, Knight, Rook and Pawn) each
of which can be represented by a subclass of a class Piece.
The chess board is an 8 x 8 array of squares, each of which can
hold any type of piece. We might, then, see something like this:
- In the declaration of class Board:
Piece * _square[8][8];
- In the implementation of the constructor that sets the
board to its initial state:
_square[0][0] = new Rook(WHITE);
_square[0][1] = new Knight(WHITE);
_square[0][2] = new Bishop(WHITE);
...
b. In so doing, of course, we lose some information about the
specific type of each object in the container.
Example: Our chess board might have a method getPiece() which
returns the piece in a specific slot on the board (or NULL if
it is vacant). The return type of this method must be Piece *,
which means that the caller of the method cannot directly know
what type of Piece is coming back, and can only invoke the methods
that are common to all Pieces on the result.
c. One approach to solving this problem might be to have a method
called kind() defined for class Piece that allows a Piece to tell
its caller what kind of Piece.
i. TRANSPARENCY - Piece class hierarchy
ii. Now suppose we wrote the following program:
TRANSPARENCY - Piece class tester #1
Not surprisingly, the output of this program is
? King Queen
iii. But now, continuing the above example, suppose we wrote the
following instead:
TRANSPARENCY - Piece class tester #2
If the user types K, what will get printed? What about Q?
ASK
DEMO POLY1.EXE
- Recall that, in C++, the selection of the correct method to
use in a case like this is made by the COMPILER, and thus
must be based on information available at COMPILE TIME.
- As a result, the version of kind() that will be selected
will be based on the type of the variable ptr, which is
Piece *. Thus, the method Piece::Kind will always be
selected, and the output will always be "?".
- This approach to choosing the correct method to use in a
case like this is called STATIC (or EARLY) BINDING.
iv. As this example illustrates, the method we have been trying
will NOT allow us to recover type information from a
polymorphic pointer.
d. Overcoming this problem involves another form of polymorphism.
E. We have looked at a number of forms of polymorphism which depend on the
COMPILER choosing the correct meaning for a polymorphic name, based on
how it is used. This choice must, of necessity, be made at COMPILE
TIME, based on information available then. We now turn to a form of
polymorphism that defers the interpretation of a polymorphic name
to RUN-TIME.
1. TRANSPARENCY - Revised Piece hierarchy with virtual on kind() in
Piece
a. Only one difference - ASK
b. "virtual" on first declaration of kind()
2. DEMO - POLY2.EXE
a. Test 1 behaves as before
b. Test 2 is different
3. Preceeding a method declaration with virtual informs the compiler
that:
a. The class containing the declaration is likedly to be subclassed.
b. Subclasses are likely to contain their own version of this method.
c. When this method is invoked on a reference or pointer to an object
of this class, the compiler should NOT make the choice of which
actual method to call. Instead, this choice should be deferred
until run time, based on the actual type of the object that is
pointed to or referenced. The compiler generates the code needed
to perform this check.
i. Associated with every class that contains one or more virtual
methods is a special compiler-created table called a VIRTUAL
METHOD TABLE with one entry for every virtual method in the
class, containing a pointer to the correct implementation of
the method for that particular class. (This table is sometimes
called a vtable or vtbl.)
ii. Each object that belongs to such a class contains a hidden
field (sometimes called the vptr) that points to the table for
that class. (All objects of the same class share the same
table).
iii. When a virtual method is called, the compiler generates code
that follows the objects vtpr to the table and gets the
address of the correct method from the appropriate slot.
TRANSPARENCY: Stroustrup Design/Evolution p. 75
d. In contrast to the previous example (static or early binding of a
method to a name), this is known as DYNAMIC (or LATE) BINDING.
e. It should be noted that "virtualness" is inherited. If a method
is declared as virtual in a base class, then it is also virtual
in every class derived from this class, whether or not the word
virtual is explicitly used.
i. TRANSPARENCY - Piece hierarchy version 2
Note omission of "virtual" from kind() method in subclasses -
though it would not have been an error to include id.
ii. It should be noted, though, that inheritance is always based
on the signature of a method - e.g. if some subclass of Piece
contained a kind() method that took one or more parameters
it would not automatically be virtual.
4. C++, then, gives the programmer two choices as to how the correct
version of an overridden method is to be chosen
a. Static (compile-time) binding: the choice is based on the declared
type of the variable through which the method invocation is made.
b. Dynamic (run-time) binding: the choice is based on the actual
type of the object accessed through a reference or pointer.
c. Note that static binding can always be used when a simple variable
is used in the call; dynamic binding only becomes relevant when
we have polymorphic references or pointers.
5. In this regard, C++ differs from some other OO languages:
a. In Smalltalk, dynamic binding is ALWAYS used.
b. In Java, dynamic binding is used UNLESS the method being used
is declared final - in which case it CANNOT be overloaded.
c. The reason for making static binding the default in C++ is that
it is slightly more efficient than dynamic binding - though the
cost of method lookup in a vtable is relatively small.
6. There is another issue we want to consider in conjunction with
virtual methods.
a. A virtual method must be declared as such in the base class that
is common to all the classes that implement it. This means, of
course, that it must have some implementation for that class.
Example: TRANSPARENCY - Piece hierarchy version 2
kind() is declared as a virtual method in class Piece,
and is implemented to return "?"
b. Sometimes, though, the base class is intended to be abstract and
there is not really sensible implementation of the method in that
class.
Example: The above
c. In this case, it is possible to declare a PURE VIRTUAL or
ABSTRACT method
TRANSPARENCY - Piece hierarchy version 3
i. The = 0 in the declaration for kind() in Piece informs the
compiler that there will be no implementation of kind() for
this class. (The entry in the virtual method table for
Piece for this method will not contain a meaningful value.)
ii. A C++ class that contains one or more pure virtual methods
is called an ABSTRACT CLASS, and can serve only as a base
class for other classes. In particular, it is impossible to
construct an object that is of such a class directly.
7. Finally, we should consider the possibility that a virtual method
of a derived class may want to EXTEND the functionality of the base
class method, not totally REPLACE it (as has been the case in our
examples thus far.)
a. Example: Suppose we build a class hierarchy for employees of a
corporation, some of whom are salaried and some of whom
are hourly:
Employee
SalariedEmployee HourlyEmployee
i. All employees have a ssn, name, and address.
ii. Salaried employees also have a salary, and hourly employees
have an hourlyRate
b. Now suppose we want to implement a print() method that prints out
information on the employee. For all employees, we will print
SSN, name, and address. For salaried employees, we will also
print salary, and for hourly employees we will also print hourly
rate.
i. This method must be virtual if we are going to include
employees in polymorphic data structures.
ii. We could have each version implement the full functionality -
i.e. the print() method for SalariedEmployee would contain
four print operations. But this involves duplicate work, and
means that any change to the base class would require the
print() methods of the derived class to change.
c. The following is a better approach:
void Employee::print()
{ cout << "SSN: " << _ssn << endl;
cout << "Name: " << _name << endl;
cout << "Address: " << _address << endl;
}
void SalariedEmployee::print()
{ Employee::print();
cout << "Salary: " << _salary << endl;
}
void HourlyEmployee::print()
{ Employee::print();
cout << "Rate: " << _hourlyRate << endl;
}
Note how the derived class methods explictly call the base class
method. Now any change to the way the base class prints itself
will be automatically picked up by all the derived classes.
III. Operator Overloading in C++
--- -------- ----------- -- ---
A. There is one last form of polymorphism in C++ that we need to consider:
OPERATOR OVERLOADING.
1. We have already seen that C++ allows method names to be overloaded.
2. C++ also allows the built-in operators to be overloaded
B. Example: In the ATM system, one of the most important data types is
that used to represent money. It overloads some of the arithmetic
and comparison operators so that they can be applied to Money. (More
could have been overloaded, but were not needed)
TRANSPARENCY - money.h
TRANSPARENCY - money.cc
TRANSPARENCY - excerpt from bank.cc
C. C++ allows almost any built-in operator to be overloaded, provided that
1. The number of operands remains the same (e.g. ! always takes one
operand, / always takes two operands, and - can be overloaded for
either one operand, two operands, or both.)
2. At least one operand is of a type different from the standard
operator. (You cannot redefine int + int, say)
TRANSPARENCY: C++ Reference Manual 13.4
D. C++ does not require the MEANING of the overloaded operator to be
the same as the builtin one - e.g. += does not have to add the right
operand to the left - it could be overloaded to always set the left
operand to 0, say. (But this would be poor design).
Copyright ©1998 - Russell C. Bjork