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:
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
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
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.
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