base class
derived class
private
protected
public
After encapsulation comes inheritance. In this unit, you will see how Visual C++ uses classes to enable you to make new code from old.
You'll see why there is a difference between protected and private members, and you'll see some of the magic that makes Visual C++ different from any language you've previously seen.
Visual C++ needs to be told in the class definition from which other class the new class is to be made.
So far, you've seen some of the simpler features of classes. You might be wondering whether all the effort of classes is worth it, compared to simpler ways of coding the programs you have developed. By the end of this unit, you will start seeing some of
the real bonuses of classes.
Definition
A base class is a class that stands on its own and is not derived from any other class.
Definition
A derived class is a class that is made by inheriting the characteristics of another class.
Let's look first at an example of a set of classes that you will develop. To make a set of classes that work well, a plan is required. Imagine that you want to write a system to look after the vehicles owned by the Wheels-U-Like rental company. It can
own cars, buses, and trucks. These vehicles have lots of things in common: mileage, cost, whether they are currently rented. There are also things that are special to each of them: The number of seats is irrelevant to a truck rental, but the carrying
capacity of a truck is important. The plan in writing a set of objects is to make a base class, which covers all the common information. Then, for each specific case, invent a derived class, which takes the base class and adds extra
information.
The starting point will be a vehicle. First, define the data you want to hold and some simple functions you know you'll need.
class Vehicle { public: Vehicle(); ~Vehicle(); void Print(); void Set(); void Rent(); void Return(); protected: long mileage; date dateRented; date dateReturned; string make; string model; };
Pretend that you have written a date handling class. Dates are always a tricky thing to handle in programs. You'll work out what the functions really look like later.
Wheels-U-Like rents cars. On top of the basic data, you will need a group for rental and a group for whether it is auto or manual; everything else will be the same as for a vehicle. In Visual C++, a class is derived from another simply by placing the
class name of the base class and the public keyword after a colon (:) and before the body of the class. (You can use other than public derivation, but then you are starting to get into the tricky parts of the language that you are unlikely to come across.)
Car : public Vehicle { };
The preceding declaration is complete in itself but not very useful. It works exactly like the Vehicle class you designed before. Just like the other classes you have declared, you can add more data items:
Car : public Vehicle { protected: int catagory; int isAutomatic; };
Now you have the data made up of all the Vehicle data and all the Car data.
New functions can be added easily. Changing the way original functions work needs more care.
You can easily add functions to get and set the category and whether the car has a manual or automatic gear shift:
Car : public Vehicle { public: void SetAuto(int Auto); int GetAuto(); // and so on...
However, you really are interested in making use of code that you already have. When Print() is called for a Vehicle, you would expect it to tell all about the data in the Vehicle class, and you need the same function for the Car class. It should be no
surprise that you can use the same function name in the Car class as the Vehicle class. How does Visual C++ tell the functions apart? If you did not give a Car printing function, the Vehicle Print() function would be called. Quite simply, when there are
two identical functions in the original class and the derived class, Visual C++ decides which to call by looking at the class of the calling object:
Vehicle v; Car c; v.Print(); // calls Vehicle::Print() c.Print(); // calls Car::Print()
Normally, this is just what you need. However, the Car version will not do anything clever with the Vehicle function it replaces. Often, you will want a replacement function to do just what the base class function did, and then do some extra code to do
the special things that the derived class needs. Within a member function, you can call other member functions, but these will still look to the current class. To say that you want to execute a different version of the function, you need to use the scope
operator (::) and tell Visual C++ which version you want to call:
void Car::Print() // Normal function signiture { Vehicle::Print();// Call a base class version cout << "Automatic? " << automatic?'Y' : 'N'; // and so on }
The Vehicle:: in front of Print() is vital. Without it, Visual C++ would call Car::Print(), which would rapidly result in a very serious loop calling itself over and over again. As you can see, inheriting a function is straightforward and consists of
these steps:
Visual C++ lets you derive further classes from an already derived class. The mechanism works in just the same way:
class SportsCar : public Car { protected: int openTop; };
Definition
The class from which a new class has been derived is called the immediate base class to distinguish it from any other classes that the new class might be derived from.
In this case, SportsCar can use all the functions of Car and Vehicle. Where the new class does not replace a function, it will see the function of the most derived class.
There is a special trick for constructors. Instead of calling the constructor in the body of the code, you must use the initialization list:
Car::Car() : Vehicle() { }
Use this if there are parameters:
Car::Car(long Mileage) : Vehicle(Mileage) { }
If you do not specify the base class constructor to call, Visual C++ will call the default constructor for you. For a class that has been derived several times, there is no need to call all the base classes' constructors because the previous base class
has already called the next base class.
Destructors work differently. A derived class always automatically calls any base class destructors. You never need to put any code to call base class destructors. Visual C++ does it all automatically. (I thought that was worth saying twice.)
Time for a listing! Listing 21.1 makes a simple version of your rental classes. To keep things from getting too complicated, you will just store dates as character strings and not use the string class that you've developed. But when
you develop classes, you should look to use classes within classes, rather than rewriting all the string and date management features in every class you use.
1:// File name: WHEELSUL.CPP 2:// 3:// A simple set of rental classes 4:// 5:#include <iostream.h> 6:#include <string.h> 7: 8: 9:// Base class 10:class Vehicle 11: { 12: public: 13: Vehicle(long Mileage, const char * Model); // Constructor 14: void Print() const; 15: void Rent(const char * ReturnDate); 16: protected: 17: long mileage; 18: char model[21]; 19: char returnDate[9]; 20: }; 21: 22:class Car : public Vehicle // Car is dervied from Vehicle 23: { 24: public: 25: // Constructor 26: Car(long Mileage, const char * Model,char Category); 27: void Print() const; // Override Vehicle::Print 28: protected: 29: char category; // Only in Car not Vehicle 30: }; 31: 32:class Truck: public Vehicle // Truck is derived from Vehicle 33: { 34: public: 35: // Constructor 36: Truck(long Mileage, const char * Model,float MaxLoad); 37: void Print() const; // Override Vehicle::Print 38: protected: 39: float maxLoad; // Only in Truck 40: }; 41: 42:// Global function 43:void GetRentalDate(char * date) 44: { 45: cout << "When are you returning the vehicle?"; 46: cin.getline(date,9); 47: } 48: 49://------------------------------------------------------------ 50:// Main is here! 51://------------------------------------------------------------ 52:void main() 53: { 54: char type; 55: char rentalDate[9]; 56: // Ask what type of rental 57: cout << "Rent car or truck (t)? "; 58: cin >> type; 59: cin.ignore(); 60: if (type == 't' || type == 'T') 61: { 62: // truck rental 63: // Make a truck (for simplicity - hard coded) 64: Truck truck(4000,"Dodge",3.5F); 65: // Normal function 66: GetRentalDate(rentalDate); 67: // Truck does not have Rent - calls Vehicle::Rent 68: truck.Rent(rentalDate); 69: // Truck does have Print - calls Truck::Print 70: truck.Print(); 71: } 72: else 73: { 74: // Car rental 75: Car car(2500,"Buick",'B'); // W.U.L. only has 1 car! 76: GetRentalDate(rentalDate); 77: // Car does not have Rent - calls Vehicle::Rent 78: car.Rent(rentalDate); 79: // Car has Print - calls Car::Print 80: car.Print(); 81: } 82: } 83://------------------------------------------------------------ 84:// Vehicle class functions 85:// 86:Vehicle::Vehicle(long Mileage, const char * Model) 87: { 88: mileage = Mileage; // could have used intialization list 89: strcpy(model,Model); 90: returnDate[0] = '\0'; // zero length string 91: } 92: 93:void Vehicle::Print() const 94: { 95: cout << "Model : " << model << endl; 96: cout << "Mileage: " << mileage << endl; 97: cout << "Rented till: " ; 98: if (returnDate[0]) // a test to see if first char is 99: // a terminator 100: cout << returnDate; 101: else 102: cout << "Not rented"; 103: cout << endl; 104: } 105: 106:void Vehicle::Rent(const char * RentalDate) 107: { 108: strcpy(returnDate,RentalDate); 109: } 110: 111://------------------------------------------------------------ 112:// Car class functions 113:// 114:Car::Car(long Mileage, const char * Model, char Category) 115: : Vehicle(Mileage,Model) // calls base constructor 116: { 117: category = Category; 118: } 119: 120:void Car::Print() const // Overrides Vehicle::Print() 121: { 122: cout << "Car Rental" << endl; 123: Vehicle::Print(); // Calls base class Print 124: cout << "Category : " << category << endl; 125: } 126: 127://------------------------------------------------------------ 128:// Car class functions 129:// 130:Truck::Truck(long Mileage, const char * Model, float MaxLoad) 131: : Vehicle(Mileage,Model) 132: { 133: maxLoad = MaxLoad; 134: } 135: 136:void Truck::Print() const 137: { 138: cout << "Truck Rental" << endl; 139: Vehicle::Print(); 140: cout << "Max load : " << maxLoad << endl; 141: }
Output
First run
Rent car or truck (t)? c 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?09/09/95 Truck Rental Model : Dodge Mileage: 4000 Rented till: 09/09/95 Max load : 3.5
Analysis
The Vehicle class declared in lines 10 to 20 contains the code and data to be shared between all the different types of rental classes. The data is declared as protected (line 16) so that derived classes can directly access it if need be (which does not
happen in this program). In fact, it is often better to use access functions to get and set the data in a safe way.
In line 22, a Car is derived from the class Vehicle. Typical of a derived class, the constructor needs all the parameters of the base class (compare lines 13 and 26) and any extra data for the new class. In lines 114 to 118, the derived class calls the
base class constructor (line 115) in the initialization list, passing on the parameters that belong to the Vehicle class. This means that Car does not need to know how to set up the Vehicle part of the data. In line 117, the rest of the class is
initialized.
The Truck class is very nearly identical. Although Truck is derived from the same class as Car, the classes can't see each other.
When the program runs, if Truck is chosen, the program calls the Truck constructor (line 64), which in turn calls the Vehicle constructor. When the program calls Rent(), it must look for the definition of the function. It is not
declared in the Truck class, so Visual C++ looks to see whether it could be anywhere else. Visual C++ looks in the base class and finds a matching function, which it then calls. Although the calling object is a Truck, the function executes as if the
calling object type was a Vehicle. This is an important point, because even though the calling class is Truck, Vehicle can't see any data or functions that belong to Truck.
Next, in line 70, there is a call to Print(). This time, Print() has been overridden. Visual C++ looks to Truck's class definition and finds that there is a matching function. The function in lines 136 through 141 executes as if the other functions did
not exist. Then, in line 139, it wants to output the data belonging to the base class. Fortunately, the base class Print() has been designed so that the data will fit in with a standard listing. The call to the base class (line 139) needs the scope
operator and class to tell Visual C++ which version of the function must be executed. (If Vehicle was derived from another class that contained Print(), and Vehicle did not contain it, Visual C++ is clever enough to still use the base class version, even
with the explicit class scope being called.)
The Car class works in exactly the same way as Truck, except that it has its own data to work with. Car can't see Truck.
Visual C++ uses the type of the calling object to decide which version of the function to use.
When a derived class is given to a pointer, the pointer can be of the type of the derived class or of any base class, as in the following example:
Vehicle * v = new Car("Ford",3000,'A');
Something strange happens when you use an object via the Vehicle pointer. Instead of working like the Car class, it only appears to be a vehicle:
v->Print();
This code would print the following:
Model : Ford Mileage: 3000 Rented till: Not Rented
However, somewhere in the heart of Visual C++, the class always remembers its ancestry. If it is known that the base class pointer really points to a Car, you can turn it back into one with a cast:
((Car*)v)->Print();
This code would print the following:
Car Rental Model : Ford Mileage: 3000 Rented till: Not rented Category : A
This strange collection of brackets is required because of operator precedence: Casting is a low precedence, and without the brackets, it would be applied to the return value of Print(). An easier to follow expression—though not in the "all in
one breath" style so beloved by your fellow C++ programmers—would be the following:
Car* c = (Car*)v; c->Print();
You can always make a derived class into a base class without so much as a hiccup. You can never safely make a base class into a derived class. The cast will work, but there is not a way in Visual C++ of checking that the cast is valid. One workaround
solution is to store the type of object yourself in the base class, and then implement an IsA() function that you can use to run different versions of code depending on the type. This solves the problem, but has the downside of being error-prone and a
maintenance problem as new derived classes are added. (Fortunately, in the next unit, you'll see that Visual C++ has an extremely clever mechanism to deal with this problem.)
In the latest versions of C++ as defined by the ANSI committee, there is a new feature called dynamic_cast, which allows the checking of the type as the cast is made. However, this is not available in this version of Visual C++, and it only solves the problem of detecting an error, not determining the type of class.
Vehicle * v = new Vehicle("Ford", 3500); ((Car*)v)->Print()
The preceding code will compile, but it won't work. Figure 21.1 gives a clue as to why. You can imagine (but not rely on the fact) that the storage of a derived class is added to the end of the base class as C++ makes the structure. Imagine that
accessing the storage is like using a cardboard cutout placed over the storage. As you can see from Figure 21.1, going toward the base class, you'll always see a valid storage area; but trying to look at a base class with a derived class cutout, you'll see
bits of storage that don't belong.
Figure 21.1 Viewing class storage via pointers
The same is true for using references, aside from the different access operator. This is important because quite often you'll want to store collections of objects. For example, Wheels-U-Like wants to expand from renting one truck and one car (even
though the owner is extremely lazy, he wants to make money one day). Therefore, the program needs to store lots of vehicles. The program could store one array for cars and another for trucks. But then, for reports such as vehicles on rental today, the
program would have to look through two lists, even though the program is not interested in the type of vehicle.
The program could hold an array of Vehicle pointers, and then use the appropriate class depending on the new rentals to create the objects.
Care must be taken when using delete. This only deletes the class that is pointed to. It will not remove any storage or perform any other tidying up, which is required by a more derived class type than the type of the object that is being operated on by
delete.
The Vehicle class uses protected rather than private. To the world outside the class, they are identical; neither protected nor private can be accessed. To a derived class, there is a difference. The base class designer can decide to allow a derived
class to access the data. If it is safe, the data is declared protected, and to the code within member functions, it is as if the data is public. Here's an example:
class A { public: int a; protected: int b: private: int c: }; class B: public A { void Bfunction(); }; void main() { B b; b.a = 5; // ok, public b.b = 5; // Illegal - protected b.c = 5; // Illegal - private } B::Bfunction() { a = 5; // ok, public b = 5; // ok, protected appears as public to // derived classes c = 5; // Illegal - private cannot be seen }
To understand why there is a difference, notice that the access requirements for a derived class are different from the access requirements for a base class. To improve a class by derivation means that the derived class is likely to need privileged
access to class members. The target is still to create a public interface for both the base and derived classes in which only safe functions and variables can be accessed.
The derived class can be allowed to be trusted by the designer of a base class. There might be some complicated code that the base class designer needs to look after carefully. The members and functions to run this code would be private. There might be
certain variables and (especially) functions that the designer knows a derived class should be able to access in order to write a good class. For example, the Print() function of vehicle might not be adequate on its own. It does not put out a title. The
base class designer might decide that it is not a suitable part of the public class, but it does need to be made available for the derived classes to use. In this case, declaring Print() protected would allow any derived class to use the function without
restriction. But any coder using the Vehicle class would not be allowed to use Print().
The following should complete your understanding of the three classes of access:
The choice between public and private is normally easy. Trying to decide which data is protected and which is private is more difficult. The dilemma for the novice programmer is that providing lots of access functions seems tedious, so it's very
tempting to make everything protected or even public. If you look at professional C++ classes, they are nearly always made up of lots of very small functions. This means that the code is normally very easy to follow. The downside is that working out when
the functions execute can be a little complicated at times.
Before you go on to the final listing, there is a different way of coding class functions. Because a class tends to lead to writing small one-line functions, the overhead of the C++ calling mechanism could make your programs inefficient. By adding the
modifier inline, you can suggest to the compiler that, instead of using a function call, it could replace the call in the calling function with all the code in the called function:
void inline A::DoIt(int a) { b = a * 5; } void main() { A a; int i = 2; a.DoIt(i); }
This code becomes the following:
void main() { A a; int i = 2; tempi = 2; // pass by value assume not optimized // tempi some internal variable invented // by the compiler a.b = tempi * 5;// compiler looks after access itself }
Normally, you need not worry about this finesse in your early coding days. Worry about getting the code right first! However, there is a shortcut way of writing class functions. You can put the code within the class declaration, and then it
automatically becomes inlined:
class A { public: int GetA() { return a; } private: a; };
The temptation is to write all your classes like this because it is compact. This is fine for routines of a few lines (less than five is a rule of thumb). However, there are certain instructions that C++ can't inline, so the compiler might warn that it
won't inline the code after all. Also, too much code in the class definitions makes them difficult to read.
Listing 21.2 uses this method to keep the listings small and manageable.
1:// File name : PETS.CPP 2:// Object oriented pet program to manage 3:// a simple list of different types of 4:// pets 5:// 6:#include <iostream.h> 7: 8://------------------------------------------------------------ 9:// Pet class 10:// 11:const int nameLength = 21; 12: 13:class Pet 14: { 15: public: 16: enum PetType {cat, dog, fish}; 17: void Print() 18: { 19: cout << name; 20: } 21: PetType GetType() 22: { 23: return type; 24: } 25: void Query(); 26: protected: 27: Pet(PetType pt) // protected - 28: // do not make Pets 29: { 30: type = pt; 31: name[0] = '\0'; 32: } 33: private: 34: PetType type; 35: char name[nameLength]; 36: }; 37: 38:void Pet::Query() 39: { 40: cout << "What is your pet's name? "; 41: cin.getline(name,nameLength); 42: } 43: 44://------------------------------------------------------------ 45:// Cat class - derived from Pet 46:// 47:class Cat : public Pet 48: { 49: public: 50: Cat(int Scratches = 1) 51: : Pet(cat) // can see protected constructor 52: { 53: scratches = Scratches; 54: } 55: void Print(); // Overrides Pet::Print 56: void Query(); // Overrides Pet::Query 57: private: 58: int scratches; 59: }; 60: 61:void Cat::Print() 62: { 63: cout << "Cat: " ; 64: Pet::Print(); 65: cout << endl << "Scratches: " << (scratches? "Yes":"No") 66: << endl; 67: } 68: 69:void Cat::Query() 70: { 71: char yn; 72: Pet::Query(); 73: cout << "Does your cat scratch? "; 74: cin >> yn; 75: cin.ignore(80,'\n'); 76: if (yn == 'Y' || yn == 'y') 77: scratches = 1; 78: else 79: scratches = 0; 80: } 81: 82://------------------------------------------------------------ 83:// Dog class - derived from Pet 84:// 85: 86:class Dog : public Pet 87: { 88: public: 89: Dog(int Barks = 1) 90: : Pet(dog) 91: { 92: barks = Barks; 93: } 94: void Print(); 95: void Query(); 96: private: 97: int barks; 98: }; 99: 100:void Dog::Print() 101: { 102: cout << "Dog: " ; 103: Pet::Print(); 104: cout << endl << "Barks: " << (barks? "Yes":"No") 105: << endl; 106: } 107: 108:void Dog::Query() 109: { 110: char yn; 111: Pet::Query(); 112: cout << "Does your dog bark? "; 113: cin >> yn; 114: cin.ignore(80,'\n'); 115: if (yn == 'Y' || yn == 'y') 116: barks = 1; 117: else 118: barks = 0; 119: } 120: 121://------------------------------------------------------------ 122:// Fish class - derived from Pet 123:// 124:class Fish : public Pet 125: { 126: public: 127: Fish(int ColdWater = 0) 128: : Pet(fish) 129: { 130: coldWater = ColdWater; 131: } 132: void Print(); 133: void Query(); 134: private: 135: int coldWater; 136: }; 137:void Fish::Print() 138: { 139: cout << "Fish: " ; 140: Pet::Print(); 141: cout << endl << "Water: " << (coldWater? "Cold":"Tropical") 142: << endl; 143: cout << "You can't take a goldfish for walks" << endl; 144: } 145: 146:void Fish::Query() 147: { 148: char yn; 149: Pet::Query(); 150: cout << "Is your fish tropical? "; 151: cin >> yn; 152: cin.ignore(80,'\n'); 153: if (yn == 'Y' || yn == 'y') 154: coldWater = 0; 155: else 156: coldWater = 1; 157: } 158: 159://------------------------------------------------------------ 160:// main procedure 161:// 162: 163:void main() 164: { 165: char type; 166: Pet * pets[20] = {0}; 167: int count = 0; 168: 169: while (1) // Do forever 170: { 171: // Where do you want to go today? 172: cout << "Pet type, [C]at, [D]og, [F]ish, [Q]uit: "; 173: cin >> type; 174: cin.ignore(80,'\n'); 175: 176: // Stop forever loop 177: if (type == 'q' || type == 'Q') 178: break; 179: 180: switch (type) 181: { 182: case 'C': // Cat 183: case 'c': 184: { 185: pets[count] = new Cat; // Looses "Catness" 186: ((Cat*)pets[count])->Query(); // nasty cast 187: break; 188: } 189: case 'D': // Dog 190: case 'd': 191: { 192: Dog * d = new Dog; // Use Dog pointer to 193: d->Query(); // keep dogginess 194: pets[count] = d; // automatic type conversion 195: break; 196: } 197: case 'F': // Fish 198: case 'f': 199: { 200: pets[count] = new Fish; // Looses Fish 201: Fish * f = (Fish*)pets[count]; // Get Fish back 202: f->Query(); // f points to same plaice(!) 203: // as pets[count] 204: break; 205: } 206: default: 207: count--; 208: } 209: count++; 210: } 211: 212: // List pets - don't need derived classes for this 213: cout << endl << "Pet Names" << endl; 214: for (int i = 0; i < count; i++) 215: { 216: pets[i]->Print(); 217: cout << endl; 218: } 219: 220: // List characteristics - need exact types 221: cout << endl << "Characteristics" << endl; 222: for (i = 0; i < count; i++) 223: { 224: switch (pets[i]->GetType()) 225: { 226: case Pet::dog: // Use access to get at class enum 227: ((Dog*)pets[i])->Print(); // nasty cast 228: break; 229: case Pet::cat: 230: { 231: Cat* c = (Cat*)pets[i]; // still nasty 232: c->Print(); 233: break; 234: } 235: case Pet::fish: 236: ((Fish*)pets[i])->Print();// no escape from cast 237: break; 238: } 239: } 240: // Tidy up storage 241: for (i = 0; i < count; i++) 242: { 243: switch (pets[i]->GetType()) 244: { 245: case Pet::dog: 246: delete (Dog*)pets[i]; 247: break; 248: case Pet::cat: 249: delete (Cat*)pets[i]; 250: break; 251: case Pet::fish: 252: delete (Fish*)pets[i]; 253: break; 254: } 255: } 256: }
Output
Pet type, [C]at, [D]og, [F]ish, [Q]uit: c What is your pet's name? Tibbles Does your cat scratch? no Pet type, [C]at, [D]og, [F]ish, [Q]uit: dog What is your pet's name? Ross Does your dog bark? Yes Pet type, [C]at, [D]og, [F]ish, [Q]uit: f What is your pet's name? Bubbles Is your fish tropical? n Pet type, [C]at, [D]og, [F]ish, [Q]uit: q Pet Names Tibbles Ross Bubbles Characteristics Cat: Tibbles Scratches: No Dog: Ross Barks: Yes Fish: Bubbles Water: Cold You can't take a goldfish for walks
Analysis
This program makes use of the common base class Pet, declared in lines 13 through 36, to make the basic operations required for the pet handling. Most of its functions—except Query() defined in line 38—are inlined (declared within the class)
because they are small and simple.
The class uses an enumerated constant, defined in line 16, to record the allowed derived types. A Visual C++ purist would frown on this because it shows that you need to know what types you are going to derive from the class. This makes the class
subject to amendment in the future.
The designer of the Pet class has decided that the class is not meant to be used on its own. To stop anyone from using a class, the constructor is made protected. This means that only a derived class can call it. Any attempt to declare a Pet object will
result in a compile-time error. The constructor simply takes the class type, stores it, and terminates the name storage to ensure that nothing nasty happens if a program tries to print the name before a value is set.
The three derived classes all work in a similar way. Let's look at the Cat class in detail. Line 47 shows C++ that a Cat is a Pet. That is a very important sentence. When you make a class, should it have another class as a member or be derived? If you
can say "class a is a class b," you can derive a new class. If you say "class a has a class b," you should not derive but simply make the class a member of the new class.
Lines 50 and 51 show C++ how to make a Cat. Cat has a constructor that is used to initialize its own private member. But this is defaulted, so this constructor can do double time as a default constructor. In line 51, it calls the base class constructor
to set the class type of Pet. This setting is very important to the correct working of the classes, so only derived classes are allowed to get at the constructor. Protected objects can be accessed by derived classes.
Lines 55 and 56 tell us that Cat has two functions that replace the base Pet functions. In lines 61 through 67, the way the Cat::Print() works is defined. In line 64, it calls the base class Print() to get the base class information. The base class
function did not need to be called Print() to be called from the derived class function. In fact, you could argue that the two functions do something different and that it would have been clearer if the base class function was called PrintName() instead.
Jumping down to main() in lines 163 to 256, this program simply loops until the user has finished putting in the list. (A more professional programmer would have made sure the array bounds of pets were not exceeded!)
The test to stop the loop in lines 177 and 178 can't make use of the switch statement because the break would only exit the switch statement and not the complete loop.
All of the pets are stored in a single array. This array has to be declared as the common base type to be able to store the pointers. This can be trouble because when a particular class is needed, the special features can't be accessed. The calling type
(that is, the type of the array) is used by default. To work around this, casts must be used to make the calling type into the right type. In line 186, a tricky cast expression is used. Recall that you saw that casting has a low precedence. You need to use
two sets of parentheses—the inner set around the type name to represent the cast operator, and a second set around the expression for which you need to change the type. So the second closing parenthesis has to be placed where the Pet pointer, not the
array itself, is retrieved from the array.
To do the same thing with Dog, this time you can plan ahead and make a temporary Dog pointer. Now the calls can be made without any casting, and the Dog pointer can simply be assigned into the pets array at the end of any special processing.
Just to drive the point home, the Fish method explicitly makes a Fish pointer. This is a waste of code because you have to make the Fish pointer using a cast on the array; the type was lost on assigning the pointer into the array. Out of the three
methods, the Dog method is the best.
The lines 207 and 209 ensure that the store is correctly incremented. In case of an error, the counter is decremented to correct the following increment.
The two reports at the end have different requirements. The first report, simply lists the names. Because this is all held in the Pet class, there is no need to do any casting. All the objects are treated as Pet objects, as in line 216.
In line 224, the special Pet function to decide what sort of class is held is needed. Then, in each case, the casting method is used to get the right information out.
You can see how careful you need to be with the typing, because even the delete needs special care. The correct delete is called by using the casting operator again.
Pet * p = new Pet(Pet::cat); ((Cat*)p->Print();
class A { int a; void Print() { cout << a; } }; void main() { A a; a.Print(); }
Pet* p = new Cat; ((Dog*)p->Print();