One call, many behaviours
Polymorphism means "many forms". In C++ it is the ability to write one call, such as a->speak(), that does different things depending on the real class of the object. You write the call once, and the right override runs each time.
It builds on the two previous lessons. If Dog and Cat both inherit from Animal and override speak(), any code that works with an Animal works for both. It keeps working when someone adds a Cow class next month, without editing the code that calls speak().
Static type and dynamic type
A pointer of type Animal* can hold the address of an Animal or of any class derived from it. Two types are involved:
- The static type is the type the variable was declared with:
Animal*. The compiler sees only this. - The dynamic type is the type of the object the pointer points at right now:
Dog,Cator another class. It's known only while the program runs.
For a virtual function, C++ picks the version to call using the dynamic type. This is called dynamic dispatch. For a non-virtual function, it uses the static type, and you get the base version even when the object is a Dog. So the function must be declared virtual in the base class.
Why a vector of pointers
To keep a mixed collection, you need a container of pointers, such as vector<Animal*>. A vector<Animal> would not work. It stores whole Animal objects, so putting a Dog into it copies only the Animal part and throws away the rest. This is called slicing. With this code:
vector<Animal> sliced = { Dog(), Cat() };
for (auto& a : sliced) a.speak();
both elements are plain animals, and the output is ... twice, not Woof and Meow. A pointer or a reference keeps the whole object, so the dynamic type survives. References work the same way: a function void talk(Animal& a) { a.speak(); } prints Meow when it is given a Cat.
Cleaning up with delete
Objects created with new are not destroyed automatically, so each one needs a matching delete. Deleting through a base pointer is safe only if the base class has a virtual destructor. Without one, delete a on an Animal* that points to a Dog is undefined behaviour, because the Dog part might never be destroyed correctly. That is why the lesson's Animal has this line:
virtual ~Animal() = default;
The rule of thumb is simple: a class meant to be a base class that is used through pointers should have a virtual destructor. In modern C++ you can also avoid manual delete with smart pointers: vector<unique_ptr<Animal>> destroys its elements by itself. The lesson uses plain pointers so that you see the whole life of the objects.
The lesson's exercise
The classes Animal, Dog and Cat are given. Animal::speak is virtual, and the two subclasses override it. The goal is to print:
Woof
Meow
- In
mainFunction, declarevector<Animal*> animalsholdingnew Dog()andnew Cat(). Eachnewexpression gives a pointer to a derived object, and a derived-class pointer converts toAnimal*automatically. - Loop over the vector with a range-based
forand anAnimal*variablea, and calla->speak(). The call is written once, and each element chooses its own version. - Loop over the vector again and
delete afor each element, to free whatnewcreated.
The solution in C++
class Animal {
public:
virtual void speak() {
cout << "..." << endl;
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() override {
cout << "Woof" << endl;
}
};
class Cat : public Animal {
public:
void speak() override {
cout << "Meow" << endl;
}
};
class Solution {
public:
void mainFunction() {
vector<Animal*> animals = { new Dog(), new Cat() };
for (Animal* a : animals) {
a->speak();
}
for (Animal* a : animals) {
delete a;
}
}
};
A few details:
- The arrow
->is used becauseais a pointer. A dot is for objects and references. - The loops copy the pointer, not the object, so deleting through
afrees the real object. The vector still holds the old addresses afterwards, so don't use them again. - If you remove
virtualfromspeak()inAnimal, the first loop prints...twice.
Common mistakes
- Storing objects, not pointers, in the vector. The elements are sliced and every call runs the base version.
- Forgetting
virtualon the base function, so the static type decides. - Never deleting the objects. The program prints the right output but leaks memory.
- Deleting twice, or using a pointer after deleting it. Both are undefined behaviour.
- Deleting through a base pointer when the base destructor is not virtual.
- Using a dot with a pointer:
a.speak()is an error whenais anAnimal*.
Key takeaways
- Polymorphism lets one call, through a base pointer or reference, run the version belonging to the real object.
- It requires
virtualin the base class and a pointer or reference, not a copy. - Give base classes a virtual destructor before deleting derived objects through them.
- Every
newneeds exactly onedelete, unless a smart pointer does it for you.