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. In static polymorphism, memory will be allocated at compile time. In dynamic polymorphism, memory will be allocated at run-time.
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. In the code section below the first function is called if the parameter is an int, the 2nd function is called if the parameter is a char, and the third if the parameters are two ints.
#include <iostream> using namespace std; class base { public: // function with int parameter void func(int x) { cout << "value of x is " << x << endl; } // function with char parameter void func(char x) { std::cout << "value of x is " <<x << " or "<<(int)x << endl; } // function with 2 int parameters void func(int x, int y) { cout << "value of x and y is " << x << ", " << y << endl; } }; int main() { base obj1; // Which function is called will depend on the parameters passed obj1.func(33); // The first 'func' is called obj1.func('!'); // The second 'func' is called obj1.func(33,66); // The third 'func' is called return 0; }
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.
Generally speaking operators in C++ can be classified into: unary and binary.
Overloading the Binary Addition Operator - the following code segment illustrates how to overload the binary operator by adding two class objects
#include<iostream> using namespace std; class area { private: int x,y; public: area(int a = 0, int b =0) {x = a; y = b;} //initialise the object's value // overloaded operator '+' area operator + (area const &rhs) { area returnobject; // Create an object to return returnobject.x = x + rhs.x; returnobject.y = y + rhs.y; return returnobject; } void print() { cout << x << " " << y << endl; } }; int main() { area c1(10, 5), c2(2, 4); //declare to area object and initialise area c3 = c1 + c2; // call to overloaded operator+ c3.print(); //output total area }
Overloading the unary increase and decrease operator - the following code segment illustrates how to overload the unary increase and decrease Operator
#include <iostream> using namespace std; class Overloaded { private: int count; public: Overloaded(): count(1){} void operator ++() //Define overloaded prefix increment operator { count = count+1; //increase count by 1 } void operator ++(int) //Define overloaded postfix increment operator { count = count+10; //increase count by 10 } void operator --() //Define overloaded prefix decrement operator { count = count -1; //decrease count by 1 } void operator --(int) //Define overloaded postfix decrement operator { count = count -10; //decrease count by 10 } void DisplayCount () { cout<<"Count: " << count << endl; } }; int main() { Overloaded value; ++value; //calls prefix increment operator value.DisplayCount(); value++; //calls postfix increment operator value.DisplayCount(); --value; //calls prefix decrement operator value.DisplayCount(); value--;//calls postfix decrement operator value.DisplayCount(); return 0; }
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: 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; }
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
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.