|

Polymorphism allows pointers to base classes
like Mammal to be assigned to derived class objects like Cat or Human.
Let's look at an example:
Mammal *MammalPointer = new
Cat;
This would create a new Mammal object on
the heap and return
a pointer to that object which would be assigned to a pointer to Mammal.
The usefulness of this
is more clearly understood when we look at
virtual methods.
| "If
patterns of ones and zeros were like patterns of
human lives and death, if everything about an
individual could be represented in a computer record
by a long string of ones and zeros, then what kind
of creature would be represented by a long string of
lives and deaths?"
- Thomas Pynchon |
Virtual methods - Virtual methods are pointers to base classes that are assigned to derived class objects. For example, if
we wanted to use a pointer to access base class methods in Mammal, and
yet create a Cat object on the free store, virtual methods would allow the Mammal methods
that are overridden in Cat to call the correct functions in Cat, even though the pointer points to
a base class Mammal object. The functions in Mammal that would allow this would be marked with the keyword "virtual".
This allows us to dynamically allocate memory for Mammal objects when
the derived Cat objects don't yet exist, and then later have their
correct derived class methods called.
//Virtual methods
#include <iostream.h>
//--------------------------------------------------------------------------------------------------------------------
class Mammal
{
public:
Mammal():itsAge(1) { cout << "Mammal constructor...\n"; }
~Mammal() { cout << "Mammal destructor...\n"; }
void Move()
const { cout << "Mammal move one step\n"; }
virtual void Speak() const { cout
<< "Mammal making sound!\n"; }
protected:
int itsAge;
};
//--------------------------------------------------------------------------------------------------------------------
class Squirrel
: public Mammal
{
public:
Squirrel() { cout << "Squirrel constructor...\n"; }
~Squirrel() { cout << "Squirrel destructor...\n"; }
void SwishTail() { cout << "Swishing Tail...\n"; }
void Speak()const { cout << "Squeak!\n"; }
void Move()const { cout << "Squirrel moves 5 steps...\n"; }
};
//--------------------------------------------------------------------------------------------------------------------
int main()
{
Mammal *MammalPointer =
new Squirrel;
MammalPointer->Move();
MammalPointer->Speak();
return 0;
} |
Output:
Mammal constructor . . .
Squirrel constructor . . .
Mammal move one step
Squeak!
In the example above, Mammal is provided the VIRTUAL method "speak()".
Speak() is virtual, Move() is not. This indicates this class expects to eventually be another class's base type.
This also signifies that the derived class will probably want to override this function.
A pointer to Mammal object is created, pSquirrel. It is assigned the address of a new
Squirrel object. Squirrel is a Mammal -
it derived from Mammal, so this is legal. The pointer is then used to call the Move() function. The compiler knows
pSquirrel only to be a Mammal, and so looks to the Mammal object to find the Move() method.
This pointer is then used to call the "Speak()" method. Because
the Speak() method is virtual, the Speak() method in Squirrel is invoked,
overriding the Mammal method's Speak().
Note: As far as the calling function knew, it had a Mammal pointer, but
in this case a method from the derived Squirrel class was called. If you had an array of pointers to Mammal, each of which pointed to a subclass of Mammal, you could call each in turn and the correct function would be called instead of the ones in Mammal,
all accomplished by using the keyword "virtual".
//Multiple virtual member functions called in turn
#include <iostream.h>
//--------------------------------------------------------------------------------------------------------------------
class Monster
{
public:
Monster():itsAge(1) { }
~Monster() { }
virtual void
Speak()
const { cout <<
"Monster speaking!\n"; }
protected:
int itsAge;
};
//--------------------------------------------------------------------------------------------------------------------
class Giant
: public Monster
{
public:
void
Speak()const { cout << "Fee!
Fi! Fo! Fum!\n"; }
};
//--------------------------------------------------------------------------------------------------------------------
class Ogre
: public Monster
{
public:
void
Speak()const { cout << "Grrrrrrrrrrrr!\n"; }
};
//--------------------------------------------------------------------------------------------------------------------
class Troll
: public Monster
{
public:
void
Speak()const { cout << "I
hate you!\n"; }
};
//--------------------------------------------------------------------------------------------------------------------
class Dragon
: public Monster
{
public:
void Speak()const { cout << "Hiss!\n"; }
};
//--------------------------------------------------------------------------------------------------------------------
int main()
{
Monster *ArrayOfMonsterPointers[5];
Monster *SingleMonsterPointer;
int choice, i;
for(i = 0; i<5; i++)
{
cout << "(1)Giant (2)Ogre (3)Troll (4)Dragon: ";
cin >> choice;
switch(choice)
{
case 1: SingleMonsterPointer = new
Giant;
break;
case 2:
SingleMonsterPointer = new
Ogre;
break;
case 3:
SingleMonsterPointer = new
Troll;
break;
case 4:
SingleMonsterPointer = new
Dragon;
break;
default:
SingleMonsterPointer = new
Monster;
break;
}
ArrayOfMonsterPointers[i] =
SingleMonsterPointer;
}
for(i = 0;i < 5; i++)
ArrayOfMonsterPointers[i]->Speak();
return 0;
}
|
Output:
(1)Giant (2)Ogre (3)Troll (4)Dragon: 1
(1)Giant (2)Ogre (3)Troll (4)Dragon: 2
(1)Giant (2)Ogre (3)Troll (4)Dragon: 3
(1)Giant (2)Ogre (3)Troll (4)Dragon: 4
(1)Giant (2)Ogre (3)Troll (4)Dragon: 5 |
Fee! Fi! Fo! Fum!
Grrrrrrrrrrrr!
I
hate you!
Hiss!
Monster Speaking! |
In the example listed above, the
Monster class's Speak() function is declared to be virtual. All of
the classes that derive from Monster override the implementation of Speak() in
Monster. In this case, the pointer points to Monster and not the classes
that derive from Monster. The correct functions would not be
called if Monster's Speak() function were not declared to be virtual.
In main, the user is prompted for which objects to create and pointers are added to the array.
As the user makes choices, objects are dynamically created on the heap
by each choice.
This is an example of Dynamic Binding (Runtime Binding),
instead of Static Binding (Compile-time Binding). At compile time, it is unknown which objects will be
chosen to be created on the heap, and therefore which Speak() methods will be invoked. The pointer "SingleMonsterPointer" is bound to
the object it represents on the heap at runtime. Again notice
that, when an object that derive from Monster is created, such as an
Ogre or a Troll, first the constructor for the base class is called, then the constructor for the derived class is called. The
Monster part of the object is contiguous in memory with the Ogre and
Troll part. This brings us to a couple of keywords that have to do
with virtual methods:
v-table - virtual function table, keeps track of virtual functions
vptr - (v-pointer) virtual table pointer, points to v-table.
It is important to note, virtual functions only operate on pointers and references. They don't work when passing by value. Example:
//Invoking
virtual methods when passing by value?
#include <iostream.h>
//--------------------------------------------------------------------------------------------------------------------
class Monster
{
public:
Monster():itsAge(1) { }
~Monster() { }
virtual void
Speak()
const { cout << "Monster speaking!\n"; }
protected:
int itsAge;
};
//--------------------------------------------------------------------------------------------------------------------
class Giant
: public Monster
{
public:
void
Speak()const { cout << "Fee!
Fi! Fo! Fum!\n"; }
};
//--------------------------------------------------------------------------------------------------------------------
class Ogre
: public Monster
{
public:
void
Speak()const { cout << "Grrrrrrrrrrrr!\n"; }
};
//--------------------------------------------------------------------------------------------------------------------
class Troll
: public Monster
{
public:
void
Speak()const { cout << "I
hate you!\n"; }
};
//--------------------------------------------------------------------------------------------------------------------
class Dragon
: public Monster
{
public:
void
Speak()const { cout << "Hiss!\n"; }
};
//--------------------------------------------------------------------------------------------------------------------
//Function
prototypes
void ValueFunction(Monster);
//attempting to pass mammal by value parameter
void PointerFunction(Monster*);
//pointer declared to Mammal parameter
void ReferenceFunction(Monster&);
//reference declared to Mammal parameter
//--------------------------------------------------------------------------------------------------------------------
int main()
{
Monster *MonsterPointer=0;
int choice;
while(1)
{
bool fQuit =
false;
cout << "(1)Giant (2)Ogre (3)Troll (4)Dragon (0)Quit: ";
cin >> choice;
switch(choice)
{
case 0: fQuit =
true;
break;
case 1: MonsterPointer
= new Giant;
break;
case 2: MonsterPointer
= new Ogre;
break;
case 3: MonsterPointer
= new Troll;
break;
case 4: MonsterPointer
= new Dragon;
break;
default: MonsterPointer
= new Monster;
break;
}
if(fQuit)
break;
//Break out
of the while true loop
ValueFunction(*MonsterPointer);
PointerFunction(MonsterPointer);
ReferenceFunction(*MonsterPointer);
} //end while true loop
return 0;
} //close main()
//--------------------------------------------------------------------------------------------------------------------
void ValueFunction(Monster MonsterValue)
//attempting to pass
Monster by value
{ MonsterValue.Speak();
}
void PointerFunction(Monster *pMonster)
//pointer declared
to Monster
{ pMonster->Speak();
}
void ReferenceFunction(Monster &rMonster)
//reference declared to Monster
{
rMonster.Speak();
} |
Output:
(1)Giant (2)Ogre (3)Troll (4)Dragon (0)Quit: 1
Monster Speaking.
Fee! Fi! Fo! Fum!
Fee! Fi! Fo! Fum!
(1)Giant (2)Ogre
(3)Troll (4)Dragon (0)Quit: 2
Monster Speaking.
Grrrrrrrrrrrr!
Grrrrrrrrrrrr! |
(1)Giant
(2)Ogre (3)Troll (4)Dragon (0)Quit: 3
Monster Speaking.
I
hate you!
I hate you!
(1)Giant (2)Ogre (3)Troll (4)Dragon (0)Quit: 4
Monster Speaking.
Hiss!
Hiss!
1)Giant (2)Ogre (3)Troll (4)Dragon (0)Quit: 0 |
When the user enters a choice, a pointer, a reference, and
a value are created rendering 3 lines of output. Since virtual member functions only work with pointers and reference, the first two call the correct
Speak() method from Giant that overrides the Speak() method in the base
class, Monster. The third attempt tries to pass the object by value, but since virtual member functions don't work passing by value, the Speak() method from
Monster is called instead of the one overridden in the Giant class.
In this example 3 functions declared, PointerFunction(), ReferenceFunction(), and ValueFunction(). They take a pointer to a
Monster object, a reference to a Monster object, and a Monster object
itself by value, respectively. They all call the Speak() method.
In the first few lines the user enters 1, causing a Giant object to be
created on the heap. It is then passed as a pointer, by reference, and by value to the 3 functions.
As the user dynamically chooses objects, the pointer will dynamically
bind to the correct methods for its object.
Pointers and references invoke the virtual member functions and the
Giant->Speak() member function is invoked. The
dereferenced pointer is passed by value, so the compiler "SLICES" down
the Giant object to just its Monster part. The Monster Speak() method is
then called.
Virtual destructors and Clone Methods- When a pointer to a derived object is deleted, the derived class's destructor is called automatically, invoking the base class's destructor. If any functions in the class are virtual, the destructor should be as well.
It is important to note that NO constructor can be virtual! What if you want to pass in a pointer to a base object, and have a copy of the correct derived object that is created? A solution is to create a
clone method in the base class and make the method virtual.
Clone methods create a new copy of the current object and then return that object
using the "this" pointer. Because each derived class overrides the clone method, a copy of the
entire derived class is created. Example:
//Virtual copy constructor
- Clone Method
#include <iostream.h>
//
--------------------------------------------------------------------------------------------------------------------------
class Employee
{
public:
Employee():SSN(1) { cout << "Employee constructor.\n"; }
~Employee() { cout << "Employee destructor.\n"; }
Employee (const
Employee & rhs);
//Accessor
methods
double GetSSN()
const {
return SSN; }
void SetSSN(double
social) { SSN = social; }
float GetPayRate()
const { return
PayRate; }
void SetPayRate(float
pay) { PayRate = pay; }
//Virtual methods
virtual
void IdentifySelf()
const { cout << "I
am a generic Employee.\n"; }
virtual
Employee
*Clone() {
return new
Employee(*this);
}
protected:
double SSN;
float PayRate;
};
//Employee
Copy Constructor defined outside the employee class.
Employee::Employee (const
Employee
&
rhs):SSN(rhs.GetSSN())
{
cout << "Employee Copy Constructor...\n";
}
//
--------------------------------------------------------------------------------------------------------------------------
class EmpManager :
public Employee
{
public:
EmpManager() { cout << "EmpManager constructor...\n"; }
~EmpManager() { cout << "EmpManager destructor...\n"; }
EmpManager (const
EmpManager & rhs);
void
IdentifySelf()const { cout << "I
am an manager.\n"; }
virtual
Employee
*Clone() {
return new
EmpManager(*this); }
//Accessor
methods
void
SetManagerLevel(int
level) {ManagerLevel = level;}
int GetManagerLevel()
{ return ManagerLevel;
}
void SetPayBonus(float
bonus) {PayBonus = bonus;}
float GetPayBonus()
{ return PayBonus; }
private:
int ManagerLevel;
float PayBonus;
};
//Manager
copy constructor defined outside class
EmpManager::EmpManager(const
EmpManager &
rhs):
Employee(rhs)
{
cout << "EmpManager copy constructor...\n";
}
//
--------------------------------------------------------------------------------------------------------------------------
class EmpClerk :
public Employee
{
public:
EmpClerk() { cout << "EmpClerk constructor...\n";
PerfectAttendence = false;
EmployeeOfTheMonth = false; }
~EmpClerk() { cout << "EmpClerk destructor...\n"; }
EmpClerk (const EmpClerk &);
void
IdentifySelf()const { cout << "I
am a clerk.\n"; }
virtual
Employee *Clone() {
return new
EmpClerk(*this); }
void
IfEmployeeOfTheMonth() {
if(EmployeeOfTheMonth
== true){
cout << "\nEmployee wins Employee Of The Month
status."
<< "\nUpgrade employee\'s parking place."; }
else { cout << "Sorry. This employee
does not meet the requirements."; }
}
//close function
private:
bool
PerfectAttendence;
bool
EmployeeOfTheMonth;
};
//Clerk
copy constructor defined outside class
EmpClerk::EmpClerk(const
EmpClerk & rhs):
Employee(rhs)
{
cout << "EmpClerk copy constructor...\n";
}
//
--------------------------------------------------------------------------------------------------------------------------
enum EmployeeType{GenericEmployee,
Manager, Clerk};
const int NumEmployeeTypes = 3;
//
--------------------------------------------------------------------------------------------------------------------------
int main()
{
Employee *EmployeePointerArray[NumEmployeeTypes];
Employee *SingleEmployeePointer;
int choice;
int i;
for(i = 0; i<NumEmployeeTypes; i++)
{
cout << "(1)Manager (2)Clerk (3)Employee: ";
cin >> choice;
switch(choice)
{
case
Manager: SingleEmployeePointer =
new EmpManager;
break;
case Clerk: SingleEmployeePointer = new
EmpClerk;
break;
default: SingleEmployeePointer = new
Employee;
break;
}
EmployeePointerArray[i] = SingleEmployeePointer;
} //close for loop
Employee *AnotherEmpPointerArray[NumEmployeeTypes];
for(i=0; i<NumEmployeeTypes; i++)
{
//Calling
IdentifySelf() on a generic Employee pointer works
since it's virtual and overridden in each derived
class.
EmployeePointerArray[i]->IdentifySelf();
AnotherEmpPointerArray[i] = EmployeePointerArray[i]->Clone();
}
for(i=0; i<NumEmployeeTypes; i++)
{
//Calling IdentifySelf() on a generic Employee
pointer works since it's virtual and overridden in
each derived class.
AnotherEmpPointerArray[i]->IdentifySelf(); }
return 0;
} |
Output:
(1)Manager (2)Clerk (3)Employee: 1
Employee constructor . . .
EmpManager constructor . . .
(1)Manager (2)Clerk (3)Employee: 2
Employee constructor . . .
EmpClerk constructor . . .
(1)Manager (2)Clerk (3)Employee: 3
Employee constructor . . .
I am a manager. |
Employee copy constructor . . .
EmpManager copy constructor . . .
I am a clerk.
Employee copy constructor . . .
EmpClerk copy constructor . . .
I am an Employee.
Employee copy constructor . . .
I am a manager.
I am a clerk.
I am a generic Employee. |
In the example above, the switch statement creates an
EmpManager, EmpClerk or Employee object on the free store using
new depending on the choice made by the
user. Doing this calls the corresponding object's constructors and copy constructors. As the switch statement falls through, copy constructors are called in turn as
the virtual Clone() method overridden in each derived class creates
copies each object chosen. On line 1 of output, the user is asked
to make a choice and chooses 1, creating an EmpManager object. The
Employee and EmpManager constructors are invoked. On line 4 of the output,
the user is asked for their choice and chooses an EmpClerk object. The
Employee and EmpClerk constructors are invoked. On line 7 of the output, 3 is
chosen and an Employee object is created. This causes the Employee
constructor to be invoked.
Line 9 of the output is produced by a call to the IdentifySelf() method on first object,
EmpManager. The IdentifySelf() method is declared virtual in the
base class and is overridden in each derived class, so the correct version of
IdentifySelf() is invoked by each call. The virtual Clone() function is then called,
and since it is virtual, the Manager Clone() method is called rather
than the Employee Clone() methods, causing the Employee copy constructor and the
EmpManager copy constructor to be called. The same is repeated for
EmpClerk and Employee on the subsequent lines of output. Finally,
a new array of 3 Employee pointers is created and the for loop iterates
through each of the objects, calling the object's corresponding
IdentifySelf() method.
Let's make the observation again. There are 3 loops here. The 1st loop asks for input 3 times. The 2nd loop calls
the overridden and virtual IdentifySelf(), and then invokes constructors and copy constructors for each object
as its Clone() method is called. The 3rd loop, once constructors have been invoked, calls the
IdentifySelf() method again for each of the 3 objects to demonstrate
that they have been copied.
This is similar to previous example, but a new virtual method has been added to the
Employee class - Clone(). The method, Clone(), returns a pointer to a
new Employee object by calling the copy constructor
and passing in itself (*this) as a
const reference. This is
because it has been declared virtual in the base class and is overridden
in each derived class. EmpManager and EmpClerk both override the Clone() method, and pass copies of themselves to their own copy constructors. Since
Clone() is virtual, this will effectively create a virtual copy constructor.
The result of Clone() is a pointer to a copy of the object which is
stored in a second array.
The
cost of virtual methods - As usual, there is a price to pay for
convenience. In order for virtual methods to function and perform
their magic, "v-tables" must be maintained that point to the right
function. In this respect, virtual methods use more memory.
But once you declare any methods virtual, most of the price in terms of
memory has been paid. In this way, they can sometimes be useful
tools for reducing a program's complexity, when and if the situation
calls for it.
©2004 C. Germany
|