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 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.
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.
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.
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.
1: //Listing 12.1 Simple inheritance
2:
3: #include <iostream.h>
4: enum BREED { YORKIE, CAIRN, DANDIE,
SHETLAND, DOBERMAN, LAB };
5:
6: class Mammal
7: {
8: public:
9: // constructors
10: Mammal();
11: ~Mammal();
12:
13: //accessors
14: int GetAge()const;
15: void SetAge(int);
16: int GetWeight() const;
17: void SetWeight();
18:
19: //Other methods
20: void Speak();
21: void Sleep();
22:
23:
24: protected:
25: int itsAge;
26: int itsWeight;
27: };
28:
29: class Dog : public Mammal
30: {
31: public:
32:
33: // Constructors
34: Dog();
35: ~Dog();
36:
37: // Accessors
38: BREED GetBreed() const;
39: void SetBreed(BREED);
40:
41: // Other methods
42: // WagTail();
43: // BegForFood();
44:
45: protected:
46: BREED itsBreed;
47: };
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.
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.
View Code
1: //Listing 12.2 Using a derived object
2:
3: #include <iostream.h>
4: enum BREED { YORKIE, CAIRN, DANDIE,
SHETLAND, DOBERMAN, LAB };
5:
6: class Mammal
7: {
8: public:
9: // constructors
10: Mammal():itsAge(2), itsWeight(5){}
11: ~Mammal(){}
12:
13: //accessors
14: int GetAge()const { return itsAge; }
15: void SetAge(int age) { itsAge = age; }
16: int GetWeight() const { return itsWeight;
}
17: void SetWeight(int weight) { itsWeight =
weight; }
18:
19: //Other methods
20: void Speak()const { cout <<
"Mammal sound!\n"; }
21: void Sleep()const { cout <<
"shhh. I'm sleeping.\n"; }
22:
23:
24: protected:
25: int itsAge;
26: int itsWeight;
27: };
28:
29: class Dog : public Mammal
30: {
31: public:
32:
33: // Constructors
34: Dog():itsBreed(YORKIE){}
35: ~Dog(){}
36:
37: // Accessors
38: BREED GetBreed() const { return
itsBreed; }
39: void SetBreed(BREED breed) { itsBreed =
breed; }
40:
41: // Other methods
42: void WagTail() { cout <<
"Tail wagging...\n"; }
43: void BegForFood() { cout <<
"Begging for food...\n"; }
44:
45: private:
46: BREED itsBreed;
47: };
48:
49: int main()
50: {
51: Dog fido;
52: fido.Speak();
53: fido.WagTail();
54: cout << "Fido is "
<< fido.GetAge() << " years old\n";
55: return 0;
56: }
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().
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.
#include <iostream.h>
enum BREED { YORKIE, CAIRN, DANDIE,
SHETLAND, DOBERMAN, LAB };
class Mammal
{
public:
// constructors
Mammal();
~Mammal();
//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;
};
class Dog : public Mammal
{
public:
// Constructors
Dog();
~Dog();
// 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;
};
Mammal::Mammal():
itsAge(1),
itsWeight(5)
{
cout << "Mammal
constructor...\n";
}
Mammal::~Mammal()
{
cout << "Mammal
destructor...\n";
}
Dog::Dog():
itsBreed(YORKIE)
{
cout << "Dog
constructor...\n";
}
Dog::~Dog()
{
cout << "Dog
destructor...\n";
}
int main()
{
Dog fido;
fido.Speak();
fido.WagTail();
cout << "Fido is "
<< fido.GetAge() << " years old\n";
return 0;
}
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.
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.
View Code
1: //Listing 12.4 Overloading constructors in
derived classes
2:
3: #include <iostream.h>
4: enum BREED { YORKIE, CAIRN, DANDIE,
SHETLAND, DOBERMAN, LAB };
5:
6: class Mammal
7: {
8: public:
9: // constructors
10: Mammal();
11: Mammal(int age);
12: ~Mammal();
13:
14: //accessors
15: int GetAge() const { return itsAge; }
16: void SetAge(int age) { itsAge = age; }
17: int GetWeight() const { return
itsWeight; }
18: void SetWeight(int weight) { itsWeight =
weight; }
19:
20: //Other methods
21: void Speak() const { cout <<
"Mammal sound!\n"; }
22: void Sleep() const { cout <<
"shhh. I'm sleeping.\n"; }
23:
24:
25: protected:
26: int itsAge;
27: int itsWeight;
28: };
29:
30: class Dog : public Mammal
31: {
32: public:
33:
34: // Constructors
35: Dog();
36: Dog(int age);
37: Dog(int age, int weight);
38: Dog(int age, BREED breed);
39: Dog(int age, int weight, BREED breed);
40: ~Dog();
41:
42: // Accessors
43: BREED GetBreed() const { return
itsBreed; }
44: void SetBreed(BREED breed) { itsBreed =
breed; }
45:
46: // Other methods
47: void WagTail() { cout << "Tail
wagging...\n"; }
48: void BegForFood() { cout <<
"Begging for food...\n"; }
49:
50: private:
51: BREED itsBreed;
52: };
53:
54: Mammal::Mammal():
55: itsAge(1),
56: itsWeight(5)
57: {
58: cout << "Mammal
constructor...\n";
59: }
60:
61: Mammal::Mammal(int age):
62: itsAge(age),
63: itsWeight(5)
64: {
65: cout << "Mammal(int)
constructor...\n";
66: }
67:
68: Mammal::~Mammal()
69: {
70: cout << "Mammal destructor...\n";
71: }
72:
73: Dog::Dog():
74: Mammal(),
75: itsBreed(YORKIE)
76: {
77: cout << "Dog
constructor...\n";
78: }
79:
80: Dog::Dog(int age):
81: Mammal(age),
82: itsBreed(YORKIE)
83: {
84: cout << "Dog(int)
constructor...\n";
85: }
86:
87: Dog::Dog(int age, int weight):
88: Mammal(age),
89: itsBreed(YORKIE)
90: {
91: itsWeight = weight;
92: cout << "Dog(int, int)
constructor...\n";
93: }
94:
95: Dog::Dog(int age, int weight, BREED breed):
96: Mammal(age),
97: itsBreed(breed)
98: {
99: itsWeight = weight;
100: cout << "Dog(int, int, BREED)
constructor...\n";
101: }
102:
103: Dog::Dog(int age, BREED breed):
104: Mammal(age),
105: itsBreed(breed)
106: {
107: cout << "Dog(int, BREED)
constructor...\n";
108: }
109:
110: Dog::~Dog()
111: {
112: cout << "Dog
destructor...\n";
113: }
114: int main()
115: {
116: Dog fido;
117: Dog rover(5);
118: Dog buster(6,8);
119: Dog
yorkie (3,YORKIE);
120: Dog dobbie (4,20,DOBERMAN);
121: fido.Speak();
122: rover.WagTail();
123: cout << "Yorkie is "
<< yorkie.GetAge() << " years old\n";
124: cout << "Dobbie weighs ";
125: cout << dobbie.GetWeight() <<
" pounds\n";
126: return 0;
127: }
--------------------------------------------------------------------------------
NOTE: The
output has been numbered here so that each line can be referred to in the
analysis.
--------------------------------------------------------------------------------
Output:
1: Mammal constructor...
2: Dog constructor...
3: Mammal(int) constructor...
4: Dog(int) constructor...
5: Mammal(int) constructor...
6: Dog(int, int) constructor...
7: Mammal(int) constructor...
8: Dog(int, BREED) constructor....
9: Mammal(int) constructor...
10:
Dog(int, int, BREED) constructor...
11: Mammal
sound!
12: Tail
wagging...
13: Yorkie
is 3 years old.
14: Dobbie
weighs 20 pounds.
15: Dog
destructor. . .
16: Mammal
destructor...
17: Dog
destructor...
18: Mammal
destructor...
19: Dog
destructor...
20: Mammal
destructor...
21: Dog
destructor...
22: Mammal
destructor...
23: Dog
destructor...
24: 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.
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.
View Code
1: //Listing 12.5 Overriding a base class
method in a derived class
2:
3: #include <iostream.h>
4: enum BREED { YORKIE, CAIRN, DANDIE,
SHETLAND, DOBERMAN, LAB };
5:
6: class Mammal
7: {
8: public:
9: // constructors
10: Mammal() { cout << "Mammal
constructor...\n"; }
11: ~Mammal() { cout << "Mammal destructor...\n"; }
12:
13: //Other methods
14: void Speak()const { cout <<
"Mammal sound!\n"; }
15: void Sleep()const { cout <<
"shhh. I'm sleeping.\n"; }
16:
17:
18: protected:
19: int itsAge;
20: int itsWeight;
21: };
22:
23: class Dog : public Mammal
24: {
25: public:
26:
27: // Constructors
28: Dog(){ cout << "Dog
constructor...\n"; }
29: ~Dog(){ cout << "Dog
destructor...\n"; }
30:
31: // Other methods
32: void WagTail() { cout <<
"Tail wagging...\n"; }
33: void BegForFood() { cout <<
"Begging for food...\n"; }
34: void Speak()const { cout <<
"Woof!\n"; }
35:
36: private:
37: BREED itsBreed;
38: };
39:
40: int main()
41: {
42: Mammal bigAnimal;
43: Dog fido;
44: bigAnimal.Speak();
45: fido.Speak();
46: return 0;
47: }
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.
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.
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.
View Code
1: //Listing 12.6 Hiding methods
2:
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: void Move() const { cout <<
"Mammal move one step\n"; }
9: void Move(int distance) const
10: {
11: cout << "Mammal move ";
12: cout << distance <<"
_steps.\n";
13: }
14: protected:
15: int itsAge;
16: int itsWeight;
17: };
18:
19: class Dog : public Mammal
20: {
21: public:
22: //
You may receive a warning that you are hiding a function!
23: void Move() const { cout <<
"Dog move 5 steps.\n"; }
24: };
25:
26: int main()
27: {
28: Mammal bigAnimal;
29: Dog fido;
30: bigAnimal.Move();
31: bigAnimal.Move(2);
32: fido.Move();
33: // fido.Move(10);
34: return 0;
35: }
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.
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.
Calling the
Base Method
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
28: fido.Mammal::Move(10);
This calls
the Mammal method explicitly. Listing 12.7 fully illustrates this idea.
View Code
1: //Listing 12.7 Calling base method from
overridden method.
2:
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: void Move() const { cout <<
"Mammal move one step\n"; }
9: void Move(int distance) const
10: {
11: cout << "Mammal move
" << distance;
12: cout << "
steps.\n";
13: }
14:
15: protected:
16: int itsAge;
17: int itsWeight;
18: };
19:
20: class Dog : public Mammal
21: {
22: public:
23: void Move()const;
24:
25: };
26:
27: void Dog::Move() const
28: {
29: cout << "In dog
move...\n";
30: Mammal::Move(3);
31: }
32:
33: int main()
34: {
35: Mammal bigAnimal;
36: Dog fido;
37: bigAnimal.Move(2);
38: fido.Mammal::Move(6);
39: return 0;
40: }
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.
--------------------------------------------------------------------------------
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.
View Code
1: //Listing 12.8 Using virtual methods
2:
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: Mammal():itsAge(1) { cout <<
"Mammal constructor...\n"; }
9: ~Mammal() { cout << "Mammal
destructor...\n"; }
10: void Move() const { cout <<
"Mammal move one step\n"; }
11: virtual void Speak() const { cout
<< "Mammal speak!\n"; }
12: protected:
13: int itsAge;
14:
15: };
16:
17: class Dog : public Mammal
18: {
19: public:
20: Dog() { cout << "Dog
Constructor...\n"; }
21: ~Dog() { cout << "Dog
destructor...\n"; }
22: void WagTail() { cout <<
"Wagging Tail...\n"; }
23: void Speak()const { cout <<
"Woof!\n"; }
24: void Move()const { cout << "Dog
moves 5 steps...\n"; }
25: };
26:
27: int main()
28: {
29:
30: Mammal *pDog = new Dog;
31: pDog->Move();
32: pDog->Speak();
33:
34: return 0;
35: }
Output:
Mammal constructor...
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.
View Code
1: //Listing 12.9 Multiple virtual functions
called in turn
2:
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: Mammal():itsAge(1) { }
9: ~Mammal() { }
10: virtual void Speak() const { cout << "Mammal speak!\n"; }
11: protected:
12: int itsAge;
13: };
14:
15: class Dog : public Mammal
16: {
17: public:
18: void Speak()const { cout <<
"Woof!\n"; }
19: };
20:
21:
22: class Cat : public Mammal
23: {
24: public:
25: void Speak()const { cout <<
"Meow!\n"; }
26: };
27:
28:
29: class Horse : public Mammal
30: {
31: public:
32: void Speak()const { cout <<
"Winnie!\n"; }
33: };
34:
35: class Pig : public Mammal
36: {
37: public:
38: void Speak()const { cout <<
"Oink!\n"; }
39: };
40:
41: int main()
42: {
43: Mammal* theArray[5];
44: Mammal* ptr;
45: int choice, i;
46: for ( i = 0; i<5; i++)
47: {
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;
64: }
65: for (i=0;i<5;i++)
66: theArray[i]->Speak();
67: return 0;
68: }
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.
--------------------------------------------------------------------------------
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 wrong with this description.
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.
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."
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.
View Code
1: //Listing 12.10 Data slicing with passing
by value
2:
3: #include <iostream.h>
4:
5: enum BOOL { FALSE, TRUE };
6: class Mammal
7: {
8: public:
9: Mammal():itsAge(1) { }
10: ~Mammal() { }
11: virtual void Speak() const { cout
<< "Mammal speak!\n"; }
12: protected:
13: int itsAge;
14: };
15:
16: class Dog : public Mammal
17: {
18: public:
19: void Speak()const { cout <<
"Woof!\n"; }
20: };
21:
22: class Cat : public Mammal
23: {
24: public:
25: void Speak()const { cout <<
"Meow!\n"; }
26: };
27:
28 void ValueFunction (Mammal);
29: void PtrFunction (Mammal*);
30: void RefFunction (Mammal&);
31: int main()
32: {
33: Mammal* ptr=0;
34: int choice;
35: while (1)
36: {
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);
56: }
57: return 0;
58: }
59:
60: void ValueFunction (Mammal MammalValue)
61: {
62: MammalValue.Speak();
63: }
64:
65: void PtrFunction (Mammal * pMammal)
66: {
67: pMammal->Speak();
68: }
69:
70: void RefFunction (Mammal & rMammal)
71: {
72: rMammal.Speak();
73: }
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.
This
experiment is then repeated for the Cat object, with similar results.
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.
View Code
1: //Listing 12.11 Virtual copy constructor
2:
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: Mammal():itsAge(1) { cout <<
"Mammal constructor...\n"; }
9: ~Mammal() { cout << "Mammal
destructor...\n"; }
10: Mammal (const Mammal & rhs);
11: virtual void Speak() const { cout
<< "Mammal speak!\n"; }
12: virtual Mammal* Clone() { return new
Mammal(*this); }
13: int GetAge()const { return itsAge; }
14: protected:
15: int itsAge;
16: };
17:
18: Mammal::Mammal (const Mammal &
rhs):itsAge(rhs.GetAge())
19: {
20: cout << "Mammal Copy
Constructor...\n";
21: }
22:
23: class Dog : public Mammal
24: {
25: public:
26: Dog() { cout << "Dog
constructor...\n"; }
27: ~Dog() { cout << "Dog
destructor...\n"; }
28: Dog (const Dog & rhs);
29: void Speak()const { cout <<
"Woof!\n"; }
30: virtual Mammal* Clone() { return new
Dog(*this); }
31: };
32:
33: Dog::Dog(const Dog & rhs):
34: Mammal(rhs)
35: {
36: cout << "Dog copy
constructor...\n";
37: }
38:
39: class Cat : public Mammal
40: {
41: public:
42: Cat() { cout << "Cat
constructor...\n"; }
43: ~Cat() { cout << "Cat
destructor...\n"; }
44: Cat (const Cat &);
45: void Speak()const { cout <<
"Meow!\n"; }
46: virtual Mammal* Clone() { return new
Cat(*this); }
47: };
48:
49: Cat::Cat(const Cat & rhs):
50: Mammal(rhs)
51: {
52: cout << "Cat copy
constructor...\n";
53: }
54:
55: enum ANIMALS { MAMMAL, DOG, CAT};
56: const int NumAnimalTypes = 3;
57: int main()
58: {
59: Mammal *theArray[NumAnimalTypes];
60: Mammal* ptr;
61: int choice, i;
62: for ( i = 0; i<NumAnimalTypes; i++)
63: {
64: cout << "(1)dog (2)cat
(3)Mammal: ";
65: 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;
76: }
77: Mammal *OtherArray[NumAnimalTypes];
78: for (i=0;i<NumAnimalTypes;i++)
79: {
80: theArray[i]->Speak();
81: OtherArray[i] =
theArray[i]->Clone();
82: }
83: for (i=0;i<NumAnimalTypes;i++)
84: OtherArray[i]->Speak();
25: return 0;
86: }
1: (1)dog (2)cat (3)Mammal: 1
2: Mammal constructor...
3: Dog constructor...
4: (1)dog (2)cat (3)Mammal: 2
5: Mammal constructor...
6: Cat constructor...
7: (1)dog (2)cat (3)Mammal: 3
8: Mammal constructor...
9: Woof!
10:
Mammal Copy Constructor...
11: Dog
copy constructor...
12: Meow!
13: Mammal
Copy Constructor...
14: Cat
copy constructor...
15:
Mammal speak!
16:
Mammal Copy Constructor...
17: Woof!
18: Meow!
19: 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.
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.
--------------------------------------------------------------------------------
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. 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?
A. Yes. As
derivation continues, derived classes inherit the sum of all the functions and
data in all their base classes.
Q. If, in
the example above, Mammal overrides a function in Animal, which does Dog get,
the original or the overridden function?
A. If Dog
inherits from Mammal, it gets the function in the state Mammal has it: the
overridden function.
Q. Can a
derived class make a public base function private?
A. Yes, and
it remains private for all subsequent derivation.
Q. Why not
make all class functions virtual?
A. 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.
Q. 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.
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.
1. What is
a v-table?
2. What is
a virtual destructor?
3. How do
you show the declaration of a virtual constructor?
4. How can
you create a virtual copy constructor?
5. How do
you invoke a base member function from a derived class in which you've
overridden that function?
6. How do
you invoke a base member function from a derived class in which you have not
overridden that function?
7. 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?
8. What is
the protected keyword used for?
Exercises
1. Show the
declaration of a virtual function that takes an integer parameter and returns
void.
2. Show the
declaration of a class Square, which derives from Rectangle, which in turn
derives from Shape.
3. 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.
4. Write a
virtual copy constructor for the class Square (in Exercise 3).
5. BUG
BUSTERS: What is wrong with this code snippet?
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&);
};