Inheritance - Virtual Functions and Abstract Classes
 

A Motivational Example
 
In the student example given in the previous notes set, we may end up with many sub-categories of Students, which translates into many derived classes with different information in each.  This also means that we will probably have many different types of Grade Reports.  It would really be nice to be able to store one big list of Students, and then quickly print out ALL grade reports with a loop, like this:

  Student list[30000]; 
  .... 
  for (int i = 0; i < size; i++) 
     list[i].GradeReport(); 

However, this code is problematic, for two reasons:

  1. The items in the array above are Student objects, and Student is the base class.  These base class objects don't have the full range of information about the sub-types.  Furthermore, we can't store all these different subtypes (like Grads and Undergrads) physically in an array, because everything in an array must be the same type (and more importantly, the same size)!
  2. The GradeReport function being called is the Student class version of it, since we are calling through objects declared as type Student.  This is also not the desired call.  If the Student is a Grad, we want to call Grad's version.  If the Student is an Undergrad, we want to call Undergrad's version.

One solution, of course, would be to create a separate array for each subtype.  While this is reasonable with very small examples, it is not realistic when there are many subtypes. Real world situations are always larger and more complex than small textbook examples!

A base class pointer property:

We can get around the first problem through a special property of inheritance. Normally, a pointer can only point at one type -- the type that's used in the pointer declaration. However, there is a special rule for inheritance:


Examples:

 Student s; 
 Grad g; 
 Undergrad u; 

 Student *sp1, *sp2, *sp3;    // Student pointers 

 sp1 = &s;    // sp1 is now pointing at Student object 
 sp2 = &g;    // sp2 is now pointing at Grad object 
 sp3 = &u;    // sp3 is now pointing at Undergrad object 

Heterogeneous List:

We will create what is called a heterogeneous list by creating a single array of pointers to a base class. These pointers can be pointed to any objects derived from that base. So, we have essentially created a list of various types of objects, without breaking the array rule of same types. Everything in the array is the same type -- a pointer to the base class!

 Student * list[30000];   // an array of Student pointers 

 list[0] = &g;             // using the earlier Grad object 
 list[1] = &u;             // undergrad object 
 list[2] = new Grad;      // dynamic Grad object 

 // we can continue adding items to the array, of any subtype of Student 

Virtual Functions:

The heterogeneous list solves our first problem, but what about the second? Whose version of GradeReport will run? In the above example, we might try loading up the array, then saying:

 for (int i = 0; i < size; i++) 
    list[i]->GradeReport(); 

However, this still calls the Student version of GradeReport, since list[i] is declared as a Student pointer. The problem is due to a concept known as binding.  The function call is bound to its definition, normally, by the compiler.  This means it is static.  Since the compiler cannot possibly know exactly what we will be pointing to with these Student pointers when the program runs, it cannot guess which version we really intend (Grad, Undergrad, etc). Its assumption will be to use the type from the pointer declaration (Student *). So, we need some way of making this binding occur at run time (i.e. dynamic).

The keyword virtual will give do this. To override a base class function when it is called through a pointer, so that it will instead run the target's version of the function, declare the base function to be virtual.

 class Student 
 { 
   public: 
      virtual void GradeReport(); 
   ... 
 }; 

Now, when GradeReport is called through a base class pointer, the program will look at the target of the pointer and run the appropriate version of GradeReport:

 Student *sp1, *sp2, *sp3; 
 Student s;    Grad g;     Undergrad u; 
 sp1 = &s; 
 sp2 = &g; 
 sp3 = &u; 

 sp1->GradeReport();    // runs Student's version 
 sp2->GradeReport();    // runs Grad's version 
 sp3->GradeReport();    // runs Undergrad's version 

So, our loop will now do what we want. With the heterogeneous list, and the use of virtual functions, we can put all Students in one list, and print all grade reports this way:

 for (int i = 0; i < size; i++) 
    list[i]->GradeReport(); 
Note: Virtual functions are not specific to this example of the heterogeneous list. This is simply a nice example that helps motivate the topic with an easy and common application of virtual functions. Virtual functions can be used any time that we want to call a function through a pointer (to a base class type) that is pointing at a derived object.
 

Abstract Classes

In some situations, the base class is so abstract that we really don't want to build objects out of it. For instance, class Student might not have enough information for the full grade reports -- we need to know what kind of student (i.e. which subtype). If you do not want to actually do anything in the Student's version of GradeReport, you are allowed to leave out its function definition altogether. Above, we had:

  virtual void GradeReport(); 

This function CAN be given its own definition, but if we desire that this virtual function be left un-implemented for the Student class itself (perhaps because there's nothing this version can do), then we can put "=0" on the function declaration. This tells the compiler that this function declaration will not have a corresponding definition.

 virtual void GradeReport()=0; 

Such a function is known as a pure virtual function. This is not required when making a function virtual, but it is often done when the base class version of the function really has no work of its own to do.

Any class that has at least one pure virtual function is known as an abstract class. An abstract class cannot be instantiated (i.e. you cannot build an object of this class type). Abstract classes are generally used as base classes for inheritance hierarchies, and they are intended only as a place to put data and functions common to classes derived from them, as well as a place for virtual functions. Abstract classes can still be used to build pointers, though, which is useful when taking advantage of virtual functions.

Example: Suppose we have an abstract class called Shape. Of the following statements, the first is not legal, and the second one is legal.

 Shape s;       // illegal declaration, since Shape is an abstract class
 Shape * sptr;  // legal declaration.  sptr may point to derived objects

Employee Example:

This example presents a hierarchy of derived classes that is based on a class called Employee.  The goal is a program that stores Employee information and handles the printing of their paychecks.  Note the use of the virtual PrintCheck function, and the building of the heterogeneous list in the sample main program.