Day 12
Inheritance
It is a fundamental aspect of human intelligence to seek out, recognize,
and create relationships among concepts. We build hierarchies, matrices,
networks, and other interrelationships to explain and understand the ways in
which things interact. C++ attempts to capture this in inheritance hierarchies.
Today you will learn
What inheritance is.
How to
derive one class from another.
What
protected access is and how to use it.
What
virtual functions are.
What Is Inheritance?
What is a dog? When you look at your pet, what do you see? A biologist
sees a network of interacting organs, a physicist sees atoms and forces at
work, and a taxonomist sees a representative of the species canine domesticus.
It is
that last assessment that interests us at the moment. A dog is a kind of
canine, a canine is a kind of mammal, and so forth. Taxonomists divide the
world of living things into Kingdom, Phylum, Class, Order, Family, Genus, and
Species.
This hierarchy establishes an is-a relationship. A dog is a kind of
canine. We see this relationship everywhere: A Toyota is a kind of car, which
is a kind of vehicle. A sundae is a kind of dessert, which is a kind of food.
What do we mean when we say something is a kind of something else? We
mean that it is a specialization of that thing. That is, a car is a special
kind of vehicle.
Inheritance and Derivation
The concept dog inherits, that
is, it automatically gets, all the features of a mammal. Because it is a
mammal, we know that it moves and that it breathes air--all mammals move and
breathe air by definition. The concept of a dog adds the idea of barking,
wagging its tail, and so forth to that definition. We can further divide dogs
into hunting dogs and terriers, and we can divide terriers into Yorkshire
Terriers, Dandie Dinmont Terriers, and so forth.
A Yorkshire Terrier is a kind of terrier, therefore it is a kind of dog,
therefore a kind of mammal, therefore a kind of animal, and therefore a kind of
living thing. This hierarchy is represented in Figure 12.1.
C++ attempts to represent these
relationships by enabling you to define classes that derive from one
another.
Derivation is a way of expressing the is-a relationship. You derive a new
class, Dog, from the class Mammal. You don't have to state explicitly that dogs move, because they
inherit that from
Mammal.
New Term: A class which adds new functionality to an existing class is
said to derive from that original class. The original class is said to be the
new class's base class.
If the Dog class
derives from the Mammal class, then Mammal is a
base class of Dog. Derived classes
are supersets of their base classes. Just as dog adds certain features
to the idea of mammal, the Dog class will add certain methods
or data to the Mammal class.
Typically, a base class will have more than one derived class. Just as
dogs, cats, and horses are all types of mammals, their classes would all derive
from the Mammal class.
The Animal Kingdom
To facilitate the discussion of derivation and inheritance, this chapter
will focus on the relationships among a number of classes representing animals.
You can imagine that you have been asked to design a children's game--a
simulation of a farm.
In time
you will develop a whole set of farm animals, including horses, cows, dogs,
cats, sheep, and
so forth. You will create methods for these classes so that they can act
in the ways the child might expect, but for now you'll stub-out each method
with a simple print
statement.
Stubbing-out a function means you'll write only
enough to show that the function was called, leaving the details for later when
you have more time. Please feel free to extend the minimal code provided in
this chapter to enable the animals to act more realistically.
The Syntax of Derivation
When you declare a class, you can indicate what class it derives from by
writing a colon after the class name, the type of derivation (public or
otherwise), and the class from which it derives. The following is an example.
class Dog : public
Mammal
The type of derivation will be
discussed later in this chapter. For now, always use public. The
class
from which you derive must have been declared earlier, or you will get a
compiler error. Listing 12.1 illustrates how to declare a Dog class that is derived from a Mammal class.
Listing 12.1. Simple inheritance.
1: //Listing
12.1 Simple inheritance
2:
#include
<iostream.h>
enum
BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB
};
class
Mammal
{
public:
//
constructors
Mammal();
~Mammal();
12:
//accessors
int
GetAge()const;
void
SetAge(int);
int
GetWeight() const;
void
SetWeight();
18:
//Other
methods
void
Speak();
void
Sleep();
protected:
int
itsAge;
int
itsWeight;
};
28:
class
Dog : public Mammal
{
public:
32:
Dog();
~Dog();
36:
//
Accessors
BREED
GetBreed() const;
void
SetBreed(BREED);
//
Other methods
//
WagTail();
//
BegForFood();
protected:
BREED
itsBreed;
};
This program has no output because it is only a set of class
declarations without their implementations. Nonetheless, there is much to see
here.
Analysis: On lines 6-27, the Mammal class is declared. Note that in this example, Mammal does not derive from any other class. In the real world, mammals do
derive--that is, mammals are kinds of animals. In a C++ program, you can
represent only a fraction of the information you have about any given object.
Reality is far too complex to capture all of it, so every C++ hierarchy is an
arbitrary representation of the data available. The trick of good design is to
represent the areas that you care about in a way that maps back to reality in a
reasonably faithful manner.
The hierarchy has to begin
somewhere; this program begins with Mammal. Because
of this decision,
some member variables that might properly belong in
a higher base class are now represented here. For example, certainly all
animals have an age and weight, so if Mammal is
derived from Animal, we
might expect to inherit those attributes. As it is, the attributes appear in
the Mammal class.
To keep the program reasonably simple and manageable, only six methods
have been put in the Mammal class--four
accessor methods, Speak(), and
Sleep().
The Dog class inherits from Mammal, as
indicated on line 29. Every Dog object will have three member
variables: itsAge, itsWeight, and itsBreed. Note
that the class declaration for Dog does not include the member
variables itsAge and itsWeight. Dog objects inherit these variables
from the Mammal class,
along with all of Mammal's
methods except the copy operator and the
constructors
and destructor.
Private Versus Protected
You may
have noticed that a new access keyword, protected, has been introduced on lines 24 and 45 of Listing 12.1. Previously,
class data had been declared private.
However, private members
are not available to derived classes. You could make itsAge and itsWeight public,
but that is not desirable. You don't want other classes accessing these data
members directly.
What you want is a designation that says, "Make these visible to
this class and to classes that derive from this class." That designation
is protected. Protected data members and functions are fully visible to derived
classes, but are otherwise private.
There are, in total, three access specifiers:
public, protected, and private. If a function has an object of your class, it
can access all the public member data and functions. The member functions, in
turn, can access all private data members and functions of their own class, and
all protected data members and functions of any class from which they derive.
Thus, the function Dog::WagTail() can
access the private data itsBreed and can
access the protected data in the Mammal class.
Even if
other classes are layered between Mammal and Dog (for example, DomesticAnimals), the Dog class
will still be able to access the protected members of
Mammal, assuming that these other
classes
all use public inheritance. Private inheritance is discussed on Day 15,
"Advanced Inheritance."
Listing 12.2 demonstrates how to create objects of type Dog and access the data and functions of that type.
Listing 12.2. Using a derived object.
1: //Listing
12.2 Using a derived object
2:
#include
<iostream.h>
enum
BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB
};
class
Mammal
{
public:
//
constructors
Mammal():itsAge(2),
itsWeight(5){}
~Mammal(){}
12:
//accessors
int
GetAge()const { return itsAge; }
void
SetAge(int age) { itsAge = age; }
int
GetWeight() const { return itsWeight; }
void
SetWeight(int weight) { itsWeight = weight; }
//Other
methods
void
Speak()const { cout << "Mammal sound!\n"; }
22:
23:
protected:
int
itsAge;
int
itsWeight;
};
28:
class
Dog : public Mammal
{
public:
32:
//
Constructors
Dog():itsBreed(YORKIE){}
~Dog(){}
36:
//
Accessors
BREED
GetBreed() const { return itsBreed; }
void
SetBreed(BREED breed) { itsBreed = breed; }
//
Other methods
void
WagTail() { cout << "Tail wagging...\n"; }
void
BegForFood() { cout << "Begging for food...\n"; }
private:
BREED
itsBreed;
};
48:
int
main()
{
Dog
fido;
fido.Speak();
fido.WagTail();
cout
<< "Fido is " << fido.GetAge() << " years
old\n";
return
0;
}
Output: Mammal sound!
Tail wagging...
Fido is 2 years old
Analysis: On lines 6-27, the Mammal class is declared (all of its functions are inline to save
space here). On lines
29-47, the Dog class is declared as a derived class of Mammal. Thus, by these
declarations, all Dogs have an
age, a weight, and a breed.
On line 51, a Dog is declared: Fido. Fido inherits
all the attributes of a Mammal, as well
as all the attributes of a Dog. Thus, Fido knows how to WagTail(), but
also knows how to Speak() and
Sleep().
Constructors and Destructors
Dog
objects are Mammal objects. This is the essence of the is-a relationship. When
Fido is created, his base constructor is called first, creating a Mammal. Then the Dog constructor is called,
completing the construction of the Dog object. Because we gave Fido no parameters, the default constructor was
called in each case. Fido doesn't
exist until he is completely constructed, which means that both his Mammal
part and his Dog part must
be constructed. Thus, both constructors must be called.
When Fido is destroyed, first the Dog destructor will be called and then the destructor for the Mammal
part of Fido. Each destructor is given an opportunity to clean up after its own part
of Fido.
Remember to clean up after your Dog! Listing
12.3 demonstrates this.
Listing 12.3. Constructors and destructors called.
1: //Listing
12.3 Constructors and destructors called.
2:
#include
<iostream.h>
enum
BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB
};
class
Mammal
{
public:
//
constructors
Mammal();
~Mammal();
12:
//accessors
int
GetAge() const { return itsAge; }
void
SetAge(int age) { itsAge = age; }
int
GetWeight() const { return itsWeight; }
void
SetWeight(int weight) { itsWeight = weight; }
//Other
methods
void
Speak() const { cout << "Mammal sound!\n"; }
void
Sleep() const { cout << "shhh. I'm sleeping.\n"; }
protected:
int
itsAge;
int
itsWeight;
28:
class
Dog : public Mammal
{
public:
32:
//
Constructors
Dog();
~Dog();
36:
//
Accessors
BREED
GetBreed() const { return itsBreed; }
void
SetBreed(BREED breed) { itsBreed = breed; }
//
Other methods
void
WagTail() { cout << "Tail wagging...\n"; }
void
BegForFood() { cout << "Begging for food...\n"; }
private:
BREED
itsBreed;
};
48:
Mammal::Mammal():
itsAge(1),
itsWeight(5)
{
cout
<< "Mammal constructor...\n";
}
55:
Mammal::~Mammal()
{
cout
<< "Mammal destructor...\n";
}
60:
Dog::Dog():
itsBreed(YORKIE)
{
cout
<< "Dog constructor...\n";
}
66:
Dog::~Dog()
{
cout
<< "Dog destructor...\n";
}
int
main()
{
fido.Speak();
fido.WagTail();
cout
<< "Fido is " << fido.GetAge() << " years old\n";
return
0;
}
Output: Mammal
constructor...
Dog constructor...
Mammal sound!
Tail wagging...
Fido is 1 years old
Dog destructor...
Mammal destructor...
Analysis: Listing 12.3 is just like Listing 12.2, except that the
constructors and destructors now print to the screen when called. Mammal's constructor is called, then Dog's. At that point the
Dog fully
exists, and its methods can be called. When fido goes out of scope, Dog's destructor is called, followed
by a call to Mammal's
destructor.
Passing Arguments to Base
Constructors
It is possible that you'll want
to overload the constructor of Mammal to take a specific age, and that
you'll
want to overload the Dog constructor to take a breed. How
do you get the age and weight parameters passed up to the right constructor in Mammal? What if Dogs want to initialize weight but Mammals don't?
Base class initialization can be performed during class initialization
by writing the base class name, followed by the parameters expected by the base
class. Listing 12.4 demonstrates this.
Listing 12.4. Overloading constructors in derived classes.
1: //Listing 12.4 Overloading constructors in derived classes 2:
#include
<iostream.h>
enum
BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB
};
class
Mammal
{
public:
//
constructors
Mammal();
Mammal(int
age);
13:
//accessors
int
GetAge() const { return itsAge; }
void
SetAge(int age) { itsAge = age; }
int
GetWeight() const { return itsWeight; }
void
SetWeight(int weight) { itsWeight = weight; }
//Other
methods
void
Speak() const { cout << "Mammal sound!\n"; }
void
Sleep() const { cout << "shhh. I'm sleeping.\n"; }
protected:
int
itsAge;
int
itsWeight;
};
29:
class
Dog : public Mammal
{
public:
33:
//
Constructors
Dog();
Dog(int
age);
Dog(int
age, int weight);
Dog(int
age, BREED breed);
Dog(int
age, int weight, BREED breed);
~Dog();
41:
//
Accessors
BREED
GetBreed() const { return itsBreed; }
void
SetBreed(BREED breed) { itsBreed = breed; }
//
Other methods
void
WagTail() { cout << "Tail wagging...\n"; }
void
BegForFood() { cout << "Begging for food...\n"; }
private:
BREED
itsBreed;
};
53:
Mammal::Mammal():
itsAge(1),
itsWeight(5)
{
}
60:
Mammal::Mammal(int
age):
itsAge(age),
itsWeight(5)
{
cout
<< "Mammal(int) constructor...\n";
}
67:
Mammal::~Mammal()
{
cout
<< "Mammal destructor...\n";
}
72:
Dog::Dog():
Mammal(),
itsBreed(YORKIE)
{
cout
<< "Dog constructor...\n";
}
79:
Dog::Dog(int
age):
Mammal(age),
itsBreed(YORKIE)
{
cout
<< "Dog(int) constructor...\n";
}
86:
Dog::Dog(int
age, int weight):
Mammal(age),
itsBreed(YORKIE)
{
itsWeight
= weight;
cout
<< "Dog(int, int) constructor...\n";
}
94:
Dog::Dog(int
age, int weight, BREED breed):
Mammal(age),
itsBreed(breed)
{
itsWeight
= weight;
cout
<< "Dog(int, int, BREED) constructor...\n";
}
102:
Dog::Dog(int
age, BREED breed):
itsBreed(breed)
{
cout
<< "Dog(int, BREED) constructor...\n";
}
109:
Dog::~Dog()
{
cout
<< "Dog destructor...\n";
}
int
main()
{
Dog
fido;
Dog
rover(5);
Dog
buster(6,8);
Dog
yorkie (3,YORKIE);
Dog
dobbie (4,20,DOBERMAN);
fido.Speak();
rover.WagTail();
cout
<< "Yorkie is " << yorkie.GetAge() << " years
old\n";
cout
<< "Dobbie weighs ";
cout
<< dobbie.GetWeight() << " pounds\n";
return
0;
}
NOTE: The output has been numbered here so that each line can be
referred to in the analysis.
Output:
1: Mammal
constructor...
Dog
constructor...
Mammal(int)
constructor...
Dog(int)
constructor...
Mammal(int)
constructor...
Dog(int,
int) constructor...
Mammal(int)
constructor...
Dog(int,
BREED) constructor....
Mammal(int)
constructor...
Dog(int,
int, BREED) constructor...
Mammal
sound!
Tail
wagging...
Yorkie
is 3 years old.
Dobbie
weighs 20 pounds.
Dog
destructor. . .
Dog
destructor...
Mammal
destructor...
Dog
destructor...
Mammal
destructor...
Dog
destructor...
Mammal
destructor...
Dog
destructor...
Mammal
destructor...
Analysis: In Listing 12.4, Mammal's constructor has been overloaded on line 11 to take an integer, the Mammal's age. The implementation on lines 61-66 initializes itsAge with the value
passed into the constructor and initializes itsWeight with the value 5.
Dog
has overloaded five constructors, on lines 35-39.
The first is the default constructor. The second takes the age, which is the same parameter that the Mammal constructor takes. The third constructor
takes both the age and the weight, the fourth takes the age and breed,
and the fifth takes the age, weight, and breed.
Note that on line 74 Dog's default constructor calls Mammal's default constructor. Although it is not strictly necessary to do
this, it serves as documentation that you intended to call the base
constructor, which takes no parameters. The base constructor would be called in
any case, but actually doing so makes your intentions explicit.
The implementation for the Dog
constructor, which takes an integer, is on lines 80-85. In its initialization
phase (lines 81-82), Dog initializes its base class,
passing in the parameter, and then it initializes its breed.
Another Dog constructor is on lines 87-93.
This one takes two parameters. Once again it initializes its base class by
calling the appropriate constructor, but this time it also assigns weight to its base class's variable itsWeight. Note that you cannot assign to the base class variable in the
initialization phase. Because Mammal does not
have a constructor that takes this parameter, you must
do this within the body of the Dog's
constructor.
Walk
through the remaining constructors to make sure you are comfortable with how
they work. Note what is initialized and what must wait for the body of the
constructor.
The output has been numbered so that each line can be referred to in
this analysis. The first two lines of output represent the instantiation of Fido, using the default constructor.
In the output, lines 3 and 4 represent the creation of rover. Lines 5 and 6 represent buster. Note that the Mammal
constructor that was called is the constructor that takes one integer, but the Dog
constructor
is the constructor that takes two integers.
After all the objects are
created, they are used and then go out of scope. As each object is destroyed,
first the Dog destructor and then the Mammal destructor is called, five of each in total.
Overriding Functions
A Dog object has access to all the
member functions in class Mammal, as well
as to any member functions, such as WagTail(), that the declaration of the Dog class
might add. It can also override a
base
class function. Overriding a function means changing the implementation of a
base class function in a derived class. When you make an object of the derived
class, the correct function is called.
New Term: When a derived class creates a function with the same return
type and signature as a member function in
the base class, but with a new implementation, it is said to be overriding that method.
When you override a function, it must agree in
return type and in signature with the function in the base class. The signature
is the function prototype other than the return type: that is, the name, the
parameter list, and the keyword const if used.
New Term: The signature of a function is its
name, as well as the number and type of its parameters. The signature does not include the return type.
Listing 12.5 illustrates what happens if the Dog class overrides the Speak() method
in Mammal. To save room, the accessor
functions have been left out of these classes.
Listing 12.5. Overriding a base class methodin a derived
class.
1: //Listing 12.5 Overriding a base class method in a derived
class
2:
#include
<iostream.h>
enum
BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB
};
class
Mammal
{
public:
//
constructors
Mammal()
{ cout << "Mammal constructor...\n"; }
~Mammal()
{ cout << "Mammal destructor...\n"; }
//Other
methods
void
Sleep()const { cout << "shhh. I'm sleeping.\n"; }
protected:
int
itsAge;
int
itsWeight;
};
22:
class
Dog : public Mammal
{
public:
26:
//
Constructors
Dog(){
cout << "Dog constructor...\n"; }
~Dog(){
cout << "Dog destructor...\n"; }
//
Other methods
void
WagTail() { cout << "Tail wagging...\n"; }
void
BegForFood() { cout << "Begging for food...\n"; }
void
Speak()const { cout << "Woof!\n"; }
35:
private:
BREED
itsBreed;
};
39:
int
main()
{
Mammal
bigAnimal;
Dog
fido;
bigAnimal.Speak();
fido.Speak();
return
0;
}
Output: Mammal
constructor...
Mammal constructor...
Dog constructor...
Mammal sound!
Woof!
Dog destructor...
Mammal destructor...
Mammal destructor...
Analysis: On line 34, the Dog class overrides the Speak() method, causing Dog objects to say
Woof!
when the Speak() method is called. On line 42, a Mammal object, bigAnimal, is
created, causing the first line of output
when the Mammal
constructor is called. On line 43, a Dog object, fido, is
created, causing the next two lines of output, where the
Mammal constructor and then the
Dog constructor
are called.
On line 44, the Mammal object
calls its Speak() method,
then on line 45, the Dog object calls its Speak()
method. The output reflects that the correct
methods were called. Finally, the two objects
go out of
scope and the destructors are called.
Overloading Versus Overriding
These terms are similar, and they do similar things. When you overload a
method, you create more than one method with the same name, but with a
different signature. When you override a method, you create a method in a
derived class with the same name as a method in the base class and the same
signature.
Hiding the Base Class Method
In the previous listing, the Dog class's Speak() method
hides the base class's method. This is just what is wanted, but it can have
unexpected results. If Mammal has a
method, Move(), which
is overloaded, and Dog overrides that method, the Dog method will hide all of the Mammal methods
with that
name.
If Mammal overloads Move() as three
methods--one that takes no parameters, one that takes an integer, and one that
takes an integer and a direction--and Dog overrides just the Move() method
that
takes no parameters, it will not be easy to access the other two methods
using a Dog object. Listing 12.6 illustrates
this problem.
Listing 12.6. Hiding methods.
1: //Listing 12.6 Hiding methods 2:
3: #include <iostream.h> 4:
class
Mammal
{
public:
void
Move() const { cout << "Mammal move one step\n"; }
void
Move(int distance) const
{
11: cout
<< "Mammal move ";
12: cout
<< distance <<" _steps.\n";
}
protected:
int
itsWeight;
};
18:
class
Dog : public Mammal
{
public:
//
You may receive a warning that you are hiding a function!
void
Move() const { cout << "Dog move 5 steps.\n"; }
};
25:
int
main()
{
Mammal
bigAnimal;
Dog
fido;
bigAnimal.Move();
bigAnimal.Move(2);
fido.Move();
//
fido.Move(10);
return
0;
}
Output: Mammal move one
step
Mammal move 2 steps.
Dog move 5 steps.
Analysis: All of the extra
methods and data have been removed from these classes. On lines 8 and 9, the Mammal class declares the overloaded Move() methods. On line 18, Dog overrides the version of Move() with no parameters. These are invoked on lines 30-32, and
the output reflects this as
executed.
Line 33, however, is commented out, as it causes a compile-time error.
While the Dog class could have called the Move(int) method if it had not overridden the version of Move() without
parameters,
now that it has done so it must override both if it wishes to use both. This is
reminiscent of the rule that if you supply any constructor, the compiler will
no longer supply a default constructor.
It is a common mistake to hide a base class method when you intend to
override it, by forgetting to include the keyword const. const is part
of the signature, and leaving it off changes the signature
and thus
hides the method rather than overriding it.
Overriding Versus Hiding
In the
next section, virtual methods are described. Overriding a virtual method
supports polymorphism--hiding it undermines polymorphism. You'll see more on
this very soon.
If you have overridden the base
method, it is still possible to call it by fully qualifying the name of the
method. You do this by writing the base name, followed by two colons and
then the method name. For example: Mammal::Move().
It would have been possible to
rewrite line 28 in Listing 12.6 so that it would compile, by writing
fido.Mammal::Move(10);
This calls the Mammal method
explicitly. Listing 12.7 fully illustrates this idea.
Listing 12.7. Calling base method from overridden method.
1: //Listing
12.7 Calling base method from overridden method.
2:
3: #include <iostream.h> 4:
class
Mammal
{
public:
void
Move() const { cout << "Mammal move one step\n"; }
void
Move(int distance) const
{
11: cout
<< "Mammal move " << distance;
12: cout
<< " steps.\n";
13: }
14:
protected:
int
itsAge;
int
itsWeight;
};
19:
class
Dog : public Mammal
{
public:
void
Move()const;
24:
25: }; 26:
void
Dog::Move() const
{
cout
<< "In dog move...\n";
Mammal::Move(3);
}
int
main()
{
Mammal
bigAnimal;
Dog
fido;
bigAnimal.Move(2);
fido.Mammal::Move(6);
return
0;
}
Output: Mammal move 2
steps.
Mammal move 6 steps.
Analysis: On line 35, a Mammal, bigAnimal, is created, and on line 36, a Dog, fido, is created. The method call on
line 37 invokes the Move() method of Mammal, which takes an int.
The programmer wanted to invoke Move(int) on the Dog object, but had a problem. Dog overrides the Move() method,
but does not overload it and does not provide a version that takes an int. This is solved by the explicit call to the base class Move(int) method on line 33.
DO extend the functionality of tested classes by deriving. DO change the behavior of certain functions in the derived class
by overriding the base class methods. DON'T
hide a base class function by changing the function signature.
Virtual Methods
This
chapter has emphasized the fact that a Dog object
is a Mammal object. So far that has meant
only that the Dog object has inherited the
attributes (data) and capabilities (methods) of its base class. In C++ the is-a
relationship runs deeper than that, however.
C++ extends its polymorphism to allow pointers to base classes to be
assigned to derived class objects. Thus, you can write
Mammal* pMammal = new
Dog;
This creates a new Dog object on the heap and returns a
pointer to that object, which it assigns to a pointer to Mammal. This is fine, because a dog is a mammal.
NOTE: This is the essence of polymorphism. For example, you could
create many
different types of windows, including dialog boxes, scrollable windows,
and list boxes, and give them each a virtual draw() method. By creating a pointer to a window and assigning dialog boxes
and other derived types to that pointer, you can call draw()
without regard to the actual run-time type of the object pointed to. The
correct draw() function
will be called.
You can then use this pointer to invoke any method on Mammal. What you would like is for those methods that are overridden in Dog() to call the correct function. Virtual functions let you do that.
Listing 12.8 illustrates how this works, and what happens with non-virtual
methods.
Listing 12.8. Using virtual methods.
1: //Listing 12.8 Using virtual methods 2:
3: #include
<iostream.h>
4:
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 speak!\n"; }
protected:
int
itsAge;
14:
15: }; 16:
class
Dog : public Mammal
{
public:
Dog()
{ cout << "Dog Constructor...\n"; }
~Dog()
{ cout << "Dog destructor...\n"; }
void
WagTail() { cout << "Wagging Tail...\n"; }
void
Speak()const { cout << "Woof!\n"; }
void
Move()const { cout << "Dog moves 5 steps...\n"; }
};
26:
int
main()
{
29:
Mammal
*pDog = new Dog;
pDog->Move();
pDog->Speak();
33:
return
0;
}
Dog Constructor...
Mammal move one step
Woof!
Analysis: On line 11, Mammal is provided a virtual method--speak(). The designer of this class thereby signals that she expects this class eventually to be
another class's base type. The derived class will probably want to override
this function.
On line 30, a pointer to Mammal is
created (pDog), but it is assigned the address of a new Dog
object. Because a dog is a mammal, this is a legal assignment. The
pointer is then used to call the Move() function.
Because the compiler knows pDog only to
be a Mammal, it looks to the
Mammal object to find the Move() method.
On line 32, the pointer then calls the Speak() method. Because Speak() is
virtual, the Speak() method
overridden in Dog is invoked.
This is
almost magical. As far as the calling function knew, it had a Mammal pointer, but here a method on Dog was called. In fact, 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.
Listing
12.9
illustrates this idea.
Listing 12.9. Multiple virtual functions called in turn.
1: //Listing
12.9 Multiple virtual functions called in turn
2:
3: #include <iostream.h> 4:
class
Mammal
{
public:
Mammal():itsAge(1)
{ }
~Mammal()
{ }
virtual
void Speak() const { cout << "Mammal speak!\n"; }
protected:
int
itsAge;
};
14:
class
Dog : public Mammal
{
public:
void
Speak()const { cout << "Woof!\n"; }
};
20:
class
Cat : public Mammal
{
public:
void
Speak()const { cout << "Meow!\n"; }
};
27:
28:
class
Horse : public Mammal
{
public:
void
Speak()const { cout << "Winnie!\n"; }
};
34:
class
Pig : public Mammal
{
public:
void
Speak()const { cout << "Oink!\n"; }
};
40:
int
main()
{
Mammal*
theArray[5];
Mammal*
ptr;
int
choice, i;
for
( i = 0; i<5; i++)
{
48:
|
cout << "(1)dog (2)cat (3)horse (4)pig:
";
|
49:
|
cin
>> choice;
|
50:
|
switch
(choice)
|
51:
|
{
|
52:
|
case
1: ptr = new Dog;
|
53:
|
break;
|
54:
|
case
2: ptr = new Cat;
|
55:
|
break;
|
56:
|
case
3: ptr = new Horse;
|
57:
|
break;
|
58:
|
case
4: ptr = new Pig;
|
59:
|
break;
|
60:
|
default:
ptr = new Mammal;
|
61:
|
break;
|
62:
|
}
|
63:
|
theArray[i]
= ptr;
|
}
for
(i=0;i<5;i++)
66: theArray[i]->Speak();
}
Output: (1)dog (2)cat (3)horse (4)pig: 1 (1)dog (2)cat (3)horse
(4)pig: 2
(1)dog (2)cat (3)horse
(4)pig: 3
(1)dog (2)cat (3)horse
(4)pig: 4
(1)dog (2)cat (3)horse (4)pig: 5 Woof!
Meow!
Winnie!
Oink!
Mammal speak!
Analysis: This stripped-down
program, which provides only the barest functionality to each class,
illustrates virtual functions in their purest form. Four classes are
declared; Dog, Cat, Horse, and Pig
are all derived from Mammal.
On line 10, Mammal's Speak() function is declared to be virtual. On lines 18, 25, 32, and 38, the
four derived classes override the implementation of Speak().
The user is prompted to pick which objects to create, and the pointers
are added to the array on lines 46-64.
NOTE: At compile time, it is impossible to know which objects will
be created, and thus which Speak() methods will be
invoked. The pointer ptr is bound to its object
at runtime. This is called dynamic binding, or run-time binding, as
opposed to static binding, or compile-time binding.
How Virtual Functions Work
When a derived object, such as a Dog object,
is created, first the constructor for the base class is
called
and then the constructor for the derived class is called. Figure 12.2 shows
what the Dog object looks like after it is
created. Note that the Mammal part of
the object is contiguous in memory with
the Dog part.
When a virtual function is created in an object, the object must keep
track of that function. Many compilers build a virtual function table, called a
v-table. One of these is kept for each type, and each object of that type keeps
a virtual table pointer (called a vptr or v-pointer), which points to that table.
While implementations vary, all
compilers must accomplish the same thing, so you won't be too
Each object's vptr points
to the v-table which, in turn, has a pointer to each of the virtual functions.
(Note,
pointers to functions will be discussed in depth on Day 14, "Special
Classes and Functions.") When the Mammal part of the Dog is created, the vptr is initialized to point to the correct part of the
v-table,
as shown in Figure 12.3.
When the Dog constructor is called, and the Dog part of this object is added, the vptr is
adjusted to point to the virtual function overrides (if any) in the Dog object (see Figure 12.4) .
When a pointer to a Mammal is used,
the vptr
continues to point to the correct function, depending on the "real"
type of the object. Thus, when Speak() is
invoked, the correct function is invoked.
You Cant Get There from Here
If the Dog object had a method, WagTail(), which is not in the Mammal, you
could not use the pointer to Mammal to
access that method (unless you cast it to be a pointer to Dog). Because WagTail() is not a
virtual function, and because it is not in a Mammal object, you can't get there
without either a Dog object
or a Dog pointer.
Although you can transform the Mammal pointer into a Dog pointer, there are usually far
better and safer ways to call the WagTail() method. C++ frowns on explicit casts because they are error-
prone. This subject will be addressed in depth when multiple inheritance
is covered tomorrow, and again when templates are covered on Day 20,
"Exceptions and Error Handling."
Slicing
Note that the virtual function magic operates only on pointers and
references. Passing an object by value will not enable the virtual functions to
be invoked. Listing 12.10 illustrates this problem.
Listing 12.10. Data slicing when passing by value.
1: //Listing 12.10 Data slicing with passing by value 2:
3: #include
<iostream.h>
4:
enum
BOOL { FALSE, TRUE };
class
Mammal
{
public:
Mammal():itsAge(1)
{ }
virtual
void Speak() const { cout << "Mammal speak!\n"; }
protected:
int
itsAge;
};
15:
class
Dog : public Mammal
{
public:
void
Speak()const { cout << "Woof!\n"; }
};
21:
class
Cat : public Mammal
{
public:
void
Speak()const { cout << "Meow!\n"; }
};
27:
28 void
ValueFunction (Mammal);
void
PtrFunction (Mammal*);
void
RefFunction (Mammal&);
int
main()
{
Mammal*
ptr=0;
int
choice;
while
(1)
{
37:
|
BOOL
fQuit = FALSE;
|
38:
|
cout << "(1)dog (2)cat (0)Quit: ";
|
39:
|
cin
>> choice;
|
40:
|
switch
(choice)
|
41:
|
{
|
42:
|
case
0: fQuit = TRUE;
|
43:
|
break;
|
44:
|
case
1: ptr = new Dog;
|
45:
|
break;
|
46:
|
case
2: ptr = new Cat;
|
47:
|
break;
|
48:
|
default:
ptr = new Mammal;
|
49:
|
break;
|
50:
|
}
|
51:
|
if
(fQuit)
|
52:
|
break;
|
53:
|
PtrFunction(ptr);
|
54:
|
RefFunction(*ptr);
|
55:
|
ValueFunction(*ptr);
|
return
0;
}
59:
void
ValueFunction (Mammal MammalValue)
{
MammalValue.Speak();
}
64:
void
PtrFunction (Mammal * pMammal)
{
pMammal->Speak();
}
69:
void
RefFunction (Mammal & rMammal)
{
rMammal.Speak();
}
Output: (1)dog (2)cat
(0)Quit: 1
Woof
Woof
Mammal Speak!
(1)dog (2)cat (0)Quit: 2 Meow!
Meow!
Mammal Speak!
(1)dog (2)cat (0)Quit:
0
Analysis: On lines 6-26, stripped-down versions of the Mammal, Dog, and Cat classes are declared. Three functions are declared--PtrFunction(), RefFunction(), and ValueFunction(). They take a pointer to a Mammal, a Mammal reference, and a Mammal object, respectively. All three functions then do the same
thing--they call the Speak() method.
The user is prompted to choose a Dog or Cat, and based on the choice he makes, a pointer to the correct type is
created on lines 44-49.
In the
first line of the output, the user chooses Dog. The Dog object is created on the free store on line 44. The Dog is then passed as a pointer, as a reference, and by value to the three
functions.
The pointer and references all invoke the virtual functions, and the Dog->Speak() member function is invoked. This is shown on the first two lines of
output after the user's choice.
The
dereferenced pointer, however, is passed by value. The function expects a Mammal object, and so the compiler slices down the Dog object to just the Mammal part. At
that point, the Mammal Speak()
method is called, as reflected in the third line of
output after the user's choice.
Virtual Destructors
It is legal and common to pass a pointer to a derived object when a
pointer to a base object is expected. What happens when that pointer to a
derived subject is deleted? If the destructor is virtual, as it should be, the
right thing happens--the derived class's destructor is called. Because the
derived class's destructor will automatically invoke the base class's
destructor, the entire object will be properly destroyed.
The rule of thumb is this: If any of the functions in your class are
virtual, the destructor should be as well.
Virtual Copy Constructors
As previously stated, no
constructor can be virtual. Nonetheless, there are times when your program
desperately
needs to be able to pass in a pointer to a base object and have a copy of the
correct derived object that is created. A common solution to this problem is to
create a Clone() method
in the base class and to make that be virtual. The Clone() method creates a new object copy of the current
class,
and returns that object.
Because each derived class overrides the Clone() method, a copy of the derived class is created. Listing 12.11
illustrates how this is used.
Listing 12.11. Virtual copy constructor.
1: //Listing
12.11 Virtual copy constructor
2:
3: #include <iostream.h> 4:
class
Mammal
{
public:
Mammal():itsAge(1)
{ cout << "Mammal constructor...\n"; }
~Mammal()
{ cout << "Mammal destructor...\n"; }
Mammal
(const Mammal & rhs);
virtual
void Speak() const { cout << "Mammal speak!\n"; }
virtual
Mammal* Clone() { return new Mammal(*this); }
int
GetAge()const { return itsAge; }
protected:
int
itsAge;
};
17:
18: Mammal::Mammal
(const Mammal & rhs):itsAge(rhs.GetAge())
cout
<< "Mammal Copy Constructor...\n";
}
22:
class
Dog : public Mammal
{
public:
Dog()
{ cout << "Dog constructor...\n"; }
~Dog()
{ cout << "Dog destructor...\n"; }
Dog
(const Dog & rhs);
void
Speak()const { cout << "Woof!\n"; }
virtual
Mammal* Clone() { return new Dog(*this); }
};
32:
Dog::Dog(const
Dog & rhs):
Mammal(rhs)
{
cout
<< "Dog copy constructor...\n";
}
38:
class
Cat : public Mammal
{
public:
Cat()
{ cout << "Cat constructor...\n"; }
~Cat()
{ cout << "Cat destructor...\n"; }
Cat
(const Cat &);
void
Speak()const { cout << "Meow!\n"; }
virtual
Mammal* Clone() { return new Cat(*this); }
};
48:
Cat::Cat(const
Cat & rhs):
Mammal(rhs)
{
cout
<< "Cat copy constructor...\n";
}
54:
enum
ANIMALS { MAMMAL, DOG, CAT};
const
int NumAnimalTypes = 3;
int
main()
{
Mammal
*theArray[NumAnimalTypes];
Mammal*
ptr;
int
choice, i;
for
( i = 0; i<NumAnimalTypes; i++)
{
64: cout
<< "(1)dog (2)cat (3)Mammal: ";
cin
>> choice;
|
|
66:
|
switch
(choice)
|
67:
|
{
|
68:
|
case
DOG: ptr = new Dog;
|
69:
|
break;
|
70:
|
case
CAT: ptr = new Cat;
|
71:
|
break;
|
72:
|
default: ptr = new Mammal;
|
73:
|
break;
|
74:
|
}
|
75:
|
theArray[i]
= ptr;
|
}
Mammal
*OtherArray[NumAnimalTypes];
for
(i=0;i<NumAnimalTypes;i++)
{
80:
|
theArray[i]->Speak();
|
81:
|
OtherArray[i] = theArray[i]->Clone();
|
}
for
(i=0;i<NumAnimalTypes;i++)
84:
|
OtherArray[i]->Speak();
|
25:
|
return
0;
|
86: }
|
(1)dog
(2)cat (3)Mammal: 1
Mammal
constructor...
Dog
constructor...
(1)dog
(2)cat (3)Mammal: 2
Mammal
constructor...
Cat
constructor...
(1)dog
(2)cat (3)Mammal: 3
Mammal
constructor...
Woof!
Mammal
Copy Constructor...
Dog
copy constructor...
Meow!
Mammal
Copy Constructor...
Cat
copy constructor...
Mammal
speak!
Mammal
Copy Constructor...
Woof!
Meow!
Mammal
speak!
Analysis: Listing 12.11 is very similar to the previous two listings,
except that a new virtual method has been added to the
Mammal class: Clone(). This method returns a pointer to a new Mammal
object by calling the copy
constructor, passing in itself (*this) as a const
reference.
Dog
and Cat both
override the Clone() method,
initializing their data and passing in copies of themselves to their own copy constructors. Because Clone() is virtual, this will effectively create a
virtual
copy constructor, as shown on line 81.
The user is prompted to choose dogs, cats, or mammals, and these are
created on lines 62-74. A pointer to each choice is stored in an array on line
75.
As the program iterates over the array, each object has its Speak() and its Clone() methods
called, in turn, on lines 80 and 81. The result of the Clone() call is a pointer to a copy of the object,
which is
then stored in a second array on line 81.
On line 1 of the output, the user is prompted and responds with 1, choosing to create a dog. The Mammal and Dog constructors
are invoked. This is repeated for Cat and for Mammal on lines
4-8 of
the
constructor.
Line 9 of the constructor represents the call to Speak() on the first object, the Dog. The virtual Speak() method is
called, and the correct version of Speak() is invoked. The Clone() function
is then called, and as this is also virtual, Dog's Clone() method
is invoked, causing the Mammal
constructor and the Dog copy
constructor to be called.
The same is repeated for Cat on lines
12-14, and then for Mammal on lines
15 and 16. Finally, the new array is iterated, and each of the new objects has Speak() invoked.
The Cost of Virtual Methods
Because objects with virtual methods must maintain a v-table, there is
some overhead in having virtual methods. If you have a very small class from
which you do not expect to derive other classes, there may be no reason to have
any virtual methods at all.
Once you
declare any methods virtual, you've paid most of the price of the v-table
(although each entry does add a small memory overhead). At that point, you'll
want the destructor to be virtual, and the assumption will be that all other
methods probably will be virtual as well. Take a long hard look at any
non-virtual methods, and be certain you understand why they are not virtual.
DO use virtual methods when you expect to derive from a class. DO use a virtual destructor if any methods are virtual. DON'T mark the constructor as virtual.
Summary
Today you
learned how derived classes inherit from base classes. This chapter discussed
public inheritance and virtual functions. Classes inherit all the public and
protected data and functions from their base classes.
Protected access is public to
derived classes and private to all other objects. Even derived classes cannot
access private data or functions in their base classes.
Constructors can be initialized before the body of the constructor. It
is at this time that base constructors are invoked and parameters can be passed
to the base class.
Functions in the base class can be overridden in
the derived class. If the base class functions are virtual, and if the object
is accessed by pointer or reference, the derived class's functions will be
invoked, based on the run-time type of the object pointed to.
Methods in the base class can be invoked by explicitly naming the
function with the prefix of the base class name and two colons. For example, if
Dog inherits from Mammal, Mammal's walk() method can be called with Mammal::walk().
In classes with virtual methods, the destructor should almost always be
made virtual. A virtual destructor ensures that the derived part of the object
will be freed when delete is
called on the
pointer. Constructors cannot be virtual. Virtual copy constructors can
be effectively created by making a virtual member function that calls the copy
constructor.
Q&A
Are inherited members and functions passed along to subsequent
generations? If Dog derives from Mammal, and Mammal derives from Animal, does
Dog inherit Animal's functions and data?
Yes. As derivation continues,
derived classes inherit the sum of all the functions and data in all their base
classes.
If, in the example above, Mammal overrides a function in Animal, which
does Dog get, the original or the overridden function?
If Dog inherits from Mammal, it gets
the function in the state Mammal has it:
the overridden function.
Can a derived class make a public base function private?
Yes, and it remains private for
all subsequent derivation.
Why not make all class functions virtual?
There is overhead with the first
virtual function in the creation of a v-table. After that, the overhead is
trivial. Many C++ programmers feel that if one function is virtual, all others
should be. Other programmers disagree, feeling that there should always be a
reason for what you do.
If a
function (SomeFunc()) is virtual in a base class and is also overloaded, so as
to
take either an integer or two
integers, and the derived class overrides the form taking one integer, what is
called when a pointer to a derived object calls the two-integer form?
A. The overriding of the one-int form
hides the entire base class function, and thus you will get a compile error complaining that that function requires only
one int.
Workshop
The Workshop provides quiz questions to help you solidify your
understanding of the material that was covered, and exercises to provide you
with experience in using what you've learned. Try to answer the quiz and
exercise questions before checking the answers in Appendix D, and make sure you
understand the answers before continuing to the next chapter.
Quiz
What is a v-table?
What is a virtual destructor?
How do you show the declaration of a virtual
constructor?
How can you create a virtual copy constructor?
How do you invoke a base member
function from a derived class in which you've overridden that function?
How do you invoke a base member
function from a derived class in which you have not overridden that function?
If a base class declares a
function to be virtual, and a derived class does not use the term virtual when
overriding that class, is it still virtual when inherited by a third-generation
class?
What is the protected keyword
used for?
Exercises
Show the declaration of a virtual function that
takes an integer parameter and returns void.
Show the declaration of a class Square, which derives from Rectangle, which
in turn derives from Shape.
If, in Exercise 2, Shape takes no parameters, Rectangle takes
two (length and width), but Square takes
only one (length), show
the constructor initialization for
Square.
Write a virtual copy constructor for the class Square (in
Exercise 3).
void SomeFunction (Shape); Shape * pRect = new Rectangle;
SomeFunction(*pRect);
6. BUG
BUSTERS: What is wrong with this code snippet?
class Shape()
{
public:
Shape();
virtual
~Shape();
virtual
Shape(const Shape&);
};
0 comments:
Post a Comment