Polymorphism is a key concept in C++ that allows objects from different classes to be treated as objects of a shared base class. It makes your code more flexible and reusable. The term "polymorphism" stems from Greek words meaning "many forms."
Our goal in this lesson is to understand the basics of polymorphism in C++ by working with a chess example.
Polymorphism allows you to design and implement programs that are more flexible and easier to extend over time. It enables one interface to be used for a general class of actions, with specific actions determined by the exact nature of the situation.
In C++, polymorphism is primarily achieved through the use of base class pointers or references to invoke methods in derived classes. This makes it possible to write code that can work with objects of different types and classes in a unified way. Sounds difficult? It really isn't! Let's see an example, it will help you understand the idea.
An abstract class in C++ is a class that cannot be instantiated on its own and is designed to be inherited by other classes. It serves as a blueprint for derived classes. An abstract class typically includes one or more pure virtual functions.
A pure virtual function is a function declaration that is specified in the base class but has no implementation in that class. Instead, it must be implemented by any non-abstract derived class. This is done by assigning 0
to the function declaration in the base class.
Additionally, it's a good practice to have a virtual destructor in an abstract class. This ensures that the destructor for derived classes will be called correctly, preventing memory leaks.
We'll define an abstract class ChessPiece
with a pure virtual function draw
and a virtual destructor. This function will be overridden by derived classes.
C++1#include <iostream> 2 3class ChessPiece { 4public: 5 virtual ~ChessPiece() {} // Virtual destructor 6 virtual void draw() const = 0; // Pure virtual function 7};
Here, ChessPiece
serves as a blueprint for all chess pieces, and draw
is a method that each piece must implement.
Note that you can't create an instance of a class having at least one pure virtual function, because this class is abstract. For example, this is not allowed:
C++1int main() { 2 ChessPiece abstract_piece = new ChessPiece(); 3 abstract_piece->draw(); 4}
This code will cause an error.
Next, we'll create two derived classes, Pawn
and Knight
, which will implement the draw
method.
C++1class Pawn : public ChessPiece { 2public: 3 void draw() const override { 4 std::cout << "_Pawn_" << std::endl; 5 } 6}; 7 8class Knight : public ChessPiece { 9public: 10 void draw() const override { 11 std::cout << "_Knight_" << std::endl; 12 } 13};
Here, both classes Pawn
and Knight
provide their own implementations of the draw
method.
What if we want to create an array to store all the chess pieces?
Using pointers to the base class allows you to create an array capable of holding objects of any derived class. This is crucial because C++ is a statically-typed language, and the compiler needs to know the type and size of each array element at compile-time. By storing pointers to the base class, you enable the compiler to treat all array elements as having the same type—pointers to the base class—thus achieving polymorphic behavior.
Let's see an example:
C++1int main() { 2 ChessPiece* pieces[10]; // Array of base class pointers 3 4 for (int i = 0; i < 8; ++i) { 5 pieces[i] = new Pawn(); 6 } 7 8 for (int i = 8; i < 10; ++i) { 9 pieces[i] = new Knight(); 10 }
Here, we create an array pieces
of ChessPiece
pointers. Both Pawn
and Knight
are derived from the ChessPiece
, so both can be initialized with a ChessPiece*
pointer. The code shows how to store 8 Pawn
objects and 2 Knight
objects in this array.
Finally, we'll loop through the array and call the draw
method on each object. This demonstrates polymorphic behavior.
C++1 for (int i = 0; i < 10; ++i) { 2 pieces[i]->draw(); // Calls appropriate overridden function 3 } 4 5 for (int i = 0; i < 10; ++i) { 6 delete pieces[i]; // Clean up the heap-allocated memory 7 } 8 9 return 0; 10}
In this loop, each pieces[i]->draw()
call invokes the draw
method for the appropriate Pawn
or Knight
object. This way, we have a uniform code to work with each object. You can add more pieces of different types, and you won't need to modify this draw
loop – it will automatically call the appropriate draw
method for each piece.
Polymorphism works even when the base class is not abstract. Let's take a look at this example:
C++1class ChessPiece { 2public: 3 virtual void draw() { 4 std::cout << "Can't draw an abstract piece!" << std::endl; 5 } 6 7 virtual ~ChessPiece() {} // Virtual destructor 8}; 9 10class Pawn : public ChessPiece { 11public: 12 void draw() { 13 std::cout << "_Pawn_" << std::endl; 14 } 15}; 16 17class Knight : public ChessPiece { 18public: 19 void draw() { 20 std::cout << "_Knight_" << std::endl; 21 } 22};
It will work in exactly the same way. But in this case, the ChessPiece
class is not abstract, as its only method, draw
, is defined. We can create instances of the ChessPiece
class and operate them.
Congratulations! You've learned how to apply polymorphism in C++ using a practical example:
- Define an abstract class with a pure virtual function.
- Create derived classes that implement this pure virtual function.
- Store multiple derived class objects in a single array using base class pointers.
- Invoke methods on these objects to demonstrate polymorphism.
It's time for practice! Move on to exercises where you'll use polymorphism with virtual functions and abstract classes in C++. These will solidify your understanding and give you hands-on experience. Happy coding!