Polymorphism
Polymorphism is a core concept in Object-Oriented Programming (OOP) that refers to the ability of different objects to respond to the same function call in different ways. In C++, polymorphism allows methods to be defined in a base class and overridden in derived classes, enabling dynamic method invocation depending on the type of object being referenced. Polymorphism enhances flexibility and reusability in code, allowing for more generic and abstract programming.
Types of Polymorphism
Polymorphism in C++ can be broadly classified into two types:
- Compile-Time Polymorphism (Static Binding): The function to be invoked is determined at compile time. This includes function overloading and operator overloading.
- Run-Time Polymorphism (Dynamic Binding): The function to be invoked is determined at run time. This is achieved through function overriding and the use of virtual functions.
Compile-Time Polymorphism
Compile-time polymorphism is implemented through function overloading and operator overloading.
Function Overloading: Multiple functions can have the same name but different parameters. The correct function to be called is determined by the function signature.
class Print {
public:
void show(int i) {
cout << "Integer: " << i << endl;
}
void show(double d) {
cout << "Double: " << d << endl;
}
void show(string s) {
cout << "String: " << s << endl;
}
};
In this example, the show function is overloaded to handle different data types. The correct version of show is called depending on the argument passed.
Operator Overloading: C++ allows most operators to be overloaded so that they can work with user-defined data types.
class Complex {
private:
float real;
float imag;
public:
Complex() : real(0), imag(0) {}
Complex(float r, float i) : real(r), imag(i) {}
// Overloading the + operator
Complex operator + (const Complex& obj) {
Complex temp;
temp.real = real + obj.real;
temp.imag = imag + obj.imag;
return temp;
}
void display() {
cout << "Real: " << real << ", Imaginary: " << imag << endl;
}
};
In this example, the + operator is overloaded to add two Complex objects.
Run-Time Polymorphism
Run-time polymorphism is achieved through function overriding and the use of virtual functions.
Function Overriding: A derived class can provide a specific implementation for a function that is already defined in its base class.
class Animal {
public:
virtual void sound() {
cout << "This animal makes a sound." << endl;
}
};
class Dog : public Animal {
public:
void sound() override {
cout << "The dog barks." << endl;
}
};
class Cat : public Animal {
public:
void sound() override {
cout << "The cat meows." << endl;
}
};
In this example, the sound function is overridden in both Dog and Cat classes, each providing a specific implementation.
Virtual Functions: To achieve run-time polymorphism, the base class method that is intended to be overridden in derived classes is declared as virtual.
int main() {
Animal* animalPtr;
Dog myDog;
Cat myCat;
// Pointing to Dog object
animalPtr = &myDog;
animalPtr->sound(); // Outputs: The dog barks.
// Pointing to Cat object
animalPtr = &myCat;
animalPtr->sound(); // Outputs: The cat meows.
return 0;
}
In this code:
- animalPtr is a pointer of type Animal*.
- Despite being a base class pointer, it calls the sound method of the derived class (Dog or Cat), depending on the object it points to.
Pure Virtual Functions and Abstract Classes
A pure virtual function is a virtual function that has no definition in the base class and must be overridden in any derived class. A class that contains at least one pure virtual function is called an abstract class, and it cannot be instantiated.
class Shape {
public:
virtual void draw() = 0; // Pure virtual function
};
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing a circle." << endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
cout << "Drawing a rectangle." << endl;
}
};
In this example:
- Shape is an abstract class because it contains the pure virtual function draw().
- Both Circle and Rectangle must override the draw() method to be instantiable.
Virtual Destructors
When dealing with inheritance and polymorphism, it's important to ensure that destructors are declared as virtual in the base class to prevent resource leaks.
class Animal {
public:
virtual ~Animal() {
cout << "Animal destructor called." << endl;
}
};
class Dog : public Animal {
public:
~Dog() {
cout << "Dog destructor called." << endl;
}
};
int main() {
Animal* animalPtr = new Dog();
delete animalPtr; // Correctly calls Dog and then Animal destructors
return 0;
}
In this example, declaring the destructor in the base class (Animal) as virtual ensures that when deleting an object through a base class pointer, the destructor of the derived class (Dog) is called first, followed by the base class destructor.
Summary and Best Practices
Polymorphism is essential for writing flexible and reusable code. It allows the same interface to be used for different underlying data types. Compile-time polymorphism provides the benefits of overloading, while run-time polymorphism leverages inheritance and virtual functions to achieve dynamic behavior. When using polymorphism, always ensure proper use of virtual destructors to manage resources correctly.
This detailed course content should provide students with a strong understanding of polymorphism in C++, equipping them with the knowledge to apply these concepts effectively in their programming projects.