Skip to content

Module 04: Polymorphism, Abstract Classes, and Interfaces

Download Official Subject PDF

Key Concepts:

  • Virtual functions
  • Runtime polymorphism
  • Virtual destructors
  • Abstract classes (pure virtual)
  • Interfaces
  • Deep copy with polymorphism

The Foundation: IS-A Relationship Enables Polymorphism

Section titled “The Foundation: IS-A Relationship Enables Polymorphism”

Polymorphism builds on the IS-A relationship from inheritance:

class Animal { };
class Dog : public Animal { }; // A Dog IS-A(n) Animal
class Cat : public Animal { }; // A Cat IS-A(n) Animal

Because a Dog IS-A Animal, you can treat Dogs as Animals:

Animal* pet = new Dog(); // Valid! A Dog IS-A Animal

This is the foundation of polymorphism: you can use derived objects wherever base objects are expected. The magic of virtual functions is that calling pet->makeSound() executes Dog::makeSound() even though pet is declared as Animal*.


This module introduces virtual functions and abstract classes. Here’s the syntax explained:

virtual void makeSound() { /* ... */ }
  • virtual before a function declaration enables runtime polymorphism
  • Without virtual, C++ uses the static type (compile-time type) to decide which function to call
  • With virtual, C++ uses the dynamic type (actual object type at runtime) to decide
  • This allows a Animal* to call Dog::makeSound() if it actually points to a Dog
virtual void makeSound() = 0; // Pure virtual function
  • = 0 makes a virtual function “pure virtual” (abstract)
  • This means the class provides NO implementation - derived classes MUST override it
  • Any class with at least one pure virtual function becomes an abstract class
  • You CANNOT create objects of abstract classes: Animal pet; is an error
Animal* pet = new Dog(); // Pointer to base, object is derived
pet->makeSound(); // Calls Dog::makeSound() (if virtual)
  • * with polymorphism: declare pointers to base class, but point to derived objects
  • -> accesses members through the pointer
  • virtual ensures the correct (derived) function is called
  • Without virtual, you’d always get the base class version
void makeNoise(Animal& animal) { // Can pass Dog, Cat, etc.
animal.makeSound(); // Correct sound plays
}
  • & creates a reference to the base class
  • Like pointers, references enable polymorphism
  • The actual object type determines which function runs
  • References are safer than pointers (can’t be null, can’t be reassigned)
Animal* pet = new Dog();
delete pet; // Without virtual destructor, Dog destructor never runs!
  • delete on a base class pointer only calls the base destructor (by default)
  • This causes memory leaks if the derived class allocated resources
  • Solution: Make base class destructor virtual
  • With virtual ~Animal(), delete pet calls ~Dog() then ~Animal() - correct!

The * Operator (Clone Pattern Return Type)

Section titled “The * Operator (Clone Pattern Return Type)”
virtual Animal* clone() const = 0; // Each derived class returns its own type
  • * in return types indicates a pointer is returned
  • The Clone Pattern returns a new copy of the object
  • Each derived class overrides to return its own type: Dog*, Cat*, etc.
  • This allows copying through base class pointers while preserving the actual type
dynamic_cast<Dog*>(animal); // Safe runtime cast
  • dynamic_cast<> safely converts base class pointers to derived class pointers
  • It checks at runtime if the cast is valid
  • Returns NULL (for pointers) or throws exception (for references) if cast fails
  • Only works with polymorphic classes (classes with virtual functions)

class Animal {
public:
void makeSound() { std::cout << "Some sound" << std::endl; }
};
class Dog : public Animal {
public:
void makeSound() { std::cout << "Woof!" << std::endl; }
};
class Cat : public Animal {
public:
void makeSound() { std::cout << "Meow!" << std::endl; }
};
// THE PROBLEM:
Animal* pet = new Dog();
pet->makeSound(); // Prints "Some sound" - NOT "Woof!"
// Why? Without virtual, C++ uses STATIC binding
// It sees Animal* and calls Animal::makeSound()

class Animal {
public:
virtual void makeSound() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void makeSound() { // Overrides virtual function
std::cout << "Woof!" << std::endl;
}
};
// NOW:
Animal* pet = new Dog();
pet->makeSound(); // Prints "Woof!" - correct!
// Why? With virtual, C++ uses DYNAMIC binding
// It checks the actual object type at runtime
// Simplified view of what happens:
// Without virtual (static binding):
// Compiler sees: Animal* ptr
// Compiler calls: Animal::makeSound() - decided at compile time
// With virtual (dynamic binding):
// Object contains hidden pointer to "vtable" (virtual table)
// vtable contains pointers to correct functions for that class
// At runtime, correct function is looked up and called
┌─────────────────────────────────────────────────────────────────┐
│ How Virtual Functions Work (vtable) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Memory Layout with vptr and vtable: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌────────────────┐ ┌────────────────┐ │ │
│ │ │ Animal Object │ │ Dog Object │ │ │
│ │ ├────────────────┤ ├────────────────┤ │ │
│ │ │ [vptr]─────┐ │ │ [vptr]─────┐ │ │ │
│ │ │ _type │ │ │ _type │ │ │ │
│ │ │ _brain │ │ │ _brain │ │ │ │
│ │ │ [data...] │ │ │ [data...] │ │ │ │
│ │ └────────────┼───┘ └────────────┼───┘ │ │
│ │ │ │ │ │
│ │ └───────────┬───────────┘ │ │
│ │ │ │ │
│ │ ┌───────────▼───────────┐ │ │
│ │ │ vptr (pointer) │ │ │
│ │ │ (8 bytes in x64) │ │ │
│ │ └───────────┬───────────┘ │ │
│ │ │ │ │
│ │ ┌───────────────────┴───────────────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌───────────────────┐ ┌───────────────────┐ │ │
│ │ │ Animal vtable │ │ Dog vtable │ │ │
│ │ ├───────────────────┤ ├───────────────────┤ │ │
│ │ │ [0] destructor ──┼──┐ │ [0] destructor ──┼──┐│ │
│ │ │ [1] makeSound ──┼──┼──┐ │ [1] makeSound ──┼──┼┘│ │
│ │ │ [2] clone ──┼──┼──┼──│ [2] clone ──┼──┼─┘ │
│ │ │ [3] ... ──┼──┼──┼──│ [3] ... ──┼──┘ │
│ │ └───────────────────┘ │ │ └───────────────────┘ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌─────────────────┐ │
│ │ │ Function │ │
│ │ │ Pointers │ │
│ │ │ │ │
│ │ │ ┌─────────────┐ │ │
│ │ │ │Animal:: │ │ │
│ │ │ │makeSound() │ │ │
│ │ │ └─────────────┘ │ │
│ │ │ ┌─────────────┐ │ │
│ │ │ │Dog:: │ │ │
│ │ │ │makeSound() │ │ │
│ │ │ └─────────────┘ │ │
│ │ │ ┌─────────────┐ │ │
│ │ │ │Dog:: │ │ │
│ │ │ │clone() │ │ │
│ │ │ └─────────────┘ │ │
│ │ └─────────────────┘ │
│ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Dynamic Dispatch Process: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Animal* pet = new Dog(); │ │
│ │ │ │ │
│ │ │ pet points to Dog object (has Dog vtable) │ │
│ │ ▼ │ │
│ │ pet->makeSound(); // Runtime lookup │ │
│ │ │ │ │
│ │ ▼ 1. Follow vptr in object │ │
│ │ ┌─────────────────┐ │ │
│ │ │ vptr → Dog │ │ │
│ │ │ vtable │ │ │
│ │ └────────┬────────┘ │ │
│ │ ▼ 2. Lookup makeSound index │ │
│ │ ┌─────────────────┐ │ │
│ │ │ makeSound slot │ → Dog::makeSound() │ │
│ │ │ (index 1) │ "Woof!" │ │
│ │ └─────────────────┘ │ │
│ │ │ │ │
│ │ ▼ 3. Call the function │ │
│ │ Result: "Woof!" (not "Some sound") │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Key Points: │
│ • Every object with virtual functions has a hidden vptr │
│ • vptr points to the class's vtable (static, shared) │
│ • vtable contains function pointers to override implementations│
│ • Runtime cost: one extra pointer indirection │
│ • Memory cost: one extra pointer per object │
│ │
└─────────────────────────────────────────────────────────────────┘

class Animal {
public:
~Animal() { std::cout << "Animal destroyed" << std::endl; }
};
class Dog : public Animal {
private:
Brain* _brain;
public:
Dog() { _brain = new Brain(); }
~Dog() { delete _brain; std::cout << "Dog destroyed" << std::endl; }
};
Animal* pet = new Dog();
delete pet; // ONLY calls ~Animal()!
// Dog's brain is LEAKED!
class Animal {
public:
virtual ~Animal() { std::cout << "Animal destroyed" << std::endl; }
//^^^^^^^ ALWAYS make destructors virtual in base classes
};
Animal* pet = new Dog();
delete pet; // Calls ~Dog() THEN ~Animal() - correct!

If a class has ANY virtual functions, make its destructor virtual.


A class that CANNOT be instantiated. It serves as a blueprint (or contract) for derived classes.

The Contract Concept: An abstract class defines WHAT derived classes must do without specifying HOW. When you write virtual void makeSound() = 0;, you’re saying: “Any class that inherits from me MUST provide a makeSound() implementation.” This is a contract that derived classes must fulfill.

Why contracts matter:

  • Guarantees behavior: Code using Animal* can call makeSound() knowing it exists
  • Enforces completeness: The compiler won’t let you instantiate a class that doesn’t fulfill the contract
  • Enables polymorphism: You can write generic code that works with any Animal
class Animal {
public:
// Pure virtual function - no implementation
virtual void makeSound() const = 0;
// ^^^ Makes it pure virtual
virtual ~Animal() {}
};
// Now Animal is abstract:
Animal pet; // ERROR: cannot instantiate abstract class
Animal* ptr; // OK: can have pointer to abstract class
ptr = new Dog(); // OK: can point to concrete derived class
class Dog : public Animal {
public:
// MUST implement ALL pure virtual functions
void makeSound() const {
std::cout << "Woof!" << std::endl;
}
};
Dog dog; // OK: Dog is concrete (implements all pure virtuals)
class Animal {
public:
virtual void makeSound() const = 0; // Pure virtual
virtual void eat() { /* default */ } // Virtual with default
};
class Dog : public Animal {
public:
void makeSound() const { /* ... */ } // Must implement
// eat() inherited with default implementation
};

A class with ONLY pure virtual functions. Defines a contract without any implementation.

// Interface naming convention: prefix with 'I'
class ICharacter {
public:
virtual ~ICharacter() {}
virtual std::string const& getName() const = 0;
virtual void equip(AMateria* m) = 0;
virtual void unequip(int idx) = 0;
virtual void use(int idx, ICharacter& target) = 0;
};
class Character : public ICharacter {
private:
std::string _name;
AMateria* _inventory[4];
public:
Character(std::string name);
Character(const Character& other);
Character& operator=(const Character& other);
~Character();
// Must implement ALL interface methods
std::string const& getName() const;
void equip(AMateria* m);
void unequip(int idx);
void use(int idx, ICharacter& target);
};

class Animal {
protected:
Brain* _brain;
public:
Animal() { _brain = new Brain(); }
Animal(const Animal& other) {
_brain = new Brain(*other._brain);
}
virtual ~Animal() { delete _brain; }
};
class Dog : public Animal { /* ... */ };
// Problem: copying through base pointer
Animal* original = new Dog();
Animal* copy = new Animal(*original); // Creates Animal, not Dog!
class Animal {
public:
virtual Animal* clone() const = 0;
virtual ~Animal() {}
};
class Dog : public Animal {
public:
Dog* clone() const {
return new Dog(*this);
}
};
Animal* original = new Dog();
Animal* copy = original->clone(); // Creates Dog!
class AMateria {
protected:
std::string _type;
public:
AMateria(std::string const& type) : _type(type) {}
virtual ~AMateria() {}
std::string const& getType() const { return _type; }
virtual AMateria* clone() const = 0; // Pure virtual
};
class Ice : public AMateria {
public:
Ice() : AMateria("ice") {}
Ice(const Ice& other) : AMateria(other) {}
// Covariant return type - return Ice* instead of AMateria*
Ice* clone() const {
return new Ice(*this); // Uses copy constructor
}
};
class Cure : public AMateria {
public:
Cure() : AMateria("cure") {}
Cure(const Cure& other) : AMateria(other) {}
Cure* clone() const {
return new Cure(*this);
}
};
class Character {
private:
AMateria* _inventory[4];
public:
// Deep copy using clone
Character(const Character& other) {
for (int i = 0; i < 4; i++) {
if (other._inventory[i])
_inventory[i] = other._inventory[i]->clone();
else
_inventory[i] = NULL;
}
}
// Assignment using clone
Character& operator=(const Character& other) {
if (this != &other) {
// Delete old inventory
for (int i = 0; i < 4; i++)
delete _inventory[i];
// Clone new inventory
for (int i = 0; i < 4; i++) {
if (other._inventory[i])
_inventory[i] = other._inventory[i]->clone();
else
_inventory[i] = NULL;
}
}
return *this;
}
~Character() {
for (int i = 0; i < 4; i++)
delete _inventory[i];
}
};

// Array of Animal pointers (can hold Dogs, Cats, etc.)
Animal* animals[4];
animals[0] = new Dog();
animals[1] = new Cat();
animals[2] = new Dog();
animals[3] = new Cat();
// Polymorphic behavior
for (int i = 0; i < 4; i++) {
animals[i]->makeSound(); // Calls correct version
}
// CRITICAL: Must delete each element
for (int i = 0; i < 4; i++) {
delete animals[i]; // Virtual destructor ensures proper cleanup
}
// WITHOUT virtual destructor:
Animal* pet = new Dog(); // Dog allocates Brain
delete pet; // Only ~Animal() called - Brain leaked!
// WITH virtual destructor:
Animal* pet = new Dog(); // Dog allocates Brain
delete pet; // ~Dog() called first (deletes Brain), then ~Animal()

class Animal {
protected:
std::string _type;
public:
Animal();
Animal(const Animal& other);
Animal& operator=(const Animal& other);
virtual ~Animal();
virtual void makeSound() const;
std::string getType() const;
};
class Dog : public Animal {
public:
Dog();
Dog(const Dog& other);
Dog& operator=(const Dog& other);
~Dog();
void makeSound() const; // Barks
};
class Cat : public Animal {
public:
Cat();
Cat(const Cat& other);
Cat& operator=(const Cat& other);
~Cat();
void makeSound() const; // Meows
};
// Also implement WrongAnimal/WrongCat without virtual
// to demonstrate the difference
class Brain {
public:
std::string ideas[100];
Brain();
Brain(const Brain& other);
Brain& operator=(const Brain& other);
~Brain();
};
class Dog : public Animal {
private:
Brain* _brain; // Dynamically allocated
public:
Dog();
Dog(const Dog& other); // Must deep copy brain
Dog& operator=(const Dog& other); // Must deep copy brain
~Dog(); // Must delete brain
};
// Test deep copy:
Dog original;
Dog copy = original;
// Modifying copy's brain should NOT affect original's brain
// Make Animal abstract (cannot instantiate)
class Animal {
public:
virtual void makeSound() const = 0; // Pure virtual
// ...
};
Animal pet; // ERROR: Animal is abstract
Animal* ptr = new Dog(); // OK
class AMateria {
protected:
std::string _type;
public:
AMateria(std::string const& type);
virtual ~AMateria();
std::string const& getType() const;
virtual AMateria* clone() const = 0;
virtual void use(ICharacter& target);
};
class Ice : public AMateria {
public:
Ice();
Ice(const Ice& other);
Ice& operator=(const Ice& other);
~Ice();
AMateria* clone() const;
void use(ICharacter& target);
};
class Cure : public AMateria {
// Similar to Ice
};
class Character : public ICharacter {
private:
std::string _name;
AMateria* _inventory[4];
public:
// Implement all ICharacter methods
// Handle equip/unequip memory carefully!
};
class MateriaSource : public IMateriaSource {
// Learn and create Materias
};

The Factory Pattern creates objects without exposing instantiation logic.

class IMateriaSource {
public:
virtual ~IMateriaSource() {}
virtual void learnMateria(AMateria*) = 0;
virtual AMateria* createMateria(std::string const& type) = 0;
};
class MateriaSource : public IMateriaSource {
private:
AMateria* _templates[4];
public:
MateriaSource() {
for (int i = 0; i < 4; i++)
_templates[i] = NULL;
}
// Deep copy in copy constructor
MateriaSource(const MateriaSource& other) {
for (int i = 0; i < 4; i++) {
if (other._templates[i])
_templates[i] = other._templates[i]->clone();
else
_templates[i] = NULL;
}
}
~MateriaSource() {
for (int i = 0; i < 4; i++)
delete _templates[i];
}
void learnMateria(AMateria* m) {
if (!m) return;
for (int i = 0; i < 4; i++) {
if (_templates[i] == NULL) {
_templates[i] = m->clone(); // Store a COPY
return;
}
}
}
// Factory method - creates new objects based on type string
AMateria* createMateria(std::string const& type) {
for (int i = 0; i < 4; i++) {
if (_templates[i] && _templates[i]->getType() == type)
return _templates[i]->clone(); // Return a NEW copy
}
return NULL;
}
};
IMateriaSource* src = new MateriaSource();
// Teach the factory what it can create
src->learnMateria(new Ice());
src->learnMateria(new Cure());
// Factory creates new instances
AMateria* ice = src->createMateria("ice"); // New Ice object
AMateria* cure = src->createMateria("cure"); // New Cure object
AMateria* unknown = src->createMateria("fire"); // NULL - not learned
delete src;
  1. Decouples creation from usage: Client doesn’t need to know concrete types
  2. Uses clone(): New objects are copies of templates
  3. Memory ownership: Factory owns templates, caller owns created objects

virtual void method(); // Virtual (can override)
virtual void method() = 0; // Pure virtual (must override)
void method(); // Non-virtual (hides, doesn't override)
  • Has at least one pure virtual function
  • Cannot be instantiated
  • Can have data members and non-pure methods
  • Only pure virtual functions
  • Virtual destructor
  • No data members (typically)
  • Defines a contract
  • Virtual destructor in base class
  • Deep copy in copy constructor
  • Deep copy in assignment operator
  • Delete allocated memory in destructor
  • Handle unequip() without deleting (save pointer first)