Yesterday, you learned about a number of object-oriented relationships, including specialization/generalization. C++ implements this relationship through inheritance.
Today, you will learn
• The nature of what inheritance is
• How to use inheritance 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? I see four legs in service to a mouth. 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 is of interest 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 specialization/generalization hierarchy establishes an is-a relationship. A Homo sapiens (human) is a kind of primate. This relationship can be seen everywhere: A station wagon is a kind of car, which is a kind of vehicle. A sundae is a kind of dessert, which is a kind of food.
When something is said to be a kind of something else, it is implied 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, you know that it moves and that it breathes air. All mammals, by definition, move and breathe air. The concept of a dog adds the idea of barking, wagging its tail, eating my revisions to this chapter just when I was finally done, barking when I’m trying to sleep… Sorry. Where was I? Oh yes:
You can divide dogs into working dogs, sporting dogs, and terriers, and you can divide sporting dogs into retrievers, spaniels, and so forth. Finally, each of these can be specialized further; for example, retrievers can be subdivided into Labradors and Goldens.
A Golden is a kind of retriever, which is a sporting dog, which is a dog, and thus a kind of mammal, which is a kind of animal, and, therefore, a kind of living thing. This hierarchy is represented in Figure 12.1.
Figure 12.1. Hierarchy of animals.
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
.
A class that 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 adds certain methods or data to the Mammal
class.
Typically, a base class has more than one derived class. Because 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 focuses 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 today’s lesson to enable the animals to act more realistically.
You should find that the examples using animals are easy to follow. You also find it easy to apply the concepts to other areas. For example, if you were building an ATM bank machine program, then you might have a checking account, which is a type of bank account, which is a type of account. This parallels the idea of a dog being an mammal, which in turn is an animal.
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 format of this is:
class derivedClass : accessType baseClass
As an example, if you create a new class called Dog
that inherits from the existing class Mammal
:
class Dog : public Mammal
The type of derivation (accessType
) is discussed later in today’s lesson. For now, always use public
. The class from which you derive must have been declared earlier, or you receive 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
This program has no output because it is only a set of class declarations without their implementations. Nonetheless, there is much to see here.
On lines 7–28, 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 a carefully limited 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 without adding unnecessary complication.
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. Certainly all animals have an age and weight, for example, so if Mammal
is derived from Animal
, you would expect to inherit those attributes. As it is, the attributes appear in the Mammal
class.
In the future, if another animal sharing some of these features were added (for instance, Insect
), the relevant attributes could be hoisted to a newly created Animal
class that would become the base class of Mammal
and Insect
. This is how class hierarchies evolve over time.
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 30. You know Dog
inherits from Mammal
because of the colon following the class name (Dog
), which is then followed by the base class name (Mammal
).
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 Mammal
’s methods except the copy operator and the constructors and destructor.
You might have noticed that a new access keyword, protected
, has been introduced on lines 25 and 46 of Listing 12.1. Previously, class data had been declared private. However, private members are not available outside of the existing class. This privacy even applies to prevent access from 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.
Note
There is an argument to be made that you ought to make all member data private and never protected. Stroustrup (the creator of C++) makes this argument in The Design and Evolution of C++, ISBN 0-201-543330-3, Addison Wesley, 1994. Protected methods, however, are not generally regarded as problematic, and can be very useful.
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.
In total, three access specifiers exist: 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 of itsAge
and itsWeight
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 16, “Advanced Inheritance.”
Listing 12.2 demonstrates how to create objects of type Dog
and then how to access the data and methods of that type.
Listing 12.2. Using a Derived Object
Mammal sound!
Tail wagging...
Fido is 2 years old
On lines 8–28, the Mammal
class is declared (all its functions are inline to save space here). On lines 30–48, 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. As stated before, the age and weight come from the base class, Mammal
.
On line 52, 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 he also knows how to Speak()
and Sleep()
. On lines 53 and 54, Fido
calls two of these methods from the Mammal
base class. On line 55, the GetAge()
accessor method from the base class is also called successfully.
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 Fido
is given 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 is called and then the destructor for the Mammal
part of Fido
is called. 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 the calling of the constructors and destructors.
Listing 12.3. Constructors and Destructors Called
Mammal constructor...
Dog constructor...
Mammal sound!
Tail wagging...
Fido is 3 years old
Dog destructor...
Mammal destructor...
Listing 12.3 is like Listing 12.2, except that on lines 48 to 69 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. You see that this is confirmed in the output from the listing.
It is possible that you will want to initialize values in a base constructor. For example, you might want to overload the constructor of Mammal
to take a specific age, and 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 Dog
s want to initialize weight but Mammal
s 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
The output has been numbered here so that each line can be referred to in the analysis.
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...
In Listing 12.4, Mammal
’s constructor has been overloaded on line 12 to take an integer, the Mammal
’s age. The implementation on lines 62–67 initializes itsAge
with the value passed into the constructor and initializes itsWeight
with the value 5
.
Dog
has overloaded five constructors on lines 36–40. The first is the default constructor. On line 37, the second constructor 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 the breed, and the fifth takes the age, the weight, and the breed.
On line 74 is the code for Dog
’s default constructor. You can see that this has something new. When this constructor is called, it in turn calls Mammal
’s default constructor as you can see on line 75. 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 81–86. In its initialization phase (lines 82 and 83), Dog
initializes its base class, passing in the parameter, and then it initializes its breed.
Another Dog
constructor is on lines 88–94. This constructor takes two parameters. Once again, it initializes its base class by calling the appropriate constructor on line 89, 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 be certain 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 data members and functions in class Mammal
, as well as to any of its own data members and functions, such as WagTail()
, that the declaration of the Dog
class might add. A derived class can also override a base class function. Overriding a function means changing the implementation of a base class function in a derived class.
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 function. When you make an object of the derived class, the correct function is called.
When you override a function, its signature must agree with the signature of the function in the base class. The signature is the function prototype other than the return type; that is, the name of the function, the parameter list, and the keyword const
, if used. The return types might differ.
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 Method in a Derived Class
1: //Listing 12.5 Overriding a base class method in a derived class
2: #include <iostream>
3: using std::cout;
4:
5: enum BREED { GOLDEN, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB };
6:
7: class Mammal
8: {
9: public:
10: // constructors
11: Mammal() { cout << "Mammal constructor...
"; }
12: ~Mammal() { cout << "Mammal destructor...
"; }
13:
14: //Other methods
15: void Speak()const { cout << "Mammal sound!
"; }
16: void Sleep()const { cout << "shhh. I’m sleeping.
"; }
17:
18: protected:
19: int itsAge;
20: int itsWeight;
21: };
22:
23: class Dog : public Mammal
24: {
25: public:
26 // Constructors
27: Dog(){ cout << "Dog constructor...
"; }
28: ~Dog(){ cout << "Dog destructor...
"; }
29:
30: // Other methods
31: void WagTail() const { cout << "Tail wagging...
"; }
32: void BegForFood() const { cout << "Begging for food...
"; }
33: void Speak() const { cout << "Woof!
"; }
34:
35: private:
36: BREED itsBreed;
37: };
38:
39: int main()
40: {
41: Mammal bigAnimal;
42: Dog Fido;
43: bigAnimal.Speak();
44: Fido.Speak();
45: return 0;
46: }
Mammal constructor...
Mammal constructor...
Dog constructor...
Mammal sound!
Woof!
Dog destructor...
Mammal destructor...
Mammal destructor...
Looking at the Mammal
class, you can see a method called Speak()
defined on line 15. The Dog
class declared on lines 23–37 inherits from Mammal
(line 23), and, therefore, has access to this Speak()
method. The Dog
class, however, overrides this method on line 33, causing Dog
objects to say Woof!
when the Speak()
method is called.
In the main()
function, a Mammal
object, bigAnimal
, is created on line 41, causing the first line of output when the Mammal
constructor is called. On line 42, 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 43, the Mammal
object calls its Speak()
method; then on line 44, the Dog
object calls its Speak()
method. The output reflects that the correct methods were called. The bigAnimal
made a mammal sound and Fido
woofed. Finally, the two objects go out of scope and the destructors are called.
In the previous listing, the Dog
class’s Speak()
method hides the base class’s method. This is 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 hides all 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: #include <iostream>
3: using std::cout;
4:
5: class Mammal
6: {
7: public:
8: void Move() const { cout << "Mammal move one step.
"; }
9: void Move(int distance) const
10: {
11: cout << "Mammal move ";
12: cout << distance <<" steps.
";
13: }
14: protected:
15: int itsAge;
16: int itsWeight;
17: };
18:
19: class Dog : public Mammal
20: {
21: public:
22: // You might receive a warning that you are hiding a function!
23: void Move() const { cout << "Dog move 5 steps.
"; }
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: }
Mammal move one step.
Mammal move 2 steps.
Dog move 5 steps.
All 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 23, Dog
overrides the version of Move()
with no parameters. These methods are invoked on lines 30–32, and the output reflects this as executed.
Line 33, however, is commented out because it causes a compile-time error. After you override one of the methods, you can no longer use any of the base methods of the same name. So, although 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 wants to use both. Otherwise, it hides the method that it doesn’t override. This is reminiscent of the rule that if you supply any constructor, the compiler no longer supplies a default constructor.
The rule is this: After you override any overloaded method, all the other overrides of that method are hidden. If you want them not to be hidden, you must override them all.
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 overrides it.
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:
baseClass::Method()
You can call the Move()
method of the Mammal
class as follows:
Mammal::Move().
You can use these qualified names just as you would any other method name. It would have been possible to rewrite line 33 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 a Base Method from a Overridden Method
1: //Listing 12.7 Calling a base method from a overridden method.
2: #include <iostream>
3: using namespace std;
4:
5: class Mammal
6: {
7: public:
8: void Move() const { cout << "Mammal move one step
"; }
9: void Move(int distance) const
10: {
11: cout << "Mammal move " << distance;
12: cout << " steps." << endl;
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: void Dog::Move() const
27: {
28: cout << "In dog move...
";
29: Mammal::Move(3);
30: }
31:
32: int main()
33: {
34: Mammal bigAnimal;
35: Dog Fido;
36: bigAnimal.Move(2);
37: Fido.Mammal::Move(6);
38: return 0;
39: }
Mammal move 2 steps.
Mammal move 6 steps.
On line 34, a Mammal
, bigAnimal
, is created, and on line 35, a Dog
, Fido
, is created. The method call on line 36 invokes the Move()
method of Mammal
, which takes an integer.
The programmer wanted to invoke Move(int)
on the Dog
object, but had a problem. Dog
overrides the Move()
method (with no parameters), but does not overload the method that takes an integer—it does not provide a version that takes an integer. This is solved by the explicit call to the base class Move(int)
method on line 37.
When calling overridden ancestor class functions using “::
”, keep in mind that if a new class is inserted in the inheritance hierarchy between the descendant and its ancestor, the descendant will be now making a call that skips past the intermediate class and, therefore, might miss invoking some key capability implemented by the intermediate ancestor.
This lesson 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 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 runtime 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 enable you to do that. To create a virtual function, you add the keyword virtual
in front of the function declaration. Listing 12.8 illustrates how this works, and what happens with nonvirtual methods.
Listing 12.8. Using Virtual Methods
1: //Listing 12.8 Using virtual methods
2: #include <iostream>
3: using std::cout;
4:
5: class Mammal
6: {
7: public:
8: Mammal():itsAge(1) { cout << "Mammal constructor...
"; }
9: virtual ~Mammal() { cout << "Mammal destructor...
"; }
10: void Move() const { cout << "Mammal move one step
"; }
11: virtual void Speak() const { cout << "Mammal speak!
"; }
12:
13: protected:
14: int itsAge;
15: };
16:
17: class Dog : public Mammal
18: {
19: public:
20: Dog() { cout << "Dog Constructor...
"; }
21: virtual ~Dog() { cout << "Dog destructor...
"; }
22: void WagTail() { cout << "Wagging Tail...
"; }
23: void Speak()const { cout << "Woof!
"; }
24: void Move()const { cout << "Dog moves 5 steps...
"; }
25: };
26:
27: int main()
28: {
29: Mammal *pDog = new Dog;
30: pDog->Move();
31: pDog->Speak();
32:
33: return 0;
34: }
Mammal constructor...
Dog Constructor...
Mammal move one step
Woof!
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 29, 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 on line 30 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 10, you can see that this is a standard, nonvirtual method, so the Mammal
’s version is called.
On line 31, the pointer then calls the Speak()
method. Because Speak()
is virtual (see line 11), 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 different 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)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!
Whinny!
Oink!
Mammal speak!
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
. All four are derived from Mammal
.
On line 10, Mammal
’s Speak()
function is declared to be virtual. On lines 19, 25, 32, and 38, the four derived classes override the implementation of Speak()
.
On lines 46–64, the program loops five times. Each time, the user is prompted to pick which object to create, and a new pointer to that object type is added to the array from within the switch
statement on lines 50–62.
At the time this program is compiled, it is impossible to know which object types 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 runtime binding, as opposed to static binding, or compile-time binding.
On lines 65 and 66, the program loops through the array again. This time, each object in the array has its Speak()
method called. Because Speak()
was virtual in the base class, the appropriate Speak()
methods are called for each type. You can see in the output that if you choose each different type, that the corresponding method is indeed called.
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.
Figure 12.2. The Dog
object after it is created.
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.
Although implementations vary, all compilers must accomplish the same thing.
Each object’s vptr points to the v-table that, in turn, has a pointer to each of the virtual functions. (Note: Pointers to functions are discussed in depth on Day 15, “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.
Figure 12.3. The v-table of a Mammal
.
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).
Figure 12.4. The v-table of a Dog
.
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 have seen methods accessed in a derived class from a base class using virtual functions. What if there is a method in the derived class that isn’t in the base class? Can you access it in the same way you have been using the base class to access the virtual methods? There shouldn’t be a name conflict because only the derived class has the method.
If the Dog
object had a method, WagTail()
, which is not in Mammal
, you could not use the pointer to Mammal
to access that method. 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.
You could cast the Mammal
to act as a Dog
; however, this is not safe if the Mammal
is not a Dog
. Although this would transform the Mammal
pointer into a Dog
pointer, a much better and safer way exists to call the WagTail()
method. Besides, C++ frowns on explicit casts because they are error-prone. This subject is addressed in depth when multiple inheritance is covered on Day 15, and again when templates are covered on Day 20, “Handling Errors and Exceptions.”
Note that the virtual function magic operates only on pointers and references. Passing an object by value does not enable the virtual functions to be invoked. Listing 12.10 illustrates this problem.
Listing 12.10. Data Slicing When Passing by Value
(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
On lines 4–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. As you can see on lines 60–73, all three functions do the same thing—they call the Speak()
method.
The user is prompted to choose a Dog
or a Cat
, and based on the choice that is made, a pointer to the correct type is created on lines 44 or 46.
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 to a function as a pointer on line 53, as a reference on line 54, and by value on line 55.
The pointer and reference calls 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 on line 55 to the function on lines 60–63. The function expects a Mammal
object, and so the compiler slices down the Dog
object to just the Mammal
part. When the Mammal Speak()
method is called on line 72, only Mammal
information is available. The Dog
pieces are gone. This is reflected in the third line of output after the user’s choice. This effect is called slicing because the Dog
portions (your derived class portions) of your object were sliced off when converting to just a Mammal
(the base class).
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 automatically invokes the base class’s destructor, the entire object is properly destroyed.
The rule of thumb is this: If any of the functions in your class are virtual, the destructor should be as well.
Note
You should have noticed that the listings in today’s lesson have been including virtual destructors. Now you know why! As a general practice, it is wise to always make destructors virtual.
Constructors cannot be virtual, and so, technically, no such thing exists as a virtual copy constructor. Nonetheless, at times, 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 the Clone()
method is used.
Listing 12.11. Virtual Copy Constructor
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!
Listing 12.11 is very similar to the previous two listings, except that on line 12 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 effectively creates a virtual copy constructor. You see this when line 81 executes.
Similar to the last listing, the user is prompted to choose dogs, cats, or mammals, and these are created on lines 68–73. A pointer to each choice is stored in an array on line 75.
As the program iterates over the array on lines 78 to 82, each object has its Speak()
and its Clone()
methods called, in turn. The result of the Clone()
call on line 81 is a pointer to a copy of the object, which is then stored in a second array.
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
on line 4 and for Mammal
on line 8 of the constructor.
Line 9 of the output 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 because 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 on lines 83 and 84 of the listing, and each of the new objects has Speak()
invoked, as can be seen by output lines 17 to 19.
Because objects with virtual methods must maintain a v-table, some overhead occurs in having virtual methods. If you have a very small class from which you do not expect to derive other classes, there might not be a reason to have any virtual methods at all.
After 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 want the destructor to be virtual, and the assumption is that all other methods probably are virtual as well. Take a long, hard look at any nonvirtual methods, and be certain you understand why they are not virtual.
Today, you learned how derived classes inherit from base classes. Today’s class 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 classes. Even derived classes cannot access private data or functions in their base classes.
Constructors can be initialized before the body of the constructor. At that time, the 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 runtime 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, but can only access those that are public or protected.
Q If, in the preceding example, 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 overridden function.
Q Can a derived class make a public base function private?
A Yes, the derived class can override the method and make it private. It then remains private for all subsequent derivation. However, this should be avoided when possible, because users of your class will expect it to contain the sum of the methods provided by its ancestors.
Q Why not make all class functions virtual?
A Overhead occurs 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 As you learned in today’s lesson, the overriding of the one-integer form hides the entire base class function, and thus you receive 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 be certain you understand the answers before continuing to tomorrow’s lesson.
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?
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&);
};
3.142.212.160