Polymorphism is a fundamental concept in object-oriented programming (OOP) that allows objects to be treated as instances of their parent class. It enables a single interface to represent different underlying forms (data types). In C++, polymorphism can be achieved through two main mechanisms: compile-time polymorphism and runtime polymorphism.
Polymorphism is crucial in OOP because it promotes code reusability, flexibility, and scalability. By allowing objects of different classes to be treated as objects of a common superclass, polymorphism helps in writing more generic and maintainable code. This tutorial will explore both compile-time and runtime polymorphism, along with the concept of upcasting.
Compile-time polymorphism is achieved through function overloading and operator overloading. It occurs when multiple functions have the same name but different parameters within the same class or derived classes. The compiler decides which function to call at compile time based on the arguments passed.
Function overloading allows a class to have more than one function with the same name, as long as their parameter lists are different. This is useful when you want to perform similar operations in slightly different ways.
1#include <iostream>23class Calculator {4public:5int add(int a, int b) {6return a + b;7}89double add(double a, double b) {10return a + b;11}12};1314int main() {15Calculator calc;16std::cout << "Adding integers: " << calc.add(5, 3) << std::endl;17std::cout << "Adding doubles: " << calc.add(5.5, 3.2) << std::endl;18return 0;19}
Adding integers: 8 Adding doubles: 8.7
In this example, the Calculator class has two overloaded versions of the add function. The first version takes two integers and returns their sum, while the second version takes two doubles and returns their sum. The compiler determines which function to call based on the arguments passed.
Operator overloading allows you to redefine the behavior of operators for user-defined types. This can make your code more intuitive and easier to read.
1#include <iostream>23class Complex {4private:5double real;6double imag;78public:9Complex(double r = 0, double i = 0) : real(r), imag(i) {}1011Complex operator+(const Complex& other) const {12return Complex(real + other.real, imag + other.imag);13}1415void display() const {16std::cout << real << " + " << imag << "i" << std::endl;17}18};1920int main() {21Complex c1(3.0, 4.0);22Complex c2(1.5, 2.5);23Complex c3 = c1 + c2;24c3.display();25return 0;26}
4.5 + 6.5i
Here, the + operator is overloaded for the Complex class to add two complex numbers. The operator+ function takes another Complex object as a parameter and returns a new Complex object representing their sum.
Runtime polymorphism is achieved through function overriding. It occurs when a derived class provides a specific implementation of a function that is already defined in its base class. This allows the program to decide at runtime which function to call based on the actual object type.
Function overriding involves defining a function in the derived class with the same name, return type, and parameters as a function in the base class. The virtual keyword is used in the base class to indicate that the function can be overridden by derived classes.
1#include <iostream>23class Animal {4public:5virtual void speak() const {6std::cout << "Some generic animal sound" << std::endl;7}8};910class Dog : public Animal {11public:12void speak() const override {13std::cout << "Woof!" << std::endl;14}15};1617class Cat : public Animal {18public:19void speak() const override {20std::cout << "Meow!" << std::endl;21}22};2324int main() {25Animal* animal1 = new Dog();26Animal* animal2 = new Cat();2728animal1->speak(); // Outputs: Woof!29animal2->speak(); // Outputs: Meow!3031delete animal1;32delete animal2;3334return 0;35}
Woof! Meow!
In this example, the Animal class has a virtual function speak(). The Dog and Cat classes override this function to provide specific implementations. When the speak() method is called on pointers of type Animal, the actual object's type determines which version of the function is executed.
Upcasting is the process of converting a pointer or reference from a derived class to its base class. This is allowed in C++ because every derived class object can be treated as an object of its base class.
1#include <iostream>23class Base {4public:5virtual void show() const {6std::cout << "Base class show function" << std::endl;7}8};910class Derived : public Base {11public:12void show() const override {13std::cout << "Derived class show function" << std::endl;14}15};1617int main() {18Derived derivedObj;19Base* basePtr = &derivedObj; // Upcasting2021basePtr->show(); // Outputs: Derived class show function22return 0;23}
Derived class show function
In this example, a pointer to the Base class is assigned the address of a Derived object. This is an upcast because we are moving from a more specific type (Derived) to a more general type (Base). The virtual function show() in the base class ensures that the derived class's implementation is called.
Let's create a simple program that demonstrates both compile-time and runtime polymorphism, along with upcasting.
1#include <iostream>23class Shape {4public:5virtual void draw() const {6std::cout << "Drawing a shape" << std::endl;7}8};910class Circle : public Shape {11public:12void draw() const override {13std::cout << "Drawing a circle" << std::endl;14}15};1617class Square : public Shape {18public:19void draw() const override {20std::cout << "Drawing a square" << std::endl;21}22};2324void drawShape(Shape* shape) {25shape->draw();26}2728int main() {29Circle circle;30Square square;3132drawShape(&circle); // Outputs: Drawing a circle33drawShape(&square); // Outputs: Drawing a square3435Shape* basePtr = &circle; // Upcasting36basePtr->draw(); // Outputs: Drawing a circle3738return 0;39}
Drawing a circle Drawing a square Drawing a circle
In this program, the Shape class is the base class with a virtual function draw(). The Circle and Square classes override this function to provide specific implementations. The drawShape() function takes a pointer to a Shape object and calls its draw() method. This demonstrates runtime polymorphism. Additionally, upcasting is shown when a Circle object's address is assigned to a Shape* pointer.
| Concept | Description |
|---|---|
| Compile-time Polymorphism | Achieved through function overloading and operator overloading. Decided at compile time. |
| Runtime Polymorphism | Achieved through function overriding. Decided at runtime based on the actual object type. |
| Upcasting | Converting a pointer or reference from a derived class to its base class. |
In the next tutorial, we will delve deeper into function overriding, exploring more complex scenarios and best practices. Understanding function overriding is crucial for mastering runtime polymorphism in C++.