Inheritance becomes difficult to manage when using multiple types.
The mechanism of inheritance is very useful for making a new class out of an existing one. Quite often as you progress into Visual C++, you'll want to write a program that needs a class similar to one you have already written, but with a few changes.
Inheritance works very well for this. Listing 21.2 in the previous unit showed that when you try to mix up your classes, the system of inheritance gets rather messy. The problems included the following:
All in all, these problems probably confused you quite a bit!
In Visual C++, a new calling mechanism has been invented to ensure that the right function is called. This mechanism is what provides our third buzzwordpolymorphism. What you need is a system in which the called object remembers what it is, so
that when an overloaded function is called, the right version is called without all the casting.
There is another limitation, though. Consider a very simple function to print out your pets from the base class. It would be nice to write a function that prints out the type of food that the pet eats. You could do something like this:
void Pet::PrintType() { PrintHeadings(); select (type) // declared in Pet { case dog: // Print Pet::Food string case cat: // Print Pet::Food string } PrintFooter(); }
This is very unsatisfactory. The base class needs to know all about the type of class. If you change things around so that the derived classes do the printing, you get two problems: First, the calling program needs to call the correct version; and
second, each version needs to remember to call the PrintHeadings() and PrintFooter() code.
Visual C++ provides a mechanism to enable you to call functions that belong to derived classes from the base class. This is a clever thing to do because when a base class is written, it does not know about the derived classes. Visual C++ needs to wait
until the program is running to know the actual type of an object that is currently associated with a pointer. Until now, Visual C++ has had all the information it needs at the time of compilation. Until now, when the program used a base class pointer to
access a derived class, you told C++ which derived class to use with a cast. With the new mechanism, C++ hides away some information about the class so that when it is running, it knows what type it issimilar to the type information you stored in the
pet program.
A virtual function enables you to call a function depending on the type of class object.
To use this special mechanism, you simply use the keyword virtual in front of the function signature, as in this example:
class A { public: virtual void Print() { cout << "A printed" << endl; } }; A a; a.Print();
This code appears to work exactly like previous versions that you have written, and it prints out A printed.
You can now add a new class:
class B : public A { virtual void Print() { cout << "B printed" << endl; } }; B b; b.Print();
This also prints what you might expect, B printed. However, let's now mix up the types:
A * aPtr = new B; // aPtr points to a B aPtr->Print();
This prints B printed. So, even though the pointer is of type A, the object remembers what type it really is. As a class object is created, Visual C++ stores away a secret piece of information about the object. When it executes a virtual call, it looks
up the information about the object and only then decides which version to call.
There is a common trap in declaring functions as virtual. When a base class has declared a function virtual, all derived class functions that override that function will also be declared virtual. So the following class definition of B is identical to the previous definition:
class B : public A { void Print() // Implicitly virtual { cout << "B printed" << endl; } };
Things become more interesting when you put a virtual function call into a class member. In Listing 22.1, you run a little exercise to follow the different ways in which virtual and nonvirtual functions work.
1:// File name : VIRTUAL.CPP 2:// Program to demonstrate the difference between 3:// standard and virtual function calls 4:// 5:#include <iostream.h> 6: 7:class BaseClass 8: { 9: public: 10: void Plain() 11: { 12: cout << "BaseClass::Plain()" << endl; 13: } 14: virtual void Virtual() 15: { 16: cout << "BaseClass::Virtual()" << endl; 17: } 18: void CallVirtual() 19: { 20: Virtual(); 21: } 22: }; 23: 24:class DerivedClass : public BaseClass 25: { 26: public: 27: void Plain() 28: { 29: cout << "DerivedClass::Plain()" << endl; 30: } 31: virtual void Virtual() 32: { 33: cout << "DerivedClass::Virtual()" << endl; 34: } 35: }; 36: 37:void main() 38: { 39: BaseClass baseClass; 40: DerivedClass derivedClass; 41: 42: baseClass.Plain(); 43: baseClass.Virtual(); 44: baseClass.CallVirtual(); 45: cout << endl; 46: 47: derivedClass.Plain(); 48: derivedClass.Virtual(); 49: derivedClass.CallVirtual(); 50: cout << endl; 51: 52: BaseClass * basePointer = &baseClass; 53: BaseClass * derivedPointer = &derivedClass; 54: // NB type of pointer is base class 55: 56: basePointer->Plain(); 57: basePointer->Virtual(); 58: basePointer->CallVirtual(); 59: cout << endl; 60: 61: derivedPointer->Plain(); 62: derivedPointer->Virtual(); 63: derivedPointer->CallVirtual(); 64: cout << endl; 65: }
Output
BaseClass::Plain() BaseClass::Virtual() BaseClass::Virtual() DerivedClass::Plain() DerivedClass::Virtual() DerivedClass::Virtual() BaseClass::Plain() BaseClass::Virtual() BaseClass::Virtual() BaseClass::Plain() DerivedClass::Virtual() DerivedClass::Virtual()
Analysis
This is a very simple program that shows which version of the function gets called. First, it makes two objectsone for each class in lines 39 and 40. In lines 42 to 44, the base class is called, and as you might expect, all the base class output
appears after these calls.
Next, in lines 47 through 49, the derived class object is used to call all three functions. This time, you might be less certain as to what you were expecting to happen, but calling Plain() and Virtual() results in the derived class being called. Look
at CallVirtual(). This has not been overridden in DerivedClass. From what you've seen in earlier units, you know that this will execute the base class version. What happens now might seem surprising. Although you are executing BaseClass::CallVirtual(), the
call to Virtual() inside this calls DerivedClass::Virtual(). That is a clever trick because when you wrote BaseClass, you did not make any mention of DerivedClass at all!
Next, you'll use a BaseClass pointer to do the same calls. Unsurprisingly, the code works just the same for a BaseClass pointer as for a BaseClass variable.
Finally, you access the DerivedClass through a BaseClass pointer, accessing exactly the same object as before because you used the address of operator to assign the pointer. As you've seen before, the Plain() function is called because the access is by
the BaseClass pointer. In a virtual function call, something different happens. C++ only apparently calls BaseClass::Virtual(); in fact, because it is virtual, it still uses the DerivedClass::Virtual(). Then, in the final call, C++ still calls the derived
Virtual() even from the BaseClass pointer.
If, in the future, you are confused about the way Visual C++ calls functions, quickly write a little program like this to see how Visual C++ works. You can easily step through the code with the debugger to see in detail what gets executed.
When a virtual function is executed, the type of the object determines which version of a function executes, not the type of the calling object.
As you saw in the previous example, the type of the object that is made decides which version of a virtual function gets called. This is true both of calls from objects outside the class and of calls from within the class hierarchy (either base or
further derived classes). Compared with standard functions, both of these call the same function:
baseClassPointer->Virtual()
((DerivedClass*)baseClassPointer)->Virtual()
You must be very careful in spotting when an object can lose its type. If you assign or copy a derived class into a base class object (that is not using an address or pointer but an actual object copy or assignment), the data of the object gets copied
and not the object type. A simple example shows the difference:
void ThreeObjects(BaseClass bc, BaseClass& bcr, BaseClass* bcp) { bc.Virtual(); bcr.Virtual(); bcp->Virtual(); }
When this code is called by
ThreeObjects(derivedClass,derivedClass,&derivedClass);
it produces the following:
BaseClass::Virtual() DerivedClass::Virtual() DerivedClass::Virtual()
This occurs because when a parameter is passed by value, the passing mechanism makes a copy of the object, but the temporary object is actually of the type of the parameter, not of the type of the argument passed. The same applies when passing a return
value that is not a reference or pointer. Therefore, care is needed when passing objects around where you are hoping to use virtual functions. If in doubt, try to use a reference parameter rather than passing by valueand use the const keyword where
sensible.
When do you use virtual functions? It seems so useful that the classes automatically run the right function that it is tempting to always use them. However, virtual functions are not as efficient as standard functions, and there is no point in using
virtual functions for private functions or functions that are not intended to be overridden.
You can't use virtual functions in constructors because the derived class has not yet been made.
Think about your Vehicle class again from Unit 21. Wheels-U-Like needed to maintain a database of vehicles, and you used derived classes to represent the different types of vehicles. When a vehicle is to be rented, you need to get general information
and then specific information. Instead of relying on the derived classes to repeat the work of remembering to call the base class functions, you can turn the program inside out and ask the derived class to provide information at specific points in the
program. For example, Vehicle::PrintRental() could work like this:
class Vehicle { //... the rest of the class public: void PrintRental(); protected: virtual void PrintAdditionalDetails(); }; void PrintRental() { cout << "Rental Title" << endl; // cout the vehicle details PrintAdditionalDetails(); }
The preceding snippet will print out the common information about a class and then call a version of PrintAdditionalDetails() that is appropriate to the class.
What should PrintAdditionalDetails() do in the base class? The answer depends on whether the class designer wants the vehicle class to be usable in its own right. There is a trick you can use to stop the class from being usable and to force the person
who uses the class to provide a function. If the virtual function
virtual void PrintAdditionalDetails() = 0;
is declared, the class won't be usable. (In the difficult words that seem to always appear with C++, this make the class an abstract base class). C++ will reject a program that tries to define a class in which such a function has not been
overridden.
You can still force a specific version of a function to be called. A derived class virtual function replaces the base class function, so the derived class still might need to call the base class to make the function work. It would be very inconvenient
if the virtual mechanism came into play, so the call
void DerivedClass::Virtual() { BaseClass::Virtual(); cout << "DerivedClass::Virtual()"; }
still calls the base class version as its first action.
It's tricky to decide what version executes when a virtual function calls a nonvirtual member function. The answer is that the class used is determined by the class of the executing function (which might be of neither the calling class nor the object
class):
class A { public: void Print() { cout << "A"; } virtual void CallPrint() { Print(); } }; class B : public A { public: void Print() { cout << "B"; } virtual void CallPrint() { Print(); } }; class C : public A { public: void Print() { cout << "C"; } }; A * a = new C; a->CallPrint();
This looks to class C to decide which virtual function to call. Finding no virtual function there, it looks back to the base class (to class B) and executes B::CallPrint(). Then B::CallPrint() looks for Print(). Because Print() is not virtual, it uses
the current type of the call (which is now B not C) to decide which version of Print() it uses. So when you write a function, you know that the following are true:
Virtual destructors ensure that a matching destructor is called for any object.
The last hole in your code for Wheels-U-Like was trying to sort out a way of calling the right destructor. By using the virtual mechanism on the destructor, you can leave this up to Visual C++. Visual C++ knows the type of the object, so it can call the
right virtual destructor. This derived class still automatically calls all the destructors of the base class.
The rule on whether to declare a base class destructor as virtual is pretty simple: Always declare a base class destructor as virtual unless you are absolutely certain that any derived class will only be handled by objects of its own class.
Listing 22.2 shows a much cleaner implementation of the Wheels-U-Like system. By using virtual functions, the code becomes much easier.
1:// File name: WHEELSVL.CPP 2:// 3:// A simple set of rental classes using virtuals 4:// 5:#include <iostream.h> 6:#include <string.h> 7: 8: 9:// Base class 10:class Vehicle 11: { 12: public: 13: virtual ~Vehicle() // Virtual destructor 14: {} // does nothing but safe 15: void Print() const; // Never overridden 16: virtual void Rent(const char * ReturnDate); // Could be 17: protected: 18: Vehicle(long Mileage, const char * Model); // Not needed public 19: virtual const char * GetRentalType() const = 0; 20: virtual void PrintAdditionalDetails() const = 0; 21: private: 22: long mileage; 23: char model[21]; 24: char returnDate[9]; 25: }; 26: 27:class Car : public Vehicle // Car is derived from Vehicle 28: { 29: public: 30: Car(long Mileage, const char * Model,char Category); 31: protected: 32: const char * GetRentalType() const // Still virtual 33: { 34: return "Car"; 35: } 36: void PrintAdditionalDetails() const; // still virtual 37: private: 38: char category; // Only in Car not Vehicle 39: }; 40: 41:class Truck: public Vehicle // Truck is derived from Vehicle 42: { 43: public: 44: Truck(long Mileage, const char * Model,float MaxLoad); 45: protected: 46: virtual const char * GetRentalType() const 47: { 48: return "Truck"; 49: } 50: virtual void PrintAdditionalDetails() const; 51: private: 52: float maxLoad; // Only in Truck 53: }; 54: 55:// Global function 56:void GetRentalDate(char * date) 57: { 58: cout << "When are you returning the vehicle?"; 59: cin.getline(date,9); 60: } 61: 62://------------------------------------------------------------ 63:// Main is here! 64://------------------------------------------------------------ 65:void main() 66: { 67: char type; 68: char rentalDate[9]; 69: Vehicle * vehicle; 70: // Ask what type of rental 71: cout << "Rent car or truck (t)? "; 72: cin >> type; 73: cin.ignore(80,'\n'); 74: if (type == 't' || type == 'T') 75: // truck rental 76: vehicle = new Truck(4000,"Dodge",3.5F); 77: else 78: // Car rental 79: vehicle = new Car(2500,"Buick",'B'); 80: // Normal function 81: GetRentalDate(rentalDate); 82: vehicle->Rent(rentalDate); // Not overridden 83: vehicle->Print(); 84: delete vehicle; 85: } 86://------------------------------------------------------------ 87:// Vehicle class functions 88:// 89:Vehicle::Vehicle(long Mileage, const char * Model) 90: { 91: mileage = Mileage; // could have used intialization list 92: strcpy(model,Model); 93: returnDate[0] = '\0'; // zero length string 94: } 95: 96:void Vehicle::Print() const 97: { 98: cout << GetRentalType() << " Rental" << endl; 99: cout << "Model : " << model << endl; 100: cout << "Mileage: " << mileage << endl; 101: cout << "Rented till: " ; 102: if (returnDate[0]) // a test to see if first char is 103: // a terminator 104: cout << returnDate; 105: else 106: cout << "Not rented"; 107: cout << endl; 108: PrintAdditionalDetails(); 109: } 110: 111:void Vehicle::Rent(const char * RentalDate) 112: { 113: strcpy(returnDate,RentalDate); 114: } 115: 116://------------------------------------------------------------ 117:// Car class functions 118:// 119:Car::Car(long Mileage, const char * Model, char Category) 120: : Vehicle(Mileage,Model) // calls base constructor 121: { 122: category = Category; 123: } 124: 125:void Car::PrintAdditionalDetails() const 126: { 127: cout << "Category : " << category << endl; 128: } 129: 130://------------------------------------------------------------ 131:// Turck class functions 132:// 133:Truck::Truck(long Mileage, const char * Model, float MaxLoad) 134: : Vehicle(Mileage,Model) 135: { 136: maxLoad = MaxLoad; 137: } 138: 139:void Truck::PrintAdditionalDetails() const 140: { 141: cout << "Max load : " << maxLoad << endl; 142: }
Output
First run
Rent car or truck (t)? Car When are you returning the vehicle?10/10/95 Car Rental Model : Buick Mileage: 2500 Rented till: 10/10/95 Category : B
Second run:
Rent car or truck (t)? t When are you returning the vehicle?11/11/95 Truck Rental Model : Dodge Mileage: 4000 Rented till: 11/11/95 Max load : 3.5
Analysis
Comparing this listing with the original, you should see two things: First, it is shorter. Second, it is simpler but it does the same job as last time.
This is a successful class structure. The only significant function of the classes is Print() in lines 96 to 109. This is not a virtual function. It is designed to do the printing for any derived class. To do this, it needs to ask two things of the
derived classes: how they would like the rental titled (GetRentalType()), and if there is any additional information they would like printed (PrintAdditionalDetails()). Vehicle forces derived classes to provide both of these functions by declaring them
abstract (that is =0) in lines 19 and 20. It also declares them protected, because they are only intended to be helper routines, rather than for general use.
It is worth taking a look at the Car and Truck classes. Both have been reduced to very simple classes. They have a constructor that just needs to look after their own data members and pass on the rest to the base class (line 120). Then the type
description is a one-liner (lines 32 to 35not quite one line, but you get my drift). The additional details are again trivial: another one-liner, lines 125 through 128. You can see that it would be very simple to add further rental classes derived
from vehicle that looked after specific data. By leaving the right blanks in the base class in the right place, it is easy to make code very general using virtual functions. Notice that the base class designer has planned from the start that the class is
to be derived from. This is typical. Classes are not just written to fill one job; from the beginning, they will be designed to be borrowed from.
This example does not exaggerate the power of a well-designed class. When you use library code, you often need to derive from a standard class. The objective of a code library class is to do all the hard work for you and just get the derived class to
fill in any specific gaps. You'll normally find that to use a professional class to make your own specific objects, you write a few simple functions according to some simple rules, and often you're able to copy another example. You do not need to be an
expert coder of C++ to make use of sophisticated prewritten classes.
There is one important, though occasionally used, feature that should be covered before closing this lesson. At times, you want to get a pointer to the current object within a member function. For example, you could write a function to search an array
of pointers to a class to find out which array index holds the object. Although you could do this outside of a member function, you might want to encapsulate the code. You can get a pointer to the current object within a member function by using the this
keyword. this returns a pointer of the type of the current class. The code
Vehicle::Print() { cout << this->mileage; ...
is equivalent to this code:
Vehicle::Print() { cout << mileage; ...
Normally, you don't need to use the this pointer because C++ always implies it within member functions. There are some special cases in which the this pointer becomes important, such as when an object needs to identify itself to a function that it
calls. If you build linked lists of objects, you might need to store a pointer to the new object in another object, and if the processing is within a member function, this is an easy way to access the current object.
The code in Listing 22.3 shows a simple example of how you might use this.
1:// Filename : THIS.CPP 2:// A simple demonstration of using the this pointer 3:// 4:#include <iostream.h> 5: 6:class Search 7: { 8: public: 9: int IsTheSameAs(Search * s) 10: { 11: if (s == this) 12: return 1; 13: else 14: return 0; 15: } 16: }; 17: 18:void main() 19: { 20: Search s[5]; 21: Search * test = &s[3]; 22: for (int i = 0; i < 5; i++) 23: { 24: if (s[i].IsTheSameAs(test)) 25: { 26: cout << "The index is " << i; 27: break; 28: } 29: } 30: }
Output
The index is 3
Analysis
The code creates the objects in an array in line 20. The objects don't do anything useful. In line 21, the program saves a pointer to the fourth object in the array.
In lines 22 through 29, the array is searched by calling a member function against each array member. (Note that the code could just as easily have called the member function from the test pointer and passed the address of each array member.) For each
member, the IsTheSameAs test is applied. IsTheSameAs receives the pointer and compares it in line 11 to the this pointer, which is the pointer to the current object. If the object is the same, the pointer values will match.
Given the following class definitions, answer the subsequent questions:
class A { public: virtual ~A() { cout << "A deleted "; void Print() { cout << "A"; } virtual void CallPrint() { Print(); } }; class B : public A { public: void Print() { cout << "B"; } virtual void CallPrint() { Print(); } }; class C : public B { public: ~C() { cout << "C deleted "; } void Print() { cout << "C"; } };
A a;
C c;
A * a = new C; ((B*)a)->CallPrint(); delete a;
class A { public: A() {} ~A() {} }; class B : public A { public: B() { b = new char[21]; } ~B() { delete [] b; } char * b; }; void main() { A * a = new B; delete a; }
#include <iostream.h> class A { public: virtual ~A() { cout << "A deleted "; void Print() { cout << "A"; } virtual void CallPrint() = 0; }; class B : public A { public: void Print() { cout << "B"; } }; void main() { B b; }