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

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
Animal object: Dog object:
+----------+ +----------+
| vptr ---|--+ | vptr ---|--+
| ... | | | ... | |
+----------+ | +----------+ |
v v
Animal vtable Dog vtable
+------------+ +------------+
| makeSound --|-> Animal::makeSound | makeSound --|-> Dog::makeSound
+------------+ +------------+

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 for derived classes.

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)