Your browser doesn't support JavaScript Polymorphism – Windows Programming

Polymorphism

Polymorphism refers to the ability to access different types of member functions depending on the type of object that invokes the function. Polymorphism can be static or dynamic. C++ supports compile-time polymorphism and run-time polymorphism.

With compile-time polymorphism, the compiler determines which function should be called. Function overloading and operator overloading are examples of compile-time polymorphism.

With run-time polymorphism, the function to be called is determined while the programme is running. This is commonly achieved using virtual functions and inheritance.

Compile-Time Polymorphism

This type of polymorphism involved either function or operator overloading

Function overloading

Function or method overloading is the ability to create multiple methods of the same name with different parameter implementations.  The compiler determines which function to call based on the arguments supplied.

In the example below, the first function is called when an int is supplied, the second when a char is supplied, and the third when two int values are supplied.

#include <iostream>

class Base
{
public:
    // Function with an int parameter
    void func(int x)
    {
        std::cout << "Value of x is " << x << std::endl;
    }

    // Function with a char parameter
    void func(char x)
    {
        std::cout << "Value of x is "
                  << x << " or "
                  << static_cast<int>(x) << std::endl;
    }

    // Function with two int parameters
    void func(int x, int y)
    {
        std::cout << "Value of x and y is "
                  << x << ", " << y << std::endl;
    }
};

int main()
{
    Base obj1;

    // Which function is called depends on the arguments supplied
    obj1.func(33);       // Calls the first func()
    obj1.func('!');      // Calls the second func()
    obj1.func(33, 66);   // Calls the third func()

    return 0;
}

The output is:

Value of x is 33
Value of x is ! or 33
Value of x and y is 33, 66

The three functions all have the same name, func, but their parameter lists are different. The compiler uses the arguments supplied in each function call to determine which version should be used.

The conversion from char to int in the second function is used to display the numerical character code for !.

Operator overloading

Operator overloading allows the programmer to define the behaviour of an operator for a particular class. Almost all operators in C++ can be overloaded. To create an overloaded operator, an operator function is created to define the action of the overloaded operator. The function name is preceded by the keyword “operator” and then the symbol for the operator is defined. An overloaded operator function can have a return type and a parameter list.

Operators are commonly classified according to the number of operands they use. A unary operator operates on one operand, while a binary operator operates on two operands.

Overloading the Binary Addition Operator

The following example overloads the binary addition operator (+) so that two Area objects can be added together.

#include <iostream>

class Area
{
private:
    int x;
    int y;

public:
    Area(int a = 0, int b = 0)
    {
        x = a;
        y = b;
    }

    // Overloaded + operator
    Area operator+(const Area& rhs) const
    {
        Area returnObject;

        returnObject.x = x + rhs.x;
        returnObject.y = y + rhs.y;

        return returnObject;
    }

    void Print() const
    {
        std::cout << x << " " << y << std::endl;
    }
};

int main()
{
    Area c1(10, 5);
    Area c2(2, 4);

    Area c3 = c1 + c2;

    c3.Print();

    return 0;
}

The output is: 12 9. The expression: c1 + c2 uses the overloaded + operator to add the corresponding x and y values from the two objects. In this example, it is equivalent to calling:c1.operator+(c2);.The resulting Area object is returned and used to initialise c3. The resulting Area object is returned and used to initialise c3.

Overloading the unary increase and decrease operator 

The increment (++) and decrement (--) operators are unary operators because they operate on a single object.

Both operators can be overloaded as either prefix or postfix operators.

With the prefix form, the operator appears before the object:

With the postfix form, the operator appears after the object:

When overloading these operators, C++ uses an int parameter to distinguish the postfix version from the prefix version. The parameter is not normally used.

The following example overloads both the increment and decrement operators:

#include <iostream>

class Overloaded
{
private:
    int count;

public:
    Overloaded() : count(1) {}

    // Prefix increment
    void operator++()
    {
        ++count;
    }

    // Postfix increment
    void operator++(int)
    {
        count++;
    }

    // Prefix decrement
    void operator--()
    {
        --count;
    }

    // Postfix decrement
    void operator--(int)
    {
        count--;
    }

    void DisplayCount() const
    {
        std::cout << "Count: " << count << std::endl;
    }
};

int main()
{
    Overloaded value;

    ++value;              // Calls prefix increment
    value.DisplayCount();

    value++;              // Calls postfix increment
    value.DisplayCount();

    --value;              // Calls prefix decrement
    value.DisplayCount();

    value--;              // Calls postfix decrement
    value.DisplayCount();

    return 0;
}

The output is:

Count: 2
Count: 3
Count: 2
Count: 1

The int parameter in the postfix versions is only used by the compiler to distinguish them from the prefix versions. It does not represent a value being passed to the operator.

In this example, both the prefix and postfix versions change count by one, which is the normal behaviour expected from ++ and --.

Dynamic or Run-Time Polymorphism

In contrast, with compile time polymorphism, the compiler determines which function call to bind to the object after deducing it at runtime. This allows different objects to respond differently to the same method call.

Function Overriding – When a derived class creates a member function with the same return type and signature as a member function in the base class it is said to be overriding that function. Since overriding the base class function will provide a new definition for that function, this allows instances of the derived class to create unique functionality based on the method calls.

In the example code below the base class and derived call are instantiated and the method func is then called from both. To invoke the overridden Methods of a Base Class use the scope resolution operator ( :: )

include<iostream>
using namespace std;
class Base
{
 public:

 void func()
 {
  cout << "Base class function call" << endl;
 }
};
class Derived:public Base
{
 public:
 
  void func()
 {
  cout << "Derived class function call" <<endl;
 }
};
 int main()
{
 Base b;       //Instantiate Base class
  Derived d;   //Instantiate Derived class object
 b.func();     //call func() from base class
 d.func();     //call func() from derived class
 d.Base::func();//call base call func() from derived class
}

Virtual function – A virtual function is a member function declared within a base class and re-defined and overridden by a derived class. When referring to a derived class object using a pointer or a reference to the base class, the derived class function can be called by declaring the function ‘virtual’. When a function is made virtual, C++ determines which function is to be invoked at the runtime based on the type of the object pointed to by the base class pointer and hence is known as dynamic linkage or late binding.  

The code sample below demonstrates both early and late binding –

#include<iostream>
using namespace std;
class Base
{
 public:
void virtual virtfunc()
 {
  cout << "Base Virtual Class" << endl;
 }
   void  func()
 {
  cout << "Base non virtual class" << endl;
 }
};
class Derived:public Base
{
 public:
 void virtfunc()
 {
  cout << "Derived Class" << endl;
 }
  void func()
 {
  cout << "Derived Class non virtual" <<endl;
 }
};
 int main()
{
Base *b;     //Instantiate base class pointer
Derived d;   //instantiate derived class 
b=&d;        //assign derived class object to a base class object
b->func();     // on-virtual function, binded at compile time
b->virtfunc(); // Virtual function, binded at runtime
}

Virtual Destructors

A virtual destructor ensures that when a derived subclass goes out of scope or is deleted the order of destruction of each class is carried out in the correct order. If the destruction order of the class objects is incorrect, it can lead to a memory leak because system resources are not de-allocated. When a pointer to a base class is assigned to a derived class object and that object is deleted, then the base class destructor will be called instead of the derived class destructor. To address this situation, the base class should be defined with a virtual destructor to ensure that the object of the derived class is destructed properly.

In the worked example below the base class ensures that objects are deallocated

#include <iostream>
using namespace std;
class Base {
public:
    Base() {
        cout << "Base Constructer" << endl;
    }
    virtual ~Base() // virtual destructor
    {
        cout << "Base Destructor" << endl;
    }
};
class Derived : public Base {
public:
    Derived() {
        cout << "Derived Constructor" << endl;
    }
    ~Derived() {
        cout << "Derived Destructor" << endl;
    }
};
void DeleteMemory(Base* pBase) {
    delete pBase;
}
int main() {
    cout << "Allocating a derived object on the free store:" << endl;
    Derived* pBase = new Derived;
    cout << "Deleting derived class " << endl;
    DeleteMemory(pBase);
    cout << "Instantiating a derived class on the stack:" << endl;
    Derived drv;
    cout << "Automatic destruction as it goes out of scope: " << endl;
    return 0;
}

Pure Virtual Functions and Abstract Classes

An abstract class is a class that contains at least one pure virtual function. The purpose of an abstract class is to provide a base class that other classes can inherit. Abstract classes cannot be instantiated directly; their purpose is to act as an interface for their subclasses. Attempting to instantiate an object of an abstract class causes a compilation error.  A pure virtual function is declared in the base class, has no body, and is assigned the value 0.

In the example below a pure virtual function is created and implemented in the base class. Without this implementation, the compiler will throw an error –

#include <iostream>
using namespace std;
class Base
{
public:
virtual void pvf() = 0;// pure virtual function
Base(){ //constructor
  cout << "pure virtual constructor called\n"; 
    }
};
// This class inherits from Base and implements method pvf()
class Derived: public Base
{
public:
    Derived(){ //constructor
        cout << "derived constructor called \n";       
    }
void pvf() //implements put virtual function 
{ 
}
};
int main(void)
{
Derived d;
return 0;
}

Characteristics of Abstract Class

  • An abstract class cannot be instantiated, but pointers and references of Abstract class type can be created.
  • An abstract class can have normal methods along with a pure virtual function.
  • Abstract classes are primarily used to provide an interface for any derived class.
  • Classes inheriting an Abstract Class must implement all pure virtual functions, or they will become Abstract too.