CSCI 2300 Data Structures and Algorithms Spring 2000

Lab 8 Virtual Functions and Polymorphisms

In CS II you were introduced to the notion of a class hierarchy, where new object types are created from existing object types through inheritance. Classes may be organized into a logical, tree-like structure according to functionality, with the most general classes at the top of the tree. Common illustrative examples include class hierarchies of bank account types, geometric figures, and zoo animals. Within such a hierarchy the classes will have common functionality, but often different implementations. For example, associated with a geometric figure is the notion of an area and perhaps a volume. The way these are calculated depends on the type of figure; there are different ways of calculating area for a circle, a rectangle, and a sphere. Therefore each figure class has its own mechanism for calculating area. On the other hand, we might want to create a list containing a variety of different figure objects , and we might want to know the areas of each of the objects or sort them by area. Doing this requires sending a message (making a function call) to each object and having it compute its area. This is done correctly through the use of virtual functions. The list of different figures is a polymorphic list.

To explore the notions of virtual functions and polymorphism, work through the following steps, in order:

  1. Copy the lab files. You will need to modify the files in the lab, so first make your own copies. Copy the following files to a convenient location. Use Netscape or Explorer to do this. Only add the first three to your project.

  2. Start up Developer Studio. As in lab 1, create a workspace and then a project (make sure it is a "Win32 Console Application") that incorporates these files.

  3. Read through the code and make a diagram of the figure class hierarchy. At this point, the code and the main program are very simple. Be sure to note the use of the keyword protected instead of private for the member variables in each class. This allows derived classes (e.g. square) to have access to their base class member variables.

    Next examine carefully the constructor for square. The first step in any constructor, before executing the function body (between the "{" "}"), is a call to the base class constructor, if there is a base class. If not specified explicitly, as shown here, this call is done implicitly. (It can't be prevented!) In this case, an implicit call would be cause a compiler error because there is no "default constructor" for rectangle. Try it (delete the explicit call) and see!

    Also, compare the constructors for circle and sphere. Each accomplishes the same effect - initialization of member variables - but in subtly different ways. The sphere constructor does this before the body of the constructor. This is the preferred method, both in terms of style and in terms of efficiency.

    The main program is simple, just dynamically allocating a few objects. It will be discussed in more detail below.

  4. Build and execute the program What happened? Step through the program using the debugger to try to figure out exactly what function calls were made.

  5. Here's an explanation and solution. The program dynamically allocated various figure objects and pointed to each using a Figure* pointer. When it made the function calls during execution, it called the appropriate functions for Figure objects because that's the type of each pointer. One way to get things working properly would be to make the pointer type match the object type. For example, making a Circle* pointer for a Circle object. Go ahead and try it on one of the pointers.

    This isn't the desired solution, however. If we need to keep a list of different objects, we can't be identifying them with different pointers. Instead, we have to slightly change the class hierarchy. In particular, we need to declare the Output_Info, Area and Volume functions as virtual. For now, do this only for the Output_Info function in figure.h. In the declaration of the base class Figure precede the declaration of Output_Info with the keyword virtual:

           virtual void Output_Info( );
    
    This makes the Output_Info function in all derived classes virtual as well.
    Note: stylistically, it is better to explicitly name the functions in the derived class as virtual, even though this is redundant.
    If a function is virtual then the actual function called is determined at run-time (i.e. when the program is executed) according to the type of objected pointed to. By contrast when a function is non-virtual, the actual function called is determined at compile time (when the program is compiled) according to the pointer type.

  6. After making Output_Info virtual, build and execute the program. Note the difference between the results of the calls to Output_Info and Area.

  7. To illustrate another point about virtual functions, change the function Area to be virtual in Rectangle. Now compile and run the program again. Observe that the change had no apparent effect. The reason is that the function must be virtual for the pointer type. If you make Area virtual in Figure, everything will be fine. Do this for Area and for Volume.

  8. Before continuing with the rest of the lab, which involves more coding, think about the base class, Figure. It doesn't make sense to create an object (as opposed to a pointer) of type Figure. To reflect this we can make Figure an "abstract base class", by making one it is functions "pure virtual". In particular this is accomplished with the somewhat obscure notation
           virtual void Output_Info( ) = 0;
    
    Do this in figure.h and remove the corresponding function from figure.C. Then, recompile. There will be errors in the main program that you need to fix by deleting lines.
    Note: derived classes will be concrete (as opposed to abstract) when all pure virtual functions are redefined. Only objects of concrete classes can be created.

  9. Now that you have explored the basic mechanisms of virtual functions and polymorphism, it is time to add to the class hierarchy. Create a Cylinder class and make it derived from Figure. It should redefine all three virtual member functions. The Cylinder member variables should include a Circle object and should use the Area function from Circle in computing both the area and the volume. In order to do this, some additions to the Circle class will be necessary. Compile and test your program.
    Note: the implementation might have been simpler if we had made Cylinder a derived class of Circle. This isn't a great idea, however, because it runs counter to the notion of inheritance: a cylinder is not a specialization of a circle; rather a cylinder has a circle as part of its physical structure. The distinction between "is-a" and "has-a" is the primary criteria used in choosing between making class A a derived class of B and making A use an object of type B as a member variable.

  10. Delete the current main program main.cpp from your project and then add mainvect.cpp to your project. To delete from a project, in the "workspace", which usually in the left panel of the visual studio, click on "FileView", click on main.cpp, and then under the "Edit" menu click delete. This removes the file from the project but not the file system. Add mainvect.cpp to the project in the usual way.

    Examine this file, mainvect.cpp. You will notice the use of the standard library container class vector and its member functions (push_back), along with "iterators" and generic functions such as sort. A vector is in essence a dynamically resized array. It contains all the functionality of arrays without having to fix the size in advance. The namespace prefix std:: is required as part of C++ standard.

    We will explore standard library container classes in more detail in labs 4 and 5. You should have also seen them in CS II. They were (and still are) previously described as being part of STL, the "Standard Template Library". They are now in the C++ standard and may simply be thought of as being part of the standard library.

    The standard library (STL) also includes generic functions such as sort. These functions work with iterators, so they may be used with a variety of container classes (including plain old arrays!). The sort function takes either two or three arguments. The first two are the beginning and ending iterators. The last is the comparison function. If this function is not specified the overload operator< operator is used on the type stored. Since the type stored in this case is a pointer and since we can't overload this operator on a pointer, we must provide a function explicitly.

  11. Build and execute the program Try to step through the program with the debugger to see what useful information you can gather.

  12. Add a function to the program to sort the vector of figure pointers by volume and then output only those figures with non-zero volumes. Build and execute your program, fixing any bugs.


At the conclusion of this lab, take a moment to review the following:

  • You should have recalled the notions of class hierarchies and inheritance.
  • You have been introduced to and explored virtual functions and polymorphism. Think about both the intuitions as to why this is important and the mechanisms that make it work. The latter may seem tricky until you have more experience, and are often a source of errors in programs.
  • You have been introduced to the sometimes subtle distinction between "is-a" and "has-a" - the choice between gaining access to a class through inheritance or though a member variable. As a general rule, inheritance ("is-a") should be reserved for specialization.
  • You have been (re-)introduced to the vector class from the standard library and the notions of iterators and generic functions. These are covered in more detail in recent C++ text books and in the optional text STL Tutorial and Reference Guide: C++ Programming with the Standard Template Library. These will be covered in more detail in labs 4 and 5.

  • Last updated: 3 Sept 1998 by stewart@cs.rpi.edu