Start learning free

LESSON 183 OF 343 · OBJECT-ORIENTED PROGRAMMING

Polymorphism in C++: Virtual Functions and Base Pointers

Lesson Read the full explanation

TL ; DR:

Polymorphism is when a single piece of code (like animal.speak()) does different things depending on which subclass the object actually is. You write the call once, and the right override runs automatically.


vector<Animal*> animals = { new Dog(), new Cat() };
for (Animal* a : animals) {
    a->speak();   // dispatches to Dog::speak then Cat::speak
}


Full lesson:

Polymorphism (Greek for "many forms") is what makes inheritance and interfaces actually useful at scale. Once Dog and Cat both inherit from Animal and override speak(), you can stop caring which one you're holding. Any code that takes an Animal and calls speak() works for both.

Why it matters

Imagine you're writing a function that prints what every animal in a zoo says:

void Roll(Animal[] animals) {
    foreach (var a in animals) a.Speak();
}

This function works for any current or future kind of animal. Add a Cow subclass tomorrow — this loop runs Cow.Speak() automatically, no edits needed. That's the win: code that works today and keeps working as the type hierarchy grows.

Static type vs dynamic type

The pointer is typed as Animal. The object it actually points to is a Dog, or a Cat, etc. Polymorphism is the language dispatching the method call based on the dynamic type (what it is at runtime) rather than the static type (what the variable was declared as).

Notes by language

  • C# / C++ — the parent method must be virtual (and the override marked override) for runtime dispatch. Otherwise you get static dispatch and the wrong version runs.
  • Java / JS / Python — methods are virtual by default. Just override and it works.

Your task

Animal, Dog, and Cat are already defined, with the right overrides. Inside MainFunction, build a collection containing one Dog and one Cat (typed as Animal), then loop through it calling speak() on each.

Expected output:

Woof
Meow
STEP 1 OF 3
    Ctrl + Enter to check

    C++ · OBJECT-ORIENTED PROGRAMMING

    Polymorphism in C++, explained

    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, Cat or 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
    1. In mainFunction, declare vector<Animal*> animals holding new Dog() and new Cat(). Each new expression gives a pointer to a derived object, and a derived-class pointer converts to Animal* automatically.
    2. Loop over the vector with a range-based for and an Animal* variable a, and call a->speak(). The call is written once, and each element chooses its own version.
    3. Loop over the vector again and delete a for each element, to free what new created.

    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 because a is a pointer. A dot is for objects and references.
    • The loops copy the pointer, not the object, so deleting through a frees the real object. The vector still holds the old addresses afterwards, so don't use them again.
    • If you remove virtual from speak() in Animal, 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 virtual on 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 when a is an Animal*.

    Key takeaways

    • Polymorphism lets one call, through a base pointer or reference, run the version belonging to the real object.
    • It requires virtual in the base class and a pointer or reference, not a copy.
    • Give base classes a virtual destructor before deleting derived objects through them.
    • Every new needs exactly one delete, unless a smart pointer does it for you.
    Keep going with C++About 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free