Choosing the Right Class

(polymorphism)



Section Contents

Topics should be read in order presented.

After clicking on a topic below, press "BACK" on your browser to return to Section Contents. Or, instead of jumping to topics and returning back to Section Contents, begin, rather, with the topic introduction to polymorphism as it appears immediately following Section Contents, and then advance sequentially through the remainder of the topics.



introduction to polymorphism:

As you may recall stated near the end of the previous section on inheritance, declaring classes as pointers is what makes possible yet another powerful feature of object-oriented programming called polymorphism. What makes inheritance of importance here is that polymorphism is not possible unless the use of inheritance is first established. Polymorphism is the third (and final) feature of object-oriented programming we will be studying that makes object-oriented programming ("OOP") what it is. What is "polymorphism"? Continue reading.

To start off this section, refer back, if you will, to the previous section on inheritance in which class Pet, class Dog, class Cat, and class Bird were used to demonstrate the 4 types of inheritance-based object messages. They are displayed once again below:

These 4 types of messages should be clearly understood. They apply to messages sent by regular class objects. What must be emphasized, however, is that these rules apply equally to messages sent by class pointers. However, an opportunity not available with regular class objects appears when class pointers are used. The primary means of fulfilling this opportunity, as we will learn, is by classes derived from a base class sharing a common member function appearing in the base class. To better understand how class pointers work, assume that you use a base class pointer to allocate memory for a derived class object, by means of new. An example of this would be pet = new Dog. Wait a minute, one may ask, if the class Dog data type is of a different class than the class Pet data type, how could we assign a class Pet pointer to a class Dog object? The answer is simple: because class Dog is a more specific (derived) version of the more generalized class Pet (the base class), the class Pet pointer acts as a sort of "generic" container within which more specific (derived) classes can be stored. With this is mind, take note that you cannot assign a derived class pointer to a base class object: dog = new Pet will not work. It is this "generic" property of base class pointers that makes polymorphism possible.

Let us further discuss this "generic" property of base class pointers. When a member function appears in a base class, classes derived from that base class have 2 choices: The first choice is for a derived class to redefine the base class member function (provide a new version of the function). The second choice, quite clearly, is for a derived class to simply inherit the base class member function (receive the function as it is): the base class member function automatically becomes part of the derived class. Moving along: let us assume that before us lies a base class named class Pet, and that 3 classes - class Dog, class Cat, and class Bird - are derived from class Pet. Let us further assume that class Pet contains a member function named GreetBoy( ). In one way or another (either by redefining the function or by simply inheriting the function), the 3 classes derived from class Pet will share a common member function named GreetBoy( ). Because each derived class contains its own version of GreetBoy( ), the derived class versions of GreetBoy( ) can be managed "generically" using a base class (class Pet) pointer.


type casting:

What, then, is the new opportunity made possible when class pointers (rather than regular class objects) are used to send messages? The new opportunity is called "type casting". As we've learned, all calls to a base class member function (such as class Pet), using a base class object, will result in a message to that particular base class member function (an example of such a member function being Pet::GreetBoy( ) ). To better understand type casting, let us return to the above assignment of the class Pet (base class) pointer to a class Dog (derived class) object mentioned earlier: pet = new Dog. How does "type casting" work? First assume that a base class member function, such as GreetBoy( ), is redefined in class Dog (or simply inherited by class Dog). When a class Pet pointer calls GreetBoy( ) in the presence of a "type cast", the derived class version of GreetBoy( ) is called, based upon the derived class type specified by the cast. The "type cast", in summary, specifies the derived class data type that a base class pointer is to assume.

Let us continue in our study of type casting: assume, firstly, once again, that a base class pointer is used to dynamically allocate memory for a derived class object, by means of new (as in pet = new Dog). Assume, secondly, that the derived class had declared in its definition a newly-introduced member function (a new member function unique to that derived class, not appearing in the base class). If type casting is properly applied to the base class pointer, the newly-introduced derived class member function will be called. An example of this would be:

( (Dog *)pet)->ChewBone();

Take note here that if the (Dog *) type cast were not placed in front of the base class pointer pet, an error would occur. To understand this, be firstly aware that ChewBone( ) does not exist in class Pet. If the program were to encounter the statement pet->ChewBone( ), quite simply, the absence of the (Dog *) type cast would result in the program thinking that ChewBone( ) is a member function of class Pet, which it is not. The (Dog *) type cast, put simply, tells the class Pet pointer which derived class type (in our case class Dog) to assume.

Displayed below is a program portraying the use of type casting:

#include "pet.h"
#include "dog.h"
#include "cat.h"
#include "bird.h"

void main()
{
   Pet *pet;         // class Pet (base class) pointer.
 
   pet = new Pet;    // memory allocated.
   pet->GreetBoy();  // calls Pet::GreetBoy() [base class
                     //  pointer; base class function]
   delete pet;   // memory deallocated.

   pet = new Dog;             // memory allocated.
   ( (Dog *)pet)->GreetBoy(); // calls Dog::GreetBoy() [base class
                              //  pointer; function redefined]
   delete (Dog *)pet;   // memory deallocated.

   pet = new Cat;             // memory allocated.
   ( (Cat *)pet)->GreetBoy(); // calls Cat::GreetBoy() [base class
                              //  pointer; function redefined]
   delete (Cat *)pet;   // memory deallocated.

   pet = new Bird;              // memory allocated.
   ( (Bird *)pet)->GreetBoy();  // calls Pet::GreetBoy() [base class
                                //  pointer; function inherited]
   delete (Bird *)pet;   // memory deallocated.
}

OUTPUT:

Pet said [Pet greeting!]
Dog said Bark!
Cat said Meow!
Pet said Chirp!

As before, the presence of the #include statements above main( ) prepare the program to be able to use class Pet and its derived classes.

Observe, if you will, that in the fourth line of the program output, "Pet" appears instead of "Bird". For what reason? Quite clearly, GreetBoy( ) appears in class Pet, but was not redefined in class Bird. class Bird simply inherits the class Pet version of GreetBoy( ): the class Pet version of GreetBoy( ) becomes the class Bird version of GreetBoy( ). Since the class Pet version of GreetBoy( ) now belongs to class Bird, applying the (Bird *) type cast to the class Pet pointer results in the class Pet version of GreetBoy( ) being called. However, in terms of inherited data members (in our case pet_greeting), class Bird's situation is no different than that of all of the other classes derived from class Pet: as you may recall from the previous section on inheritance, these classes do not declare their own versions of 'pet_greeting'.

Rather, all of the derived classes simply inherit the class Pet (base class) data member pet_greeting. What must be understood here is that each derived class receives its own unique copy of pet_greeting: the derived class possesses its own version of the data member, separate and apart from the base class version of the data member. What each derived class does with this inherited data member is completely up to the programmer who created the derived class. As one can see by examining the program displayed above, class Bird's decision concerning the data member pet_greeting was to place the text string "Chirp!" within it. Observe also the type casting performed upon the class Pet pointer in the second, third, and fourth delete statements. The purpose of the type casting in these statements, take note, is to ensure that the correct derived class destructor is called. The type casting here also makes sure that delete uses the correct derived class means of deallocating the base class pointer.

Let us move on. Displayed below is yet another program portraying the use of type casting:

#define DOG 1
#define CAT 2
#define BIRD 3

#include "pet.h"
#include "dog.h"
#include "cat.h"
#include "bird.h"

void main()
{
   Pet *pet;      // class Pet (base class) pointer
                   // declared.
   int class_type;     // derived class type.
    
   cout << "Enter class type [1-3] of pet:"
        << endl;
   cout << " 1 - dog" << endl;    // derived class
   cout << " 2 - cat" << endl;    // type inputted.
   cout << " 3 - bird" << endl;  //
   cin >> class_type;

   switch (class_type)
   {
      case DOG  : pet = new Dog;     // memory allocated.
                  break;

      case CAT  : pet = new Cat;     // memory allocated.
                  break;

      case BIRD : pet = new Bird;    // memory allocated.
                  break;
   }   

   switch (class_type)
   {
      case DOG  : ( (Dog *)pet)->GreetBoy();   // class Dog
                  break;                       // type cast.

      case CAT  : ( (Cat *)pet)->GreetBoy();   // class Cat
                  break;                       // type cast.

      case BIRD : ( (Bird *)pet)->GreetBoy();  // class Bird
                  break;                       // type cast.
   }

   switch (class_type)
   {
      case DOG  : delete (Dog *)pet;  // memory deallocated.
                  break;

      case CAT  : delete (Cat *)pet;  // memory deallocated.
                  break;

      case BIRD : delete (Bird *)pet; // memory deallocated.
                  break;
   }
}

As you can see, #define statements are made use of at the very beginning of the program. As you may already know, their purpose is to enable the computer to substitute one program symbol for another. As you will agree, DOG is more meaningful than 1, CAT is more meaningful than 2, and BIRD is more meaningful than 3. Next come the #include statements.

Quite clearly, the two programs displayed above and earlier portray the use of "type casting". The latter of the two presents a very awkward, unreliable means of using a base class pointer to represent a base class member function shared among its derived classes. It has been by some referred to as "nasty casting". What, then, is the logic behind "nasty casting"? One first declares a base class pointer. Next, by whatever means possible, the type of the derived class object to be stored in the base class pointer is determined. In our program, this is accomplished by inputting, from the user, using cin, the value of the variable 'class_type' - which holds a number from 1 to 3. 1 represents class Dog, 2 class Cat, and 3, class Bird. Here's the awkward part: in order to know which "type cast" to use to send a message through the base class pointer, a switch( ) statement must be used. We will learn how to use a base class pointer to call the correct derived class member function without the use of type casting when we are made familiar with polymorphism later in this section.

Let us discuss the first switch( ) statement in the above program. This switch( ) statement takes the variable 'class_type' as an argument; the purpose of the variable class_type here is to specify to the program the derived class type that the program will be dealing with. Then, based upon the value of class_type, 'case' options allocate memory, using the base class pointer, for the correct derived class, by means of new.

This brings us to the second switch( ) statement. Based upon the value held by class_type, a "message" is sent through the class Pet pointer in the form of the member function GreetBoy( ): the correct type cast is applied to the base class pointer, resulting in a call to the version of GreetBoy( ) appropriate to the situation. The version of GreetBoy( ) called is ultimately dependent upon the argument contained within the switch( ) statement.

The same logic also holds true for the third switch( ) statement in the program, in which delete is used upon the base class pointer: the value of class_type determines which type cast should be applied to the base class pointer (so that the correct derived class destructor is called), and so that delete uses the correct derived class means of deallocating the base class pointer. We will learn how to apply delete to base class pointers without the use of type casting when we are made familiar with virtual destructors later in this section.


more on polymorphism:

What if we wanted to derive a fourth new class from class Pet, such as class Mouse, without the use of polymorphism? This would involve modifying / updating the 3 nasty casting switch( ) statements much like the ones displayed above. The first step in doing so would be to add a new, fourth 'case' option to the 3 preexisting 'case' options already within the 3 nasty casting switch( ) statements.

Within the fourth 'case' option of the first switch( ) statement, a base class (class Pet) pointer is assigned to a class Mouse object by means of new, as in pet = new Mouse. Within the second switch( ) statement, the (Mouse *) type cast must be applied to the base class pointer so that the correct version of GreetBoy( ) is called. Within the third and final switch( ) statement, the (Mouse *) type cast must be applied to the base class pointer so that delete acts upon it in the appropriate manner.

After all of this work is done, the program must be recompiled. As you will agree, performing all of the tasks described here can be a tedious chore.

Does a more elegant solution exist? Yes: this brings us to the second, alternate method of adding a new derived class to a base class (the derived class being present among other classes derived from the base class). This solution exists by means of what is known as polymorphism. With polymorphism, a program can determine the derived class to which a common function (such as GreetBoy( ) ) belongs, without the use of "nasty casting".

The very first aspect of approaching polymorphism is to declare, as virtual, a member function appearing in a base class. Displayed below is the class definition of class Pet performing such a declaration:

class Pet
{
   public:
      Pet();
      virtual void GreetBoy();      // base class virtual function.
      virtual ~Pet();               // virtual destructor.

   protected:
      char *pet_greeting;       // class Pet data member.
};

You may have noticed that the destructor of class Pet, as with GreetBoy( ), is declared as virtual. We have yet to discuss, as stated, the concept of base class virtual destructors later in this section.

Let us discuss the virtual function we have come to call "GreetBoy( )". To "greet a boy" is a behavior common to all pets. Depending upon the type of the pet "greeting the boy", the outcome will be different: each pet will respond to the GreetBoy( ) message in its own unique way. It is a function shared by all of class Pet's derived classes. If a programmer wanted to introduce a new pet class to a program, such as class Mouse, he would provide a version of GreetBoy( ) within class Mouse that would "greet the boy" in the special way that we would expect a mouse to "greet a boy": "Mouse said Squeak!". The creator of class Mouse would perform this feat by how he redefines the base class (class Pet) virtual function version of GreetBoy( ). The class Mouse virtual function GreetBoy( ) having been introduced, it will fit nicely among the other versions of GreetBoy( ) (also virtual). What better way to represent a pet than how it "greets a boy"?

Although it is possible for a class derived from a base class to simply inherit a member function declared as virtual in that base class (in which the base class virtual function automatically becomes part of the derived class), when working with a member function declared as virtual in a base class, it is preferable, as it would happen, for a class derived from such a base class to redefine (provide a new version of) the base class member function declared as virtual. This is because if a derived class chooses not to redefine a base class virtual function, as is the case with class Bird, we have no idea how to "greet the boy", simply because we do not know what kind of pet is greeting the boy.

The use of virtual functions enables the programmer to introduce new classes into a program with the greatest of ease. Once a base class, such as class Pet, has been established, and a virtual function is declared within that base class, other programmers can then bring new types of Pet into the situation by simply providing their own version of that virtual function. This would be done by a programmer deriving a class from class Pet and then redefining the virtual function in the derived class (that function in our case being GreetBoy( ) ). In a very real sense, it is common knowledge to the those deriving new types of pet from class Pet that a pet is known by how it "greets a boy".


using inheritance with polymorphism:

Below is a class definition expressing how a virtual function appearing in a base class would be redefined in a derived class, in which class Mouse's inheritance of class Pet is specified. The class Mouse version of GreetBoy( ), like the class Pet version, is to be considered virtual:

class Mouse : public Pet        // Mouse inherits Pet.
{
   public:
      Mouse();
      virtual void GreetBoy();    // base class function
                                  // redefined as virtual.
      ~Mouse();
};

The programmer who is introducing class Mouse as a new derived class of class Pet would then write the bodies of the member functions contained within class Mouse:

Mouse::Mouse()
{
   int length;       // length of current string.
   
   // allocate memory for pet greeting.
   length = strlen("Squeak!");
   pet_greeting = new char[length + 1];
   strcpy(pet_greeting, "Squeak!");
}

void Mouse::GreetBoy()
{
   cout << "Mouse said " << pet_greeting
        << endl;
}

Mouse::~Mouse()
{
   delete [] pet_greeting;  // deallocate pet greeting.
}

When class Mouse inherits class Pet, it is understood that class Pet contains the member function GreetBoy( ) and has declared it as virtual. In the process of inheriting class Pet, class Mouse redefines GreetBoy( ) and also declares it as virtual. Assuming that GreetBoy( ) had been redefined in the other classes derived from class Pet and also declared as virtual in those classes, class Pet will treat the class Mouse version of GreetBoy( ) no differently than it would treat the version of GreetBoy( ) contained within class Dog, class Cat, or class Bird. To better understand the details of just how a "virtual function", so to speak, works, continue reading.


virtual function class definitions:

What is a "virtual function"? "virtual functions" are what make polymorphism possible. In a very real sense, "virtual functions" are the clear answer to the problem of "nasty casting". As the means of the first attempt to understand virtual functions, we will make certain modifications to the class Pet definition displayed in the previous section on inheritance, as well as to the class definitions of the classes derived from class Pet. These class definitions are presented below:

class Pet
{
   public:
      Pet();
      virtual void GreetBoy();      // base class virtual function.
      virtual ~Pet();               // virtual destructor.

   protected:
      char *pet_greeting;       // class Pet data member.
};

class Dog : public Pet          // Dog inherits Pet.
{
   public:
      Dog();
      virtual void GreetBoy();  // base class function
                                //  redefined as virtual.
      void ChewBone();          // new class Dog function.
      ~Dog();

   private:
      char *lunch_food;      // new class Dog
      bool pet_is_hungry;    // data members.
};

class Cat : public Pet       // Cat inherits Pet.
{
   public:
      Cat();
      virtual void GreetBoy();   // base class function
                                 // redefined as virtual.
      ~Cat();
};

class Bird : public Pet   // Bird inherits Pet.
{
   public:
      Bird();
      void EatSeeds();   // new class Bird function.
      ~Bird();

   private:
      char *lunch_food;     // new class Bird
      bool pet_is_hungry;   // data members.
};

class Mouse : public Pet        // Mouse inherits Pet.
{
   public:
      Mouse();
      virtual void GreetBoy();    // base class function
                                  // redefined as virtual.
      ~Mouse();
};

virtual functions:

In order to properly understand virtual functions, it is necessary that we return to our studies on "nasty casting". Let us assume, once again, that we have before us a base class named class Pet, and that 3 classes - class Dog, class Cat, and class Bird - are derived from class Pet. Furthermore, a member function - a regular nonvirtual function - named GreetBoy( ) appears in class Pet, and is redefined in all three derived classes. In this sense, all three derived classes share a common function. Because each derived class contains its own version of GreetBoy( ), the derived class versions of GreetBoy( ) can be managed "generically" using a base class (class Pet) pointer.

Let us next assume that we have declared a class Pet (base class) pointer. Let us afterward assume that a function call to GreetBoy( ), performed through the base class pointer, appears in program code: pet->GreetBoy( ).

Even though 3 classes have been derived from class Pet, all of which redefine GreetBoy( ), this function call still ends up calling the class Pet version of GreetBoy( ). Why shouldn't this version of GreetBoy( ) be called, one may reason, in that this is exactly what the function call - pet->GreetBoy( ) - appears to imply?

In order to demonstrate what we need to know, it is necessary that we determine the derived class (class Dog, class Cat, or class Bird) appropriate to the situation. Let us assume that we choose class Dog. Having done so, we place in program code the statement pet = new Dog. Next, we write the function call pet->GreetBoy( ), then compile and run the program. What happens? Even though we had assigned the class Pet pointer to a class Dog object, the function call still calls the class Pet version of GreetBoy( ). The base class pointer, you see, hasn't the slightest idea as to which derived class to use for the function call. This is nonvirtual behavior.

Along the lines of "nasty casting", then, we must apply type casting to the function call so that it agrees with the statement pet = new Dog (the base class pointer's assignment to a class Dog object). The function call would look something like this: ( (Dog *)pet)->GreetBoy( ).

This is a rather unreliable means of managing base class member functions redefined in its derived classes. With "nasty casting", you see, a base class pointer must be "reminded", in the form of switch( ) statements and 'case' options, of the derived class appropriate to the situation. The 'case' option chosen by the switch( ) statement, in turn, ultimately determines which version of GreetBoy( ) the program calls. This can be quite awkward.

This is where virtual functions come in. As before, GreetBoy( ) appears in class Pet, and is redefined in all three of its derived classes. What is different? What is new here is that in class Pet and its derived classes, GreetBoy( ) is declared as virtual.

Let us now discuss how virtual functions work. Once again, let us assume that the statement pet = new Dog appears in program code. Next comes the function call pet->GreetBoy( ). Then, we compile and run the program. What happens? This is where the "magic" comes in. After the program encounters the statement pet = new Dog and then arrives at the function call pet->GreetBoy( ), the class Dog version of GreetBoy( ) is called, rather than the class Pet version! This is polymorphism in action, and is an important aspect of polymorphism: giving multiple meanings to the same function call (that function call being pet->GreetBoy( ) ). Future function calls through the class Pet pointer need not be type cast.

It is because class Pet and its derived classes contain virtual functions, you see, that this behavior is made possible. What more do we need to know? With polymorphism, when a base class pointer is assigned to one of its derived classes by means of new and then the program encounters a virtual function call performed through that pointer, the base class pointer "remembers", so to speak, the derived class appropriate to the situation (the derived class specified by the new operator). In the base class pointer "remembering" this derived class, the correct version of GreetBoy( ) is called.

With virtual functions, you see, there is no "test" to determine the derived class type appropriate to the situation. All of the details are taken care of "ahead of time". There is no "juggling" or 'special treatment' required. In a nutshell, base class pointers need not be "reminded", or "told", in the form of switch( ) statements and 'case' options, as to the derived class appropriate to the situation. The program knows which derived class version of GreetBoy( ) to call, without the use of "nasty casting".

What are the precise details on how polymorphism works? What are the inner workings of how virtual functions fulfill their purpose? Exactly how do base class pointers "remember" the derived class type handed to them? This is all made possible by the process of what is called dynamic binding. We will discuss, in detail, the concepts behind dynamic binding later on in this section.


Note: when a function (such as GreetBoy( ) ) is declared as virtual in a base class and is redefined in a derived class, that function automatically becomes virtual in all of the classes derived from that base class. With this in mind, let me bring up a point concerning a programming practice of mine that you may have discovered already. Before discussing this "programming practice", consider the following: once a function is declared as virtual in a base class and is redefined in a derived class, there is no need to declare derived class versions of the function as virtual (within the class definition file of a derived class) when redefining the virtual function in a derived class. Quite clearly, the compiler does it for you.

So, when I say "redefine GreetBoy( ) in class Mouse and declare the function as virtual", I am doing so simply to make the material easier to understand. In summary: it is a good programming practice to label as virtual (within the class definition file of a derived class) a base class virtual function redefined in that derived class, even though doing so is not actually required. If a base class virtual function redefined in a derived class is not labeled as virtual within the class definition file of that derived class, you see, those reading the file have no means whatsoever of knowing whether or not the function is virtual.



virtual destructors:

What is the purpose of a virtual destructor? Let us return to the virtual function class definitions displayed earlier in this section. Within the definition of class Pet we find that the destructor of class Pet is declared as virtual: virtual ~Pet( ). Understanding how virtual destructors work does not require of us to go far beyond what we've covered on the topic of virtual functions. In review: let us assume that a virtual function appears in class Pet and its derived classes (in the form of the function GreetBoy( ) ). Let us further assume that in a program, a base class (class Pet) pointer is declared (as in Pet *pet), and that the statement pet = new Dog is written. After having encountered the statement pet = new Dog, when the program afterward comes across the function call pet->GreetBoy( ), the class Pet pointer "remembers", as we've discussed in detail, the derived class type of the object to which the pointer had been previously assigned by means of new, and the program calls the correct derived class (in our case class Dog) version of GreetBoy( ).

Things operate similarly with virtual destructors. Rather than ensuring that a base class (class Pet) pointer calls the correct derived class version of a virtual function appearing in that base class and its derived classes, however, a virtual destructor appearing in a base class ensures that the correct derived class destructor is called when delete is applied to a pointer of that base class (as in delete pet); the presence of a virtual destructor within a base class also ensures that delete uses the correct derived class means of deallocating a base class pointer. In other words, when a base class destructor is declared as virtual, applying delete to a pointer of that base class - whatever derived class type it is to which the base class pointer had been assigned (by means of new) - does not require the presence of a type cast. An example, in review, of such a usage of type casting could be delete (Dog *)pet. As with GreetBoy( ), the base class pointer "remembers" the appropriate way in which delete should behave, without the need for a type cast.

Sometimes, virtual destructors of a base class go unused. To designate this, the programmer simply leaves the body of the virtual destructor empty: an opening - { - and closing - } - brace with nothing in between, as in virtual ~Pet( ) { }.

Without the use of virtual destructors. and assuming that the statement pet = new Dog appears in a program, applying delete to the class Pet (base class) pointer (as in delete pet) would result in the pointer being treated as if it were a class Pet (rather than a class Dog) object, which could cause problems.


arrays of base class pointers:

When applying polymorphism to classes in a program, it is not uncommon to do so through the use of an array of base class pointers. Recall firstly, if you will, from earlier in this section, the emphasis on the "generic" property that a base class pointer possesses: a base class pointer can act as a sort of "generic" container within which more specific (derived) classes can be stored. A derived class is a more specific version of a more generalized (base) class. We then learned that when a base class pointer is used to send a "message" (in the form of a function call to a member function appearing in that base class and its derived classes), the base class pointer can be "type cast" to make the message call the correct derived class version of a member function appearing in that base class. However, next we encountered the concept of "nasty casting", which in itself brings forth many complications. As a whole, "nasty casting" (relying upon a switch( ) statement and its 'case' options to call the correct derived class version of a base class member function) should be seen as awkward and undesirable. Furthermore, using "nasty casting" with an array of base class pointers (rather than with a single base class pointer as demonstrated earlier) is even more of a difficulty.

We cannot fully envision the capabilities of an array of base class pointers until we have been made familiar with how polymorphism works. Let us discuss what we can concerning an array of base class pointers, then, in order to help us to be better prepared when the time comes to confront the topic of polymorphism. What is the advantage to using an array of base class pointers when applying polymorphism within a program? It allows a sort of "assembly line" approach to using classes. What is meant by "assembly line"? When an array of base class pointers is used to apply polymorphism in a program, each and every element in an array of base class pointers can hold an object of any of the classes derived from that base class. Therefore, an array of base class pointers can be said to be generic, as stated, as well as flexible. With this in mind, think of an array of base class pointers as an opportunity to accomplish a group of similar tasks. The member functions of the derived class objects held by the elements of an array of base class pointers could be said to represent similar tasks, for the simple reason that those objects are derived from a common base class.

Since the derived class objects held by the elements of an array of base class pointers have their roots in a common base class, it is most likely that those objects, because they are derived from a common base class (in our case class Pet), will contain member functions with the same name - and hence the same purpose - as that of a member function appearing in the base class. Such is the case with GreetBoy( ): GreetBoy( ) appears in class Pet. Therefore, because GreetBoy( ) appears in class Pet, each of the derived class objects held by the elements of the base class (class Pet) pointer array will possess their own versions of GreetBoy( ) as well. In its own way (either by redefining or by simply inheriting the class Pet version of GreetBoy( ) ), each derived class object in the base class (class Pet) pointer array will share a member function named GreetBoy( ).

Quite clearly, because the classes derived from class Pet all share a member function with the same name (that member function in our case being GreetBoy( ) ), the versions of GreetBoy( ) within those derived classes, are, therefore, as stated, centered around the same purpose. What this means, then, is that with polymorphism, any element in an array of class Pet pointers has the potential to serve as a kind of "generic" container within which an object of the desired derived class - this object being among objects of other classes derived from class Pet - can be stored (each derived class possessing its own version of GreetBoy( ) ). It is these generic, flexible aspects of an array of base class pointers that make possible an "assembly line" approach to managing a group of objects derived from a common base class. The same "generic" nature of base class pointers described here also holds true when applying delete to an element of a base class pointer array. Finally: an array of base class pointers is a very common means through which a programmer can apply virtual functions in a program.


an example of polymorphism:

Displayed below is a program portraying the use of polymorphism:

POLYPROG.CPP:

#define NUM_OBJ 4

#include "pet.h"
#include "dog.h"
#include "cat.h"
#include "bird.h"
#include "mouse.h"

void main()
{
   Pet *pet[NUM_OBJ];   // array of class Pet (base
                        // class) pointers declared.
   int index;        // current index of array element.

   pet[0] = new Dog;    // derived class objects "stored"
   pet[1] = new Cat;    // within an array of class Pet
   pet[2] = new Bird;   // (base class) pointers.
   pet[3] = new Mouse;  //

   // correct pet specified.
   cout << "Which pet?" << endl;
   cout << " 1 - Dog" << endl;
   cout << " 2 - Cat" << endl;
   cout << " 3 - Bird" << endl;
   cout << " 4 - Mouse" << endl;

   cin >> index;

   index -= 1;

   // situation managed using a single
   //  GreetBoy() message.
   pet[index]->GreetBoy();

   // situation managed using a single
   //  delete statement.
   delete pet[index];


   // a "walk" through base class pointer array - correct
   //  version of GreetBoy() message sent.
   for (index = 0; index < NUM_OBJ; index ++)
      pet[index]->GreetBoy();

   // a "walk" through base class pointer array -
   //  correct destructor called.
   for (index = 0; index < NUM_OBJ; index ++)
      delete pet[index];
}

OUTPUT:

number 1 chosen from menu:
Dog said Bark!

number 2 chosen from menu:
Cat said Meow!

number 3 chosen from menu:
Pet said Chirp!

number 4 chosen from menu:
Mouse said Squeak!

Dog said Bark!
Cat said Meow!
Pet said Chirp!
Mouse said Squeak!

Focus, if you will, upon the very first statement of this program: #define NUM_OBJ 4. It is not at all difficult to understand what is taking place here. Wherever the compiler finds "NUM_OBJ" in program code, it "replaces", so to speak, that occurrence of NUM_OBJ with 4. Making a single modification to the #define statement, you see, will apply to all places in program code where "NUM_OBJ" appears. As you may already have guessed, this statement informs the compiler as to how many derived classes will be used in the program.

Next come the #include statements. These statements, as stated, prepare the program to be able to use class Pet and its derived classes. In review: because these class definition files are surrounded by double quotes, they refer to classes created by a programmer. The other possibility is that such a file be surrounded by angle brackets, as in <stdio.h>. Angle brackets mean that a program is using a file within the C / C++ library of prewritten functions.

At the very beginning of "polyprog.cpp", an array of class Pet (base class) pointers is declared. Next, each element of the array is assigned to one of the classes derived from class Pet, by means of new. The next portion of the program is a good demonstration of the concept expressed by the title of this present current section: Choosing the Right Class. In this demonstration, the user is prompted to choose the pet appropriate to the situation. Afterward, the response from the menu - index - is converted to array notation by subtracting 1 from it (as you may already know, in C and C++, arrays begin at zero). Next, the correct element of the class Pet (base class) array is accessed based upon the value of index. This single message is the fulfillment of the class that the user had chosen. Upon arriving at this statement, the program "remembers", so to speak, the derived class type that the array element in the statement had been previously assigned to (by means of new). In a nutshell, the correct version of GreetBoy( ) is chosen. Afterward, the destructor of the correct derived class is called using a single delete statement. The delete statement also performs deallocation of the object. All of this is possible without the need for a type cast.

The next portion of the program consists of two for( ) loops. Each for( ) loop is a "walk" through the array of base class pointers, using the variable index for incrementation. Upon arrival at each value of index, a different element of the class Pet (base class) pointer array is accessed. Upon being accessed, the element "remembers" the derived class type that the element had been previously assigned to (by means of new), and then the appropriate action is taken. This "walk" through the base class pointer array could be thought of as being an "assembly line" approach to managing objects.

As you can see by examining the program, both "walks" through the elements of the base class pointer array are quite similar. Let us discuss how they are different. As you can see, upon each cycle through the first for( ) loop, a new element of the base class array is accessed, and the correct derived class version of GreetBoy( ) is called. Next comes the second "walk" through the base class array, also performed by means of a for( ) loop. This time, upon each new element of the base class array being accessed, delete is applied to that element, as in delete pet[index]. This accomplishes two things. Firstly, it ensures that the correct derived class destructor is called. Second and finally, it ensures that the correct derived class means of deallocating the base class pointer is performed. All of this is possible without the need for a type cast.

As you can see by observing "polyprog.cpp", gone is the nasty switch( ) statement. To send messages to the correct version of GreetBoy( ) (and for delete to behave as it should), all that is required is a for( ) loop. There is no need to "choose" between a list of possible derived class types (using a switch( ) statement) to determine which version of GreetBoy( ) to call, simply because each element in the array of base class pointers "remembers", as you may recall, the derived class type that the element had been assigned to (by means of new) - all of the details are taken care of "ahead of time", so to speak. There is no awkward "juggling". No 'special treatment' is required to walk through the class Pet pointer array - all that remains is easy, generic access to a flexible array of objects.


dynamic binding:

How does polymorphism work? How do base class pointers "remember" the derived class type to which they had been previously assigned? In order to further proceed on this topic we need to return to material we had covered within the previous section on inheritance.

In the previous section we were introduced to the term binding. In review: binding is the process by which a computer converts the bits and pieces of a program into sets of computer instructions and most importantly, afterward, into actual memory addresses. As it would happen, the concept of binding also applies to functions (hence the term function binding). This is no surprise, given the fact that, as we've learned, all functions lie at an address in memory in one way or another. In terms of dynamic binding, "function binding" is the process by which the version of a function to use for a function call is determined.

Two kinds of function binding exist. One form is called early (or static) binding. From now on we will call this static binding. The remaining form of function binding is called late (or dynamic) binding. From now on we will call this dynamic binding. The first program we will be studying, displayed below, demonstrates an example of static binding. This program is really self-explanatory.

class Base
{
   public:
      void Func() { cout << "Base::Func() called."; }
};

class Deriv : public Base
{
   public:
      void Func() { cout << "Deriv::Func() called."; }
};

void main()
{
   Base *base;

   base = new Deriv;
   base->Func();
}

OUTPUT:

Base::Func( ) called.

Before focusing our attention on this program it is necessary that we are introduced to two key concepts: compile-time and run-time. Compile-time is the period of time during which a computer converts C++ source code into machine language (ones and zeros that only a computer can understand). Run-time, on the other hand, is the period of time after which a compiled program has begun execution and is in the process of running.

The most important thing to bring up here, firstly, is that in class Base and class Deriv, the function Func( ) is nonvirtual: Func( ) is declared as a regular nonvirtual function. Unless otherwise stated, this is the kind of function you will come across. This having been made clear, let us study the program itself, one statement at a time.

COMPILE-TIME:

Base *base;
this is the declaration of the base class (class Base) pointer. the compiler allocates the space in memory necessary to hold the contents of the pointer.
base = new Deriv;
the base class pointer is assigned to a Deriv class object by means of new. however, nothing happens here during compile-time because the new operator does not take effect until run-time.
base->Func( );
because Func( ) is nonvirtual, it is, so to speak, "complete" at compile-time. that is, the function call is taken exactly how it appears in the written program code: the compiler associates the function call with the class Base version of Func( ). this resolution of the correct function to call during compile-time is known as static binding. worth mentioning is that although the compiler encounters this function call within written program code, it is not until run-time that the actual function call takes place. this is true, quite clearly, for all function calls.

RUN-TIME:

Base *base;
this was taken care of during compile-time.
base = new Deriv;
this allocates the space in memory, by means of new, necessary to hold the class Base pointer. however, the base class pointer does not "remember" its being assigned to a class Deriv object because Func( ) was declared as nonvirtual.
base->Func( );
as was determined at compile-time, this statement calls Base::Func( ).

Having completed our studies on static binding, let us now proceed to a program, displayed below, demonstrating an example of dynamic binding. Again, this program is really self-explanatory.

class Base
{
   public:
      virtual void Func() { cout << "Base::Func() called."; }
};

class Deriv : public Base
{
   public:
      virtual void Func() { cout << "Deriv::Func() called."; }
};

void main()
{
   Base *base;

   base = new Deriv;
   base->Func();
}

OUTPUT:

Deriv::Func( ) called.

The most important thing to bring up here, firstly, is that in class Base and class Deriv, the function Func( ) is declared as virtual. This is what sets the program displayed above on dynamic binding apart from the program we studied earlier on the concept of static binding. Let us study this second program one statement at a time.

COMPILE-TIME:

Base *base;
this is the declaration of the base class (class Base) pointer. the compiler allocates the space in memory necessary to hold the contents of the pointer.
base = new Deriv;
the base class pointer is assigned to a Deriv class object by means of new. however, nothing happens here during compile-time because the new operator does not take effect until run-time.
base->Func( );
one important point worth mentioning, as you may have noticed, is that this function call, in terms of actual written program code, is completely identical to the function call within the previous program on static binding. what is different? the difference is that the compiler treats virtual functions in a manner other than how it treats regular ordinary functions. this is one important aspect of polymorphism: giving multiple meanings to the same function call. let us continue. as we've learned, the new operator does not take effect until run-time. because this is so, the computer hasn't the slightest idea, during compile-time, as to which derived class to associate the function call with: the function call has the potential to be performed by means of any derived class. in this sense, the actual function that the function call should bring the program to is in a scrambled state. it is not until run-time that the new operator is acknowledged, and, in being acknowledged, the program hence knows the correct derived class version of Func( ) to call (through the base class pointer).

RUN-TIME:

Base *base;
this was taken care of during compile-time.
base = new Deriv;
the base class pointer is assigned to a Deriv class object by means of new. because class Base and class Deriv contain virtual functions (in this case Func( ) ), the base class pointer "remembers" the derived class type handed to it.
base->Func( );
now that the base class pointer has been handed to one of its derived classes by means of new, we are now ready to move on to the actual function call. it is by "remembering" the correct derived class type, you see, that this function call is made possible. because the program "remembers" the previous statement base = new Deriv, therefore, Deriv::Func( ) is called. exactly how does a base class pointer "remember" the derived class object handed to it (through the use of the new operator)? this is a key, core aspect of polymorphism. this act of "remembering" the derived class appropriate to the situation is carried out by means of what we were introduced to earlier as dynamic binding. like acknowledgement of the new operator, dynamic binding is a run-time occurrence.

In order to better understand dynamic binding, let us return, once again, to material we had covered within the previous section on inheritance.

In the previous section we were introduced to the concept of function pointers. This included a program demonstrating the use of function pointers to run a menu-driven system. Displayed below, once again, is that program:

void main()
{
   void (*Calc)(int, int);
   int method;
   int num1, num2;

   cout << "Enter method choice:"
        << endl;
   cout << " 1 - Add" << endl;
   cout << " 2 - Subtract" << endl;
   cout << " 3 - Multiply" << endl;
   cin >> method;

   cout << "Enter 2 numbers:"
        << endl;
   cin >> num1 >> num2;

   switch (method)
   {
      case 1 : (*Calc) = Add;
               break;

      case 2 : (*Calc) = Subtract;
               break;

      case 3 : (*Calc) = Multiply;
               break;
   }

   (*Calc)(num1, num2);
}

This program, as you may recall, was used to portray the concept of dynamic binding. In a nutshell, this program prompts the user to choose one of three methods of mathematical calculation, and then prompts the user to enter the 2 numbers upon which to perform the calculation. Finally, the calculation is performed, and the results are displayed.

Worth mentioning in the above program is a good programming practice to which we were introduced in the previous section. In review: when the asterisk operator is applied to a function pointer in program code when assigning the function pointer to a real written function (as in *Calc = Add), surrounding *Calc by parentheses (as in (*Calc) = Add) within the statement tells those reading a program that the type of pointer we are dealing with is in fact a function pointer (and not any other kind of pointer).

Of vital importance is the final line of code in the program: the function call (*Calc)(num1, num2). What makes this function call of importance is that the function this statement calls (through the function pointer (*Calc) ) is dependent upon the real written function to which (*Calc) had been assigned within the switch( ) statement. The outcome of the switch( ) statement, in turn, is determined by input from the user. Given this chain of reasoning, then, we can conclude that the function called through (*Calc) cannot be determined until after user input has occurred. User input, furthermore, is a run-time occurrence: user input, as you will agree, requires the presence of a running program. What we can conclude, then, is that because user input is a run-time occurrence, the function address held by (*Calc) cannot be known during compile-time. The necessity of a running program to determine the function address held by (*Calc) is an example of dynamic binding.

The content of the above program is of importance in that it is quite similar in structure to the inner workings of how base class pointers "remember" the derived class to which they are assigned by means of new (a concept we're quite familiar with). We will now study the precise details on how polymorphism and dynamic binding are related.

POLYMORPHISM AND DYNAMIC BINDING:

Dynamic binding is how polymorphism works: the process by which a program determines which version of a base class virtual function shared amongst derived classes to call, based upon the derived class type of the object through which the call is being performed.

Although it can be explained in a complicated fashion, there is nothing really complex or advanced about how polymorphism and dynamic binding are related. If you've understood everything up until now, grasping the concept should not be too difficult.

In fact, if you understood the program displayed above portraying a menu-driven system, you've already covered most of what you need to know concerning dynamic binding. What more do we need to know in being introduced to dymamic binding? In summary and as a whole, if you understood the concept of "nasty casting", and it is quite likely that you did, it is equally likely that you will understand the concept of dynamic binding, because, as it would happen and in a nutshell, dynamic binding does the nasty casting for you!

There are two aspects to dynamic binding: what happens at compile-time and what happens at run-time. We will cover compile-time first.

COMPILE-TIME:

  1. The compiler searches through all of the classes in a program to find classes that contain virtual functions.
  2. A special array of function pointers is constructed for such a class (the class in this case being class Dog).
  3. Each virtual function in such a class becomes an element in this array (the virtual function in this case being GreetBoy( ) ).
  4. In being placed in this array element, the address in memory of the virtual function is stored (in this case the array element would hold &Dog::GreetBoy( ) ).

All that need be mentioned here (that was not covered in the previous section on inheritance) is that function pointers, like all of the built-in data types of C++, can be expressed as an array. Worth bringing up, and in review, is that all functions lie at an address in memory in one way or another.

RUN-TIME:

  1. A class variable containing virtual functions is declared (the declaration in this case being pet = new Dog).
  2. A special pointer is created for the declared class variable whose purpose is to access elements of the special function pointer array constructed during compile-time. This pointer also informs the program as to which class the function pointer array belongs.
  3. The program encounters a virtual function call in program code (the virtual function call in this case being pet->GreetBoy( ) ).
  4. The program sets the special pointer to the called virtual function. The program then uses the special pointer to arrive at the correct array index, established at compile-time.
  5. The program uses this array index to find the appropriate array element. Having done so, a function call is performed to the function address held by the array element (the called function in this case being Dog::GreetBoy( ) ).

Displayed below is a graphical representation of how dynamic binding is carried out at run-time:

pet = new Dog;
|
V
pet->GreetBoy( );
|
V
Dog object
pointer set
|
V
class Dog
function pointer
array accessed
|
V
&Dog::GreetBoy( )
|
V
void Dog::GreetBoy()
{
   cout << "Dog said Bark!"
        << endl;
}

This graphical representation of what happens with dynamic binding during run-time should make things somewhat clearer. However, there is one important fact about dynamic binding, during both compile-time and run-time, that we need to know. In a nutshell, some of the instructions involved in the process of dynamic binding do not appear in written program code. What we will attempt to do next, then, is to describe, in everyday language, these "hidden" instructions, and, if possible, express them in C++ program code. Portions within square brackets mark "hidden" steps that are not visible in written program code.

COMPILE-TIME:

The primary occurrence concerning dynamic binding, during compile-time, is the construction of the special function pointer array, as well as the assignment of virtual function addresses to its elements. Although class Dog contains only one virtual function (GreetBoy( ) ), we can still think of the special function pointer array as a single-element array. The code would appear as follows:

[ compiler finds classes containing virtual functions ]

[ void (*Fptr[NUM_VFUNC])(); ]

[ (*Fptr[0]) = &Dog::GreetBoy; ]

The first line of code declares the function pointer array. The second and final line of code assigns the array element to the address of the class Dog version of GreetBoy( ).

RUN-TIME:

Let us now move on to run-time.

pet = new Dog;

[ special pointer created ]

pet->GreetBoy();

[ special_ptr = &Dog::GreetBoy; ]

[ begin convert virtual function to array index

switch (special_ptr)
{
   case GreetBoy:
      index = 0;
      break;
}

end convert virtual function to array index ]

[ (*Fptr[index])(); ]

Do not be surprised if some of these instructions look familiar. As was stated earlier, our familiarity with the program portraying a menu-driven system, as well as the concept of "nasty casting", contributed to our understanding of dynamic binding, in the form of the use of function pointers, switch( ) statements, and of the presence of class functions within case options. As with our program portraying a menu-driven system, for example, in the code shown above we first find a switch( ) statement being used to choose the correct function, followed by a function call to the function chosen by the switch( ) statement.

Let us advance through these instructions one piece at a time. First is the declaration of the class Dog object. Immediately afterward, the special pointer for the object is created. The function call appearing next is an example of polymorphism: the class Dog version of GreetBoy( ) is called, rather than the class Pet version. Next is where things get a bit advanced.

Next, in a nutshell, is where the program uses its knowledge of the called virtual function to arrive at the correct array index. Firstly, the special pointer is assigned to the called virtual function. Next, a switch( ) statement takes the special pointer as an argument. Finally, the switch( ) statement is used to "convert" this knowledge of the virtual function into the correct array index value.

The final line of code is really self-explanatory: where the actual function call to the virtual function takes place. As you can see, the array index value determined by the switch( ) statement is used to access the correct function pointer array element. The program having accessed this array element, a function call is performed to the address of the virtual function held by the element (the virtual function in this case being the class Dog version of GreetBoy( ) ).

This concludes our study on dynamic binding.


polymorphic loops:

In the previous discussion on how polymorphism works, we learned about the process of dynamic binding. Part of the study on the topic of how polymorphism works involved the importance of polymorphic assignment to performing virtual function calls. We also learned that polymorphic assignment and virtual function calls are run-time occurrences: they take place at run-time. Quite clearly, when a polymorphic assignment statement is encountered at compile-time, the compiler "puts off" acknowledging the statement until run-time: at compile-time, the derived class type a virtual function call is to assume is unknown. It is not until run-time that the correct version of a derived class virtual function can be determined. Up until now all virtual function calls in program code have gone through the process of "type checking" at compile-time. What does this mean? It means that the presence of virtual function calls has been ACKNOWLEDGED at compile-time. Why is this important? Continue reading.

Let us assume, once again, that before us lies a base class named class Pet; the virtual function (or possibly pure virtual function) GreetBoy( ) appears in class Pet, and each of class Pet's 3 derived classes - class Dog, class Cat, and class Bird - redefine GreetBoy( ). A polymorphic loop is a for( ) loop in program code which performs a "walk", so to speak, through an array of base class pointers that share a common virtual function; in our case that common virtual function is GreetBoy( ). Displayed below is an example of our polymorphic loop:

for (i = 0; i < 3; i ++)
   pet[i]->GreetBoy();

How is a polymorphic loop different from the virtual function calls we have been working with up until now? So far, virtual function calls in code have been acknowledged at compile-time. As stated, this means also that "type checking" is applied to these virtual function calls. Not so concerning polyporphic loops at compile-time. Like polymorphic assignment statements, polymorphic loops are not acknowledged until run-time. Why not? Quite clearly, the compiler has no means of being aware of what goes on inside of a for( ) loop at compile-time. In other words, a program does not actually enter a for( ) loop until run-time.

What does this mean? It means that a programmer can write a polymorphic loop without even knowing what the object types of the elements of the array will be. This is a powerful feature of applying polymorphic loops in a program. A program can "command a wide variety of objects to behave in a manner appropriate to those objects without even knowing the types of those objects". What is meant by "knowing" the types of such objects? For the purpose of things here and now we will assume "knowing" an object to mean declaring an object in program code. In our case such a declaration would be a polymorphic assignment statement, as in pet[0] = new Dog.

What is the advantage to not needing to know the object types of array elements in a polymorphic loop? By "known", as stated, we mean declared by means of a polymorphic assignment statement. The clear "advantage" here is that a polymorphic loop can successfully compile regardless of the fact that any of the elements in the base class pointer array may not have been declared previously in program code. Therefore, array elements in a polymorphic loop need not have been declared at compile-time. However, the declaration of array elements in a polymorphic loop is required at run-time, simply because a program does not begin cycling through a for( ) loop until run-time. In other words, declarations of all array elements within a polymorphic loop must have been performed before a program's final compile. The "final compile" of a program, in review, is the last time a program is compiled before the program is executed.

If the array elements in a polymorphic loop that had been declared by means of polymorphic assignment were declared using new - as in pet[0] = new Dog, somewhere later in the program - when the program is finished using the base class pointer array - there must exist a for( ) loop to go along with the polymorphic loop that performs a "walk" through the base class pointer array, and upon arriving at each element of the base class pointer array, applies delete to that element. Of course, this for ( ) loop is being written under the assumption that the destructor of the base class (in our case class Pet) had been declared as virtual. Declaring the base class (class Pet) destuctor as virtual ensures, firstly, that applying delete to the current base class pointer array element calls the correct derived class destructor. Declaring the base class destructor as virtual ensures secondly, that the correct derived class means of deallocating the current base class pointer array element is performed. Displayed below is what the destructor loop to go along with our polymorphic loop would look like:

for (i = 0; i < 3; i ++)
   delete pet[i];

Because for( ) loops do not begin cycling until run-time, a program can successfully compile without needing to know what goes on inside a polymorphic loop. The array elements of such a loop may or may not have been declared previously in program code. You see, a programmer can write a polymorphic loop without even knowing what the object types of the elements of the array will be: "programs can be written to process objects of types that may not exist when the program is under development". What should be further said of polymorphic loops? Applying a polymorphic loop in a program enables the loop to "reserve" a "slot" in the polymorphic loop for a class that is "under construction" - a class that has yet to be completed. Once the creator of such a class has "completed" his class, so to speak, the programmer then declares an object of the class within program code, and in doing so "fills in" the "slot" reserved for the class.

In review: a "polymorphic loop" is a for( ) loop that performs a "walk" through an array of base class pointers that share a common virtual function. In our case that common virtual function is GreetBoy( ). It is quite possible that the reason for a class being "under construction" is that the creator of the class is putting forth effort toward constructing his own version of GreetBoy( ) to contribute to a polymorphic loop (GreetBoy( ) being the common virtual function within the loop). On the other hand, it could be that a slot in a polymorphic loop is reserved for a class that the programmer is completely unaware of - a class that the programmer has yet to be made familiar with: "a class need not be complete or even exist for a program to successfully compile". In conclusion: at compile-time, the compiler has no means of being aware of what goes on inside of a polymorphic loop (a for( ) loop). A program does not actually enter a polymorphic loop (a for( ) loop) until run-time. Furthermore: at compile-time, array elements in a polymorphic loop need not have been declared previously in program code. However, at run-time, declarations of all array elements within a polymorphic loop must be provided.


adding new classes:

When a programmer introduces a base class into the situation, there is always the potential for other programmers to derive their own classes from that base class. Having derived a new class, it is up to the creator of that class to write the bodies of the member functions of the new class. Having written these member functions, the creator of the new class can then distribute them to other programmers. Having been given this new class (in the form of its member functions) such a programmer can put the new class to use in any way he wishes. Exactly how would the creator of such a new class "distribute", so to speak, the class? In order to answer this question it is necessary that we be introduced to a new term: the object file.

There is an issue concerning "object files" that is of vital importance here. As we learned within the section on classes, an object is a class variable: a "bringing into being" of a class. The object is then used to access public member functions of the class. To understand this new usage of the term "object", continue reading.

What we need to know, firstly, is that the file which contains the bodies of the member functions of a class is known as a class declaration file (which can also be refered to as a .cpp file). Once a programmer has finished writing the class declaration file of his class, he compiles it into an "object file".

An "object file" contains machine language: "ones" and "zeros" that only a computer can understand. As things go, an object file is how the creator of a class makes his class member functions available to other programmers. However, object files are not in an executable format. Most usually, rather, multiple object files are "combined" into a single, complete file that is the equivalent of a runnable program. This "combining" is called "linking", and the final program file is known as an "executable" (.exe) file. Like an object file, an executable file contains machine language. Unlike an object file, however, an executable file is in a runnable format.

What else to we need to know about "object files"? In general, a single programmer could be thought of being responsible for the construction of a class declaration file that he himself writes (and afterward compiles into an object file). In this sense, compiling class declaration files into object files enables programmers to create their classes independently of the work that creators of other classes may be involved in, making things simpler for the programmer. Quite clearly, an object file is not a finished product. Those who have created a complete class can "distribute" their object files to other programmers, and the network of object files combined ("linked") into an executable file by the programmer will fit together nicely.

One aspect of the use of object files worth mentioning is that by distributing a class in the form of an object file, the creator of that class need not give others access to the written source code of the class, enabling the private, inner workings of the class to be hidden from others.

Let us go through the basic steps involved in adding a new class to a program. Before doing so, let us lay down a few fundamental assumptions. We are first assuming that a base class named class Pet exists, and that 3 already-existing classes - class Dog, class Cat, and class Bird - are derived from class Pet. We are also assuming that these classes contain their own public member functions (the names not being important). We are further assuming that the class declaration files of these four classes have been compiled into object files. As our means of adding a new class to a program, we will derive a new class - class Mouse - from class Pet. Our final assumption is that the program being written takes place, of course, from within main( ) (main( ) being contained within the file "mainfile.cpp").

  1. class Mouse is derived from class Pet.
  2. The creator of class Mouse completes the class definition (.h) file of class Mouse.
  3. The creator of class Mouse writes the class declaration (.cpp) file of class Mouse.
  4. The class declaration file of class Mouse is compiled into an object file ("mouse.obj").
  5. The header (.h) and object (.obj) files of class Mouse are distributed to other programmers.
  6. The programmer places the statement #include "mouse.h" at the top of main( ).
  7. The programmer makes use of class Mouse, in the form of placing calls, in main( ), to class Mouse member functions (performed through a declared object).
  8. The programmer compiles main( ) into the object file "mainfile.obj".
  9. All object files (including that of main( ), class Pet, and its already-existing derived classes) are linked together to form "mainfile.exe".
  10. Worth mentioning is that the class declaration (.cpp) files of the already-existing classes that came before class Mouse need not be recompiled in preparation for the linking process. This powerful feature of C++ programming saves the programmer both time and effort.

Let us focus our attention upon step ten, which perhaps refers to the most vital concept concerning adding new classes to a program. This would involve returning to what we were introduced to as "nasty casting". As we learned, when a new 'case' option is added onto a switch( ) statement, the entire switch( ) statement must be recompiled. This includes, quite clearly, all of the 'case' options that were already part of the switch( ) statement. Having to recompile all already-existing 'case' options in such a fashion is unnecessary and awkward.

When we are working with object files, on the other hand, and a new class is added to a program, ONLY the newly-introduced class need be compiled (into an object file). All preexisting classes need not be recompiled (into an object file). This, quite clearly, lifts the burden off the shoulders of the programmer to have to awkwardly recompile all of the classes in a program every single time he uses the compiler to perform the linking process. No logic exists, as you will agree, to recompile code that has already been compiled.


classes under construction:

Let us assume that a program is in the process of being written. Let us further assume that this program uses a class that is incomplete (or even nonexistent). How do those writing the program manage the absence of such a class? This leads us to what could be called an important common programming practice.

When a programmer "distributes" his class to other programmers, it is common practice, as we've discussed, for such a programmer to provide the object file (compiled class declaration file) of the class. The purpose of an object file is to give those using a class access to the compiled member functions of that class. Equally so, when distributing his class, the programmer most usually provides the header file (class definition file) of the class: the file containing the function prototypes of the member functions of the class (as well as the data members of the class).

Let us assume, then, that a program is in the process of being written that uses an "incomplete" class: the object file of the class has not yet been provided. However, we are assuming that the creator of this class had provided the complete header file for the class. Such a file, so to speak, acts as a kind of "skeleton" for the class.

In order to move on it is required that we return to a concept to which we were introduced earlier: compile-time. "Compile-time", so to speak, and in review, is the period of time during which the computer converts a class declaration file (C++ program code of the member functions of a class) into an object file. Let us now discuss how this relates to a program, in the process of being written, deals with a class "under construction".

What we need to know concerning compile-time is that the bodies of functions (both nonvirtual and virtual) need not be provided in order for a program to successfully compile. Let us assume, then, that we have before us an "incomplete" class named class Cat and that it contains a single public member function named ChaseToy( ). How does the programmer writing "mainfile.cpp" (the file that we are assuming to contain main( ) ) handle the absence of class Cat?

What we are assuming, quite clearly, is that the creator of class Cat had provided the complete header file for class Cat. The first step before the writer of "mainfile.cpp", therefore, is to place the header file of class Cat at the top of "mainfile.cpp", as in #include "cat.h". The next step for the writer of "mainfile.cpp" to perform is to put class Cat to use within "mainfile.cpp". This would, in general, consist firstly of declaring a class Cat object (as in Cat cat), and then using the object to perform a function call class Cat's member function, as in cat.ChaseToy( ). Next and finally, the writer of "mainfile.cpp" writes the remaining code of his program.

What does this accomplish? It allows the programmer to concentrate upon writing his program, even though the object file of class Cat had not yet been provided. The programmer, you see, can compile his program (which is a normal part of writing a program), without having to "worry" about the body of ChaseToy( ) not having been provided, simply because function bodies are not required during the compile process.

Having made use of class Pet in "mainfile.cpp", and having completed writing the remainder of the code in his program, the next step before the programmer is to compile "mainfile.cpp" into an object file ("mainfile.obj"). However, the program is far from complete. In what way? In general, one would assume that compiling a program means that the program is in a finished state. Yet the object file of class Cat has yet to be provided.

What does all of this mean? The obvious conclusion is that the creator of class Cat can provide the object file of class Cat even after "mainfile.cpp" has been compiled! To better understand how this is so, continue reading. This powerful feature of C++ programming gives the creator of class Cat all the time he would ever need to come up with a complete object file, without the programmer having to delay compiling his program until class Cat was complete. This practice is not limited to the file that contains main( ), however: it is possible for the header file of an incomplete class to appear at the top of a class declaration file that uses the incomplete class.

After "mainfile.cpp" has been compiled into an object file, the creator of class Cat would finish writing the class declaration file ("cat.cpp") of class Cat, compile it to form the object file "cat.obj", and then give "cat.obj" to the programmer. The next step before the programmer would be to "link", so to speak, "mainfile.obj" and "cat.obj".

What is there about the "linking" process that we need to know? Unlike the "compile" process, as we've learned, in which the bodies of functions (nonvirtual and virtual) are not required, during the "linking" process, all functions require the presence of a body. How would this work?

Once the process of a group of object files being "linked" into an executable file has begun, the compiler searches through all of the object files that are part of the linking process for the body of every function (nonvirtual and virtual) it comes across (a possible function in this case being the class Cat member function ChaseToy( ) ). If the function body is found, the compiler continues the compile process undisturbed. If the linking process has completed and the function body has not been found, on the other hand, an error is reported. Such an error will not occur, quite clearly, since "cat.obj" has now been provided.

Things are similar with the main( ) function. Like class declaration files, main( ) can be compiled into an object file. In fact, doing so is quite necessary: if the linking process has completed and the compiler has not found the body of main( ), an error is reported. Equally so, if more than one main( ) is found during the linking process, an error is also reported.

Furthermore, on main( ): how does the compiler know what to name the executable file obtained from linking a group of object files? As it would happen, the compiler gives the executable file the name that was given to the file containing main( ). If the file containing main( ) was named "mainfile.cpp", the resulting executable file would be named "mainfile.exe".

In conclusion: as you will agree, by following the common programming practice described within the current present topic classes under construction, incomplete (or even nonexistent) classes can be added to a program with the greatest of ease. It is by the "linking" of object files, you see, that a program need not be recompiled in order to introduce a new class into the situation. The "linking" of a group of object files results in a final, complete executable (.exe) file, and so the programmer can then go about the process of running the file.


conclusion:

As we've learned, virtual functions - the means through which polymorphism works - make things both elegant and straightforward. The use of virtual functions, first of all, removes from the programmer the burden of "nasty casting" and of having to awkwardly modify switch( ) statements and introduce new type casts (so that the correct version of a common, shared derived class function can be called) whenever new derived classes are added to a preexisting base class. All affected code must be recompiled. Furthermore, virtual functions allow for a swift, clean "assembly line" treatment to managing arrays of base class pointers. It is not uncommon for such arrays to be the means through which virtual functions are applied in a program. Finally, but of equal importance, the use of polymorphism provides tremendous programming flexibility: because a group of derived classes share a function declared in a base class, that common function enables the programmer to introduce new classes into a program with the greatest of ease.


If you have any comments, questions, or feedback concerning C++ Tutorial, do not hesitate to e-mail me at jsfsite@yahoo.com. When done e-mailing, close out your e-mail program to return to this screen.

Having finished viewing C++ Tutorial, the next step is to close out this window (or Web browser tab). If you arrive at the C++ Tutorial opening screen, press the "BACK" button on your Web browser to return to the point where you left off.

to
previous
section
to
table of
contents


Comments, questions, feedback: jsfsite@yahoo.com