Previous Page TOC Next Page



- 21 -
Inheritance


public


What You'll Learn


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.

Making a New Class from an Existing Class




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.

Adding and Replacing Functions




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.

Initialization with Inheritance


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.

Input Listing 21.1. Wheels-U-Like hits the road.
  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.

Pointers to Classes




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.

Accessing Members When Using Inheritance


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.

Making Function Calls Faster


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.

Input Listing 21.2. Object-oriented pets.
  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.

Homework



General Knowledge


  1. How does inheritance help a C++ programmer?

  2. Which needs to be defined first, the base class or the derived class?

  3. How do you tell C++ which class you want another class to be made out of?

  4. What is the word used to describe a derived class function that replaces a base class function?

  5. How do you specify which version of a function you need when it appears in both the base and derived class?

  6. What happens if you don't specify which version of a function you use when calling a base function in an overloaded function?

  7. How do you call a base class constructor from a derived class constructor?

  8. How do you call a base class destructor from a derived class destructor?

  9. Which type determines which version of the function is called when a pointer is used to a derived object, the object type or the calling pointer type?

  10. Which type of access label needs to be used to allow derived classes to see members, but not allow users of the class to see members?

  11. True or False: When a class has been derived from, it can't be used in a program by itself.

  12. True or False: Member functions can be overloaded just like global functions.

  13. True or False: A private member function is available only to derived classes.

  14. True or False: delete must use the same class type as the matching new.

    What's the Output?


  15. Given the class definitions in Listing 21.2, what do the following output?

  16. Pet * p = new Cat(1);
    p->Print();

  17. Cat * c = new Cat(1);
    c->Print();

  18. Pet * p = new Cat(1);
    ((Cat*)p)->Print();

    Find the Bug


  19. Given the class definitions of Listing 21.2, why won't this compile?

    Pet * p = new Pet(Pet::cat);
    
     ((Cat*)p->Print();

  20. What's wrong here? What sort of complaint will the compiler make?

    class A
    
      {
    
        int a;
    
        void Print()
    
          {
    
             cout << a;
    
          }
    
      };
    
     void main()
    
      {
    
        A a;
    
        a.Print();
    
      }

  21. Ozwald hasn't got the hang of casts. Help him correct the following:

    Pet* p = new Cat;
    
    ((Dog*)p->Print();

    Write Code That. . .


  22. Class Bicycle is derived from class Transportation. Write a statement to change a pointer to a Bicycle into a pointer to Transportation.

  23. Class MotorCycle is also derived from class Transportation. Write a statement to change a MotorCycle pointer into a Bicycle pointer.

  24. Write a class declaration for Bicycle, MotorCycle, and Transportation containing a constructor, a destructor, a copy constructor, and a data member. Do not write the body of the code, but consider what access is appropriate to each member.

    Extra Credit


  25. Add function(s) to the program in Listing 21.2 to contain life expectancy of each type of pet.

  26. Add function(s) to the program in Listing 21.2 to set the number of lives each type of pet has and an output function to report the number lives. (Hint: Add two functions to cover the three classes for the input.)

Previous Page Page Top TOC Next Page