C++ for C Developers |Lesson 9: Polymorphism: Virtual Functions & Dynamic Behavior
Goal: Understand runtime polymorphism (“many shapes”) in C++, enabling you to treat objects of different derived classes uniformly via base class pointers or references, while ensuring the correct, specific derived class methods are executed. Learn the crucial role of the virtual
keyword.
1. The Limitation of Basic Inheritance
In Lesson 8, we saw that a Dog
is an Animal
. This “is-a” relationship means we should be able to use a pointer or reference to an Animal
to refer to a Dog
object.
Dog myDog("Labrador", 3, "Bob");
Animal* animalPtr = &myDog; // OK: A Dog IS-AN Animal
animalPtr->eat(); // This works as expected (calls Animal::eat)
// But what about methods overridden in Dog, like sleep()?
animalPtr->sleep(); // Which version gets called? Animal's or Dog's?
Without any special keywords, calling animalPtr->sleep()
would typically call the Animal::sleep()
version, even though animalPtr
actually points to a Dog
object. The compiler resolves the function call based on the static type of the pointer (Animal*
), not the dynamic type of the object it points to (Dog
). This is called static binding or early binding.
2. Enabling Dynamic Behavior: virtual
Functions
To achieve dynamic binding (or late binding), where the function call is resolved at runtime based on the object’s actual type, C++ uses the virtual
keyword.
You declare a function as virtual
in the base class if you expect derived classes to provide their own implementation (override it) and you want these derived versions to be callable through base class pointers/references.
Modified Animal Example:
#include <iostream>
#include <string>
#include <vector>
#include <memory> // For smart pointers (recommended)
// --- Base Class ---
class Animal {
protected:
std::string m_species;
int m_age;
public:
Animal(const std::string& species, int age) : m_species(species), m_age(age) {
std::cout << "Animal Constructor (" << m_species << ")" << std::endl;
}
// *** CRUCIAL: Virtual Destructor ***
// If a class has ANY virtual functions, it NEEDS a virtual destructor.
virtual ~Animal() {
std::cout << "Animal Destructor (" << m_species << ")" << std::endl;
}
// *** Declare functions intended for overriding as virtual ***
virtual void speak() const {
std::cout << "The " << m_species << " makes a generic sound." << std::endl;
}
virtual void move() const {
std::cout << "The " << m_species << " moves somehow." << std::endl;
}
// Non-virtual function - behavior is fixed for all derived types via base pointer
void eat() const {
std::cout << "The " << m_species << " is eating." << std::endl;
}
std::string getSpecies() const { return m_species; }
};
// --- Derived Classes ---
class Dog : public Animal {
private:
std::string m_breed;
public:
Dog(const std::string& breed, int age) : Animal("Dog", age), m_breed(breed) {
std::cout << " -> Dog Constructor (" << m_breed << ")" << std::endl;
}
~Dog() override { // Good practice to mark override destructors too
std::cout << " -> Dog Destructor (" << m_breed << ")" << std::endl;
}
// *** Override virtual functions from base ***
// 'override' keyword (C++11+) is optional but HIGHLY recommended:
// 1. It documents intent.
// 2. Compiler checks: Ensures a base virtual function with the exact same signature exists. Prevents subtle bugs!
void speak() const override {
std::cout << "The " << m_breed << " dog says: Woof!" << std::endl;
}
void move() const override {
std::cout << "The " << m_breed << " dog runs." << std::endl;
}
};
class Cat : public Animal {
private:
bool m_is_grumpy;
public:
Cat(int age, bool grumpy) : Animal("Cat", age), m_is_grumpy(grumpy) {
std::cout << " -> Cat Constructor" << std::endl;
}
~Cat() override {
std::cout << " -> Cat Destructor" << std::endl;
}
void speak() const override {
if (m_is_grumpy) {
std::cout << "The cat hisses." << std::endl;
} else {
std::cout << "The cat says: Meow." << std::endl;
}
}
// Cat doesn't override move(), so it will inherit Animal::move() behavior.
};
int main() {
// Polymorphism works with pointers or references
// Using smart pointers (unique_ptr) is preferred over raw pointers
std::vector<std::unique_ptr<Animal>> zoo;
// Add different types of Animals to the vector using the base class pointer type
zoo.push_back(std::make_unique<Dog>("Retriever", 5));
zoo.push_back(std::make_unique<Cat>(2, false));
zoo.push_back(std::make_unique<Dog>("Poodle", 1));
zoo.push_back(std::make_unique<Cat>(7, true)); // A grumpy cat
zoo.push_back(std::make_unique<Animal>("Parrot", 10)); // Base class object too
std::cout << "\n--- Iterating through the zoo: ---\n";
// Process all animals uniformly using the base class interface
for (const auto& animalPtr : zoo) { // animalPtr is of type const std::unique_ptr<Animal>&
std::cout << "Species: " << animalPtr->getSpecies() << " -> ";
// *** Dynamic Dispatch happens here! ***
// The correct speak() and move() is called based on the ACTUAL object type
animalPtr->speak(); // Calls Dog::speak, Cat::speak, or Animal::speak
// Only call move() if it's not the base Parrot example for variety
if (animalPtr->getSpecies() != "Parrot") {
animalPtr->move(); // Calls Dog::move or Animal::move (since Cat didn't override)
}
animalPtr->eat(); // Calls Animal::eat (non-virtual, always the base version via base pointer)
std::cout << "---" << std::endl;
}
std::cout << "\n--- Zoo vector going out of scope ---\n";
// Objects deleted automatically by unique_ptr. Crucially, the correct
// derived destructors are called BECAUSE Animal's destructor is virtual.
return 0;
}
flowchart LR AnimalClass["class Animal"] DogClass["class Dog : public Animal"] AnimalVTable["Animal VTable"] DogVTable["Dog VTable"] AnimalClass -->|"has virtual speak()"| AnimalVTable DogClass -->|"overrides speak()"| DogVTable AnimalVTable --> A1["&Animal::speak"] DogVTable --> D1["&Dog::speak"] subgraph Object Memory AnimalObj["Animal* animal = new Dog()"] AnimalObj -->|"vptr"| DogVTable end
Key Mechanisms:
virtual
in Base: Tells the compiler that this function might be overridden and calls should be resolved at runtime for polymorphic calls.override
in Derived: Explicitly states the intent to override a base virtual function and enables compiler checks for matching signatures.- Dynamic Dispatch: When
animalPtr->speak()
is called, the program looks at the actual objectanimalPtr
points to (e.g., aDog
object) and finds the appropriatespeak
method to execute (Dog::speak
). This lookup happens at runtime. (Often implemented using a hidden “virtual table” or “vtable” associated with classes that have virtual functions).
3. The Crucial Rule: Virtual Destructors
If a class has any virtual functions, or if you intend to delete objects of derived classes through a pointer to the base class, the base class destructor MUST be declared virtual
.
// In Base Class (e.g., Animal):
virtual ~Animal(); // CORRECT
// ~Animal(); // WRONG if deleting Derived via Base* !!
flowchart TD P["Animal* ptr = new Dog()"] P --> D[delete ptr] D -->|If destructor is not virtual| Leak["Only ~Animal() called"] D -->|If destructor is virtual| Clean["~Dog() → ~Animal()"] class Leak wrong class Clean correct
Why? Consider this:
Animal* ptr = new Dog("Beagle", 4);
// ... use ptr ...
delete ptr; // PROBLEM if ~Animal() is NOT virtual!
If ~Animal()
is not virtual, delete ptr;
only calls the Animal
destructor based on the pointer type (Animal*
). The Dog
destructor (~Dog()
) will not be called. If the Dog
destructor was responsible for releasing resources specific to Dog
(e.g., freeing memory), those resources will be leaked!
If ~Animal()
is virtual, delete ptr;
performs dynamic dispatch for the destructor call. It sees that ptr
points to a Dog
, calls ~Dog()
first, and then automatically calls ~Animal()
. This ensures proper cleanup in the correct order (derived then base).
Failure to provide a virtual destructor in a polymorphic base class leads to undefined behavior and resource leaks.
4. Abstract Classes and Pure Virtual Functions (Briefly)
Sometimes, a base class represents a concept for which providing a default implementation makes no sense. For example, what’s the default way for an abstract Shape
to draw()
itself?
You can create abstract classes by declaring one or more functions as pure virtual:
class Shape { // Abstract Base Class
public:
virtual ~Shape() = default; // Virtual destructor still needed! `= default` is ok here.
// Pure virtual function: No implementation in Shape
virtual double area() const = 0; // '= 0' makes it pure virtual
virtual void draw() const = 0;
};
class Circle : public Shape {
private:
double m_radius;
public:
Circle(double r) : m_radius(r) {}
// Circle MUST provide implementations for ALL pure virtual functions from Shape
double area() const override { return 3.14159 * m_radius * m_radius; }
void draw() const override { std::cout << "Drawing a circle with radius " << m_radius << std::endl; }
};
- An abstract class (like
Shape
) cannot be instantiated directly (Shape s;
is an error). - Derived classes (like
Circle
) must override all pure virtual functions inherited from the base class to become concrete (instantiable). - Abstract classes are excellent for defining interfaces – contracts that derived classes must fulfill.
Summary of Lesson 9:
- Polymorphism (runtime) allows treating derived objects via base pointers/references, calling the appropriate derived methods.
- Achieved using the
virtual
keyword on methods in the base class. - Use the
override
keyword in derived classes for clarity and compiler checks. - CRITICAL: Base classes intended for polymorphic use MUST have a
virtual
destructor to ensure correct cleanup when deleting derived objects via base pointers. - Dynamic Dispatch is the runtime mechanism that selects the correct virtual function to call based on the object’s actual type.
- Pure virtual functions (
virtual ... = 0;
) create abstract classes that define interfaces and cannot be instantiated directly.
Next Lesson: We’ll cover C++’s preferred way of handling errors – Exceptions – contrasting them with C’s error codes/errno
More Sources:
cppreference – Virtual Functions: https://en.cppreference.com/w/cpp/language/virtual
Learn C++ – Virtual Functions: https://www.learncpp.com/cpp-tutorial/virtual-functions/