COMP2012H

Honors OOP and Data Structures

Lab 12: Virtual Functions

Download

 

There are several questions regarding virtual functions on this webpage. We encourage you to think before looking at the answers; therefore, the answers are hidden. You would need to highlight the whitespace following the questions to see the corresponding answers.

Virtual Functions

Polygon is the base class. Rectangle and Triangle are derived classes inherited from the base class.

no_virtual.cpp prints the area of a polygon without using virtual funtions. Note that rectangles and triangles are also polygons (is-a relationship). We encounter a problem in choosing the right functions to call since different polygons use different formulas to calculate the area. Specifically, when polygons[i]->area( ) is invoked, it does not work as it calls Polygon::area( ) instead of Rectangle::area( ) or Triangle::area( ).

To solve this problem, we need to use virtual functions.

When a virtual function is called, a mechanism called dynamic binding takes place. The function to call is determined by the actual class type of the object which invokes the function during program execution. This is different from static binding in which the function to call is determined statically during program compilation.

In virtual.cpp, the correct functions for calculating areas are called.

Questions

1. The destructor of a base class is usually declared as virtual. Why?  Consider the following code segment.

class A {
 
public:
 
    A() { a = new int[1000]; }
 
    ~A() { delete[] a; }  // not virtual destructor
 
private:
 
    int* a;
 
};
 
 
 
class B: public A {
 
public:
 
    B() { b = new char[1024]; }
 
    ~B() { delete [] b; }
 
private:
 
    char* b;
 
};
 
 
 
int main() {
 
    A * x = new B;
 
    // ...
 
    delete x;
 
}

What destructor is called by delete x?

The destructor of class A, since x is a pointer to class A! However, x is a class B object. Therefore if the constructor for class B allocate any memory dynamically (as in this case), they are not freed when x is deleted.

However, if the destructor of the base class A is virtual, the destructor of B will be called first (before A's destructor) when delete x is executed and this solves the problem.

2. What should the destructor of a derived class do regarding the memory dynamically allocated by its base class?

The destructor of the derived class should only free the memory it allocates dynamically. It should NOT free the memory that is allocated by its base class.

The destructor of the base class will get its turn after the derived class destructor finishes execution. (Do you remember the lecture notes on "order of construction"? Order of destruction is just its reverse!) There is no need to hurry. The base class can take care of itself.

In general, when we create a derived class, it is (and should be) unnecessary for us to know all the details of the base class. How memory is de-allocated in the base class belongs to the category that "derived class ignorable".

3. Is it possible that the following program outputs "In C::f()"?

#include <iostream>
 
using namespace std;
 
 
 
class A {
 
public:
 
    A() {}
 
    void f() {cout << "A::f()" << endl;}
 
};
 
 
 
class B: public A {
 
public:
 
    B() { }
 
    void f() { cout << "In B::f( )" << endl; }
 
};
 
 
 
class C: public B {
 
public:
 
    C() { }
 
    void f() { cout << "In C::f( )" << endl; }
 
};
 
 
 
int main() {
 
    B* x = new C;
 
    x->f();
 
    delete x;
 
    
 
    B* y = new B;
 
    y->f();
 
    delete y;
 
    return 0;
 
} 

The output is "In B::f()" and "In B::f()"!

The outputs are according to the following table:

 

In class A

void f()

virtual void f()

In class B

void f()

    B::f()

B::f()

    C::f()

B::f()

virtual void f()

    C::f()

B::f()

    C::f()

B::f()

Therefore, the output is "C::f()" and "B::f()" if f() is a virtual function in either class A and/or class B!  This is because if class A has a member function f() and declares it as virtual, f() becomes virtual for class B and class C since B derives from A. It does not matter at all if class B declare f() virtual or not.

 

Pure Virtual Functions

Have you noticed a problem in virtual.cpp? We need to define Polygon::area(), but it is impossible since we do not know the exact type of the polygon. Polygon::area() returns an invalid value (-1) in virtual.cpp.

The proper solution is to declare Polygon::area( ) as a pure virtual function. In this case, there is no need to provide an implementation for it. Therefore we can avoid the (dummy) implementation of Polygon::area( ) in virtual.cpp. Also, the area functions in the derived classes are called instead. The code becomes polygons.cpp.

The syntax of a pure virtual function is to add "= 0" after declaring it as virtual in the class definition.

Questions

1. You cannot compile polygons.cpp. Why?

The reason is the class Polygon is an abstract class. It is an abstract class because Polygon has one or more pure virtual function(s). You cannot create any instance of abstract classes. So, the problem is at line 11 in the main program (i.e. polygons[2] = new Polygon;). It tries to create an instance of Polygon dynamically. For class Rectangle and Triangle, they are allow to create instances. This is because they implement all pure virtual function(s) from the abstract base class already. Hence, they are NOT abstract classes anymore.  See the fixed version polygons_fixed.cpp.

2. What do you expect to be the program output of xyz.cpp?

This program cannot be compiled, since implementation of a pure virtual function char Z::l() const is still missing. Remember that a constant member function is DIFFERENT from a non-constant member function, even if they have the same name and same argument list! In other words, class Z has a new member function char l() but it has NOT implemented char l() const.

If we modify the code by adding "const" after char l() in class Z, the code can be compiled and gives the output:

In Z::g()
 
1 In Z:h()
 
1.2

Lab Task

In functions.zip, you are given a partially implemented program with polymorphic behavior according to the inheritance hierarchy below:

Your task is to implement all the functions of the program. You have to do the following:

1. Complete class ExpFunc in exp_func.h and exp_func.cpp, which implements the exponential function:

2. Complete global function CreateExpFunc(ostream& os, istream& is).

3. Complete SinFunc and CosFunc in tri_func.cpp and tri_func.h.

    We define CosFunc as follows.

  • Value: cosθ = sin(90°-θ) ;
  • Derivative: cosθ' = [sin(90°-θ)]' = (90°-θ)'sin'(90°-θ) = -sin'(90°-θ)

Note: the implementation of class CosFunc is restricted to be used the function of its parent but not the library cmath.

4. In main.cpp, Complete the block B in main fuction.

    Input a double value x. Check if func is periodic. Evaluate at x. Check if func is differentiable at x, if yes, derivative at x. (Hint: call the function Print)

To test your program, you can run it by entering the sample input in.txt and compare your program output with the correct output out.txt.

Note:

What is the purpose of the parameters in CreateExpFunc(ostream& os, istream& is)? Why don't we just use cin and cout directly in this function?

Answer: It is possible to use cin and cout directly in the function. However, your function will be limited to get data from standard-input (keyboard by default) and output to standard-output (your screen by default).

cin is a pre-defined object, and cin is a istream object. istream implements the concept of input stream. cout is a pre-defined object, and cout is a ostream object. ostream implements the concept of output stream.

In C++ I/O library, there is a file input stream ifstream which is derived from istream, and output file stream ofstream is derived from ostream. Hence you can call the function as follow:

ofstream fout("out.txt");
 
ifstream fin("in.txt");
 
Func* f = CreateExpFunc(fout, fin);