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

Polymorphism is one of the most powerful features of object-oriented programming. It allows you to write flexible, extensible code.


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()

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

Create an Animal base class with Dog and Cat derived classes:

  • Animal has a type attribute and makeSound() method
  • Dog says “Woof!”, Cat says “Meow!”
  • The key: When using an Animal* pointer to a Dog, calling makeSound() should output “Woof!”, not a generic sound

Also create “Wrong” versions to demonstrate what happens without polymorphism.

Step 1 - Understand the virtual keyword

Without virtual:

Animal* pet = new Dog();
pet->makeSound(); // Calls Animal::makeSound() - WRONG!

With virtual:

Animal* pet = new Dog();
pet->makeSound(); // Calls Dog::makeSound() - CORRECT!

Step 2 - Virtual destructor is CRITICAL

When deleting through base pointer, you need virtual destructor to call the derived destructor.

Step 3 - Create WrongAnimal/WrongCat for comparison

Same classes without virtual to demonstrate the difference.

Stage 1 - Base class with virtual functions:

Animal.hpp
#ifndef ANIMAL_HPP
#define ANIMAL_HPP
#include <string>
class Animal {
protected:
std::string _type;
public:
Animal();
Animal(const Animal& other);
Animal& operator=(const Animal& other);
virtual ~Animal(); // VIRTUAL destructor!
virtual void makeSound() const; // VIRTUAL for polymorphism
std::string getType() const;
};
#endif

Stage 2 - Derived class overriding:

Dog.hpp
#ifndef DOG_HPP
#define DOG_HPP
#include "Animal.hpp"
class Dog : public Animal {
public:
Dog();
Dog(const Dog& other);
Dog& operator=(const Dog& other);
virtual ~Dog();
void makeSound() const; // Override
};
#endif
Dog.cpp
#include "Dog.hpp"
#include <iostream>
Dog::Dog() : Animal() {
_type = "Dog";
std::cout << "Dog constructed" << std::endl;
}
Dog::~Dog() {
std::cout << "Dog destructed" << std::endl;
}
void Dog::makeSound() const {
std::cout << "Woof!" << std::endl;
}

Stage 3 - Wrong versions (no virtual):

WrongAnimal.hpp
class WrongAnimal {
protected:
std::string _type;
public:
WrongAnimal();
~WrongAnimal(); // NOT virtual!
void makeSound() const; // NOT virtual!
std::string getType() const;
};
LineCodeWhy
virtual ~Animal()Virtual destructorEnsures derived destructor is called through base pointer
virtual void makeSound() constVirtual functionEnables runtime polymorphism
void makeSound() const (in Dog)OverrideProvides Dog-specific implementation
_type = "Dog"Set type in derivedIdentify the actual type

1. Forgetting virtual keyword

// WRONG - no polymorphism
class Animal {
public:
void makeSound() const; // Not virtual
};
Animal* pet = new Dog();
pet->makeSound(); // Calls Animal::makeSound(), NOT Dog!
// RIGHT - polymorphic
class Animal {
public:
virtual void makeSound() const;
};

2. Non-virtual destructor causes memory leaks

// WRONG
class Animal {
public:
~Animal(); // NOT virtual
};
Animal* pet = new Dog();
delete pet; // Only ~Animal() called! ~Dog() skipped!
// RIGHT
class Animal {
public:
virtual ~Animal(); // Virtual destructor
};
int main() {
// Test polymorphism
const Animal* animal = new Animal();
const Animal* dog = new Dog();
const Animal* cat = new Cat();
std::cout << animal->getType() << ": ";
animal->makeSound(); // "Generic animal sound" or similar
std::cout << dog->getType() << ": ";
dog->makeSound(); // "Woof!"
std::cout << cat->getType() << ": ";
cat->makeSound(); // "Meow!"
delete animal;
delete dog;
delete cat;
// Compare with WrongAnimal
std::cout << "\n--- Wrong versions ---\n";
const WrongAnimal* wrongCat = new WrongCat();
wrongCat->makeSound(); // Calls WrongAnimal::makeSound(), NOT WrongCat!
delete wrongCat;
return 0;
}
Animal.cpp
#include "Animal.hpp"
#include <iostream>
Animal::Animal() : _type("Animal") {
std::cout << "Animal constructed" << std::endl;
}
Animal::Animal(const Animal& other) : _type(other._type) {
std::cout << "Animal copy constructed" << std::endl;
}
Animal& Animal::operator=(const Animal& other) {
if (this != &other)
_type = other._type;
return *this;
}
Animal::~Animal() {
std::cout << "Animal destructed" << std::endl;
}
void Animal::makeSound() const {
std::cout << "* Generic animal sound *" << std::endl;
}
std::string Animal::getType() const {
return _type;
}

Exercise 01: I Don’t Want to Set the World on Fire

Section titled “Exercise 01: I Don’t Want to Set the World on Fire”

Add a Brain class to Dog and Cat:

  • Brain has an array of 100 strings (ideas)
  • Dog and Cat each have a Brain* member
  • Critical: You must implement deep copy - each Dog/Cat has its own Brain

This tests proper memory management with polymorphism.

Step 1 - Create Brain class

Simple class with array of strings.

Step 2 - Add Brain pointer to Dog/Cat

Brain* _brain - allocated in constructor, deleted in destructor.

Step 3 - Implement deep copy

Copy constructor and assignment operator must create a NEW Brain copy, not share the pointer.

Stage 1 - Brain class:

Brain.hpp
#ifndef BRAIN_HPP
#define BRAIN_HPP
#include <string>
class Brain {
public:
std::string ideas[100];
Brain();
Brain(const Brain& other);
Brain& operator=(const Brain& other);
~Brain();
};
#endif
Brain.cpp
#include "Brain.hpp"
#include <iostream>
Brain::Brain() {
std::cout << "Brain constructed" << std::endl;
}
Brain::Brain(const Brain& other) {
for (int i = 0; i < 100; i++)
ideas[i] = other.ideas[i];
std::cout << "Brain copy constructed" << std::endl;
}
Brain& Brain::operator=(const Brain& other) {
if (this != &other) {
for (int i = 0; i < 100; i++)
ideas[i] = other.ideas[i];
}
return *this;
}
Brain::~Brain() {
std::cout << "Brain destructed" << std::endl;
}

Stage 2 - Dog with Brain (deep copy):

Dog.hpp
class Dog : public Animal {
private:
Brain* _brain;
public:
Dog();
Dog(const Dog& other);
Dog& operator=(const Dog& other);
~Dog();
void makeSound() const;
Brain* getBrain() const;
};
Dog.cpp
Dog::Dog() : Animal() {
_type = "Dog";
_brain = new Brain(); // Allocate Brain
std::cout << "Dog constructed" << std::endl;
}
// Deep copy - create NEW Brain
Dog::Dog(const Dog& other) : Animal(other) {
_brain = new Brain(*other._brain); // Copy Brain content
std::cout << "Dog copy constructed" << std::endl;
}
// Deep copy assignment
Dog& Dog::operator=(const Dog& other) {
if (this != &other) {
Animal::operator=(other);
delete _brain; // Free old Brain
_brain = new Brain(*other._brain); // Copy new Brain
}
return *this;
}
Dog::~Dog() {
delete _brain; // Free Brain
std::cout << "Dog destructed" << std::endl;
}
LineCodeWhy
Brain* _brainPointer to BrainDynamic allocation needed
_brain = new Brain()Allocate in constructorEach Dog gets its own Brain
_brain = new Brain(*other._brain)Deep copyCreate NEW Brain with same content
delete _brainIn assignmentFree OLD Brain before new allocation
delete _brainIn destructorClean up memory

1. Shallow copy (the default disaster)

// WRONG - default copy shares pointer!
Dog::Dog(const Dog& other) : Animal(other), _brain(other._brain) {
// Both dogs point to SAME Brain!
// When one is destroyed, the other has dangling pointer!
}
// RIGHT - deep copy creates new Brain
Dog::Dog(const Dog& other) : Animal(other) {
_brain = new Brain(*other._brain); // Each dog has own Brain
}

2. Forgetting virtual destructor (Brain leak)

Animal* pet = new Dog(); // Dog allocates Brain
delete pet; // If ~Animal not virtual, ~Dog not called = Brain leaks!

3. Not deleting old Brain in assignment

// WRONG - memory leak!
Dog& Dog::operator=(const Dog& other) {
_brain = new Brain(*other._brain); // Old Brain leaked!
return *this;
}
// RIGHT
Dog& Dog::operator=(const Dog& other) {
delete _brain; // Free old
_brain = new Brain(*other._brain);
return *this;
}
int main() {
// Test deep copy
Dog original;
original.getBrain()->ideas[0] = "I love bones!";
Dog copy = original; // Copy constructor
// Modify original
original.getBrain()->ideas[0] = "I hate cats!";
// Copy should NOT be affected (deep copy worked)
std::cout << "Original idea: " << original.getBrain()->ideas[0] << std::endl;
std::cout << "Copy idea: " << copy.getBrain()->ideas[0] << std::endl;
// Should show different strings!
// Test with array of Animals
Animal* animals[4];
animals[0] = new Dog();
animals[1] = new Dog();
animals[2] = new Cat();
animals[3] = new Cat();
for (int i = 0; i < 4; i++)
delete animals[i]; // Should call correct destructors
return 0;
}
Dog.cpp
#include "Dog.hpp"
#include <iostream>
Dog::Dog() : Animal() {
_type = "Dog";
_brain = new Brain();
std::cout << "Dog constructed" << std::endl;
}
Dog::Dog(const Dog& other) : Animal(other) {
_brain = new Brain(*other._brain);
std::cout << "Dog copy constructed" << std::endl;
}
Dog& Dog::operator=(const Dog& other) {
std::cout << "Dog assignment operator" << std::endl;
if (this != &other) {
Animal::operator=(other);
delete _brain;
_brain = new Brain(*other._brain);
}
return *this;
}
Dog::~Dog() {
delete _brain;
std::cout << "Dog destructed" << std::endl;
}
void Dog::makeSound() const {
std::cout << "Woof!" << std::endl;
}
Brain* Dog::getBrain() const {
return _brain;
}

Make Animal abstract - it should not be instantiable directly.

  • Rename Animal to AAnimal (the A prefix indicates abstract)
  • Make makeSound() a pure virtual function
  • AAnimal a; should now be a compile error

Step 1 - Add = 0 to make pure virtual

virtual void makeSound() const = 0; // Pure virtual

Step 2 - Understand what “abstract” means

  • Cannot create instances of abstract class
  • CAN have pointers/references to abstract class
  • Derived classes MUST implement pure virtual functions
AAnimal.hpp
#ifndef AANIMAL_HPP
#define AANIMAL_HPP
#include <string>
class AAnimal {
protected:
std::string _type;
public:
AAnimal();
AAnimal(const AAnimal& other);
AAnimal& operator=(const AAnimal& other);
virtual ~AAnimal();
virtual void makeSound() const = 0; // = 0 makes it pure virtual!
std::string getType() const;
};
#endif
LineCodeWhy
= 0Pure virtual specifierMakes class abstract
AAnimalA prefixConvention indicating abstract class

1. Forgetting to change all Animal references

Update all derived classes to inherit from AAnimal instead of Animal.

2. Trying to instantiate abstract class

// WRONG - compile error!
AAnimal a;
// RIGHT - use derived class
Dog d;
AAnimal* ptr = new Dog();
int main() {
// This should NOT compile:
// AAnimal a; // Error: cannot instantiate abstract class
// This should work:
AAnimal* dog = new Dog();
AAnimal* cat = new Cat();
dog->makeSound(); // "Woof!"
cat->makeSound(); // "Meow!"
delete dog;
delete cat;
return 0;
}
AAnimal.hpp
#ifndef AANIMAL_HPP
#define AANIMAL_HPP
#include <string>
class AAnimal {
protected:
std::string _type;
public:
AAnimal();
AAnimal(const AAnimal& other);
AAnimal& operator=(const AAnimal& other);
virtual ~AAnimal();
virtual void makeSound() const = 0;
std::string getType() const;
};
#endif

Implement a Materia system with interfaces:

  • AMateria: Abstract base for magic materials (Ice, Cure)
  • ICharacter: Interface for characters who can equip materias
  • IMateriaSource: Interface for creating materias

Key features:

  • clone() method to duplicate materias
  • Characters have 4 inventory slots
  • MateriaSource can “learn” and “create” materias

Step 1 - Understand interfaces

An interface is a class with:

  • ONLY pure virtual functions
  • Virtual destructor
  • NO member variables (except constants)

Step 2 - The clone pattern

AMateria* Ice::clone() const {
return new Ice(*this); // Return a copy of myself
}

This allows creating copies without knowing the concrete type.

Step 3 - Memory ownership rules

  • equip(): Character takes ownership
  • unequip(): Character releases ownership (but doesn’t delete!)
  • createMateria(): Caller owns the returned materia

Stage 1 - AMateria base class:

AMateria.hpp
#ifndef AMATERIA_HPP
#define AMATERIA_HPP
#include <string>
class ICharacter; // Forward declaration
class AMateria {
protected:
std::string _type;
public:
AMateria(std::string const& type);
AMateria(const AMateria& other);
AMateria& operator=(const AMateria& other);
virtual ~AMateria();
std::string const& getType() const;
virtual AMateria* clone() const = 0; // Pure virtual
virtual void use(ICharacter& target);
};
#endif

Stage 2 - Concrete materia (Ice):

Ice.hpp
#ifndef ICE_HPP
#define ICE_HPP
#include "AMateria.hpp"
class Ice : public AMateria {
public:
Ice();
Ice(const Ice& other);
Ice& operator=(const Ice& other);
~Ice();
AMateria* clone() const;
void use(ICharacter& target);
};
#endif
Ice.cpp
#include "Ice.hpp"
#include "ICharacter.hpp"
#include <iostream>
Ice::Ice() : AMateria("ice") {}
Ice::Ice(const Ice& other) : AMateria(other) {}
Ice& Ice::operator=(const Ice& other) {
AMateria::operator=(other);
return *this;
}
Ice::~Ice() {}
AMateria* Ice::clone() const {
return new Ice(*this); // Return new copy
}
void Ice::use(ICharacter& target) {
std::cout << "* shoots an ice bolt at " << target.getName() << " *" << std::endl;
}

Stage 3 - ICharacter interface:

ICharacter.hpp
#ifndef ICHARACTER_HPP
#define ICHARACTER_HPP
#include <string>
class AMateria;
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;
};
#endif

Stage 4 - Character implementation:

Character.hpp
#ifndef CHARACTER_HPP
#define CHARACTER_HPP
#include "ICharacter.hpp"
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();
std::string const& getName() const;
void equip(AMateria* m);
void unequip(int idx);
void use(int idx, ICharacter& target);
};
#endif
Character.cpp
#include "Character.hpp"
#include "AMateria.hpp"
#include <iostream>
Character::Character(std::string name) : _name(name) {
for (int i = 0; i < 4; i++)
_inventory[i] = NULL;
}
Character::Character(const Character& other) : _name(other._name) {
for (int i = 0; i < 4; i++) {
if (other._inventory[i])
_inventory[i] = other._inventory[i]->clone(); // Deep copy!
else
_inventory[i] = NULL;
}
}
Character& Character::operator=(const Character& other) {
if (this != &other) {
_name = other._name;
// 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::~Character() {
for (int i = 0; i < 4; i++)
delete _inventory[i];
}
std::string const& Character::getName() const {
return _name;
}
void Character::equip(AMateria* m) {
if (!m) return;
for (int i = 0; i < 4; i++) {
if (!_inventory[i]) {
_inventory[i] = m;
return;
}
}
}
void Character::unequip(int idx) {
if (idx < 0 || idx >= 4) return;
_inventory[idx] = NULL; // Don't delete! Subject says so.
}
void Character::use(int idx, ICharacter& target) {
if (idx < 0 || idx >= 4 || !_inventory[idx]) return;
_inventory[idx]->use(target);
}

Stage 5 - MateriaSource:

MateriaSource.cpp
AMateria* MateriaSource::createMateria(std::string const& type) {
for (int i = 0; i < 4; i++) {
if (_templates[i] && _templates[i]->getType() == type)
return _templates[i]->clone(); // Return clone, not original!
}
return NULL; // Unknown type
}
LineCodeWhy
class ICharacter;Forward declarationAvoid circular dependency
virtual ~ICharacter() {}Virtual destructorRequired for interfaces
= 0Pure virtualMakes it interface
return new Ice(*this)Clone patternCreate copy without knowing type
_inventory[idx] = NULLunequipDon’t delete - subject requirement

1. Deleting in unequip()

// WRONG - subject says don't delete!
void Character::unequip(int idx) {
delete _inventory[idx]; // NO!
_inventory[idx] = NULL;
}
// RIGHT - just remove from inventory
void Character::unequip(int idx) {
_inventory[idx] = NULL;
// Dropped materia is caller's problem
}

2. Shallow copy of inventory

// WRONG - copies pointers, not materias!
for (int i = 0; i < 4; i++)
_inventory[i] = other._inventory[i];
// RIGHT - clone each materia
for (int i = 0; i < 4; i++)
_inventory[i] = other._inventory[i] ? other._inventory[i]->clone() : NULL;

3. Returning original instead of clone in createMateria

// WRONG - returns the template itself!
return _templates[i];
// RIGHT - return a new clone
return _templates[i]->clone();
int main() {
IMateriaSource* src = new MateriaSource();
src->learnMateria(new Ice());
src->learnMateria(new Cure());
ICharacter* me = new Character("me");
AMateria* tmp;
tmp = src->createMateria("ice");
me->equip(tmp);
tmp = src->createMateria("cure");
me->equip(tmp);
ICharacter* bob = new Character("bob");
me->use(0, *bob); // "* shoots an ice bolt at bob *"
me->use(1, *bob); // "* heals bob's wounds *"
delete bob;
delete me;
delete src;
return 0;
}
MateriaSource.cpp
#include "MateriaSource.hpp"
MateriaSource::MateriaSource() {
for (int i = 0; i < 4; i++)
_templates[i] = NULL;
}
MateriaSource::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& MateriaSource::operator=(const MateriaSource& other) {
if (this != &other) {
for (int i = 0; i < 4; i++)
delete _templates[i];
for (int i = 0; i < 4; i++) {
if (other._templates[i])
_templates[i] = other._templates[i]->clone();
else
_templates[i] = NULL;
}
}
return *this;
}
MateriaSource::~MateriaSource() {
for (int i = 0; i < 4; i++)
delete _templates[i];
}
void MateriaSource::learnMateria(AMateria* m) {
if (!m) return;
for (int i = 0; i < 4; i++) {
if (!_templates[i]) {
_templates[i] = m;
return;
}
}
}
AMateria* MateriaSource::createMateria(std::string const& type) {
for (int i = 0; i < 4; i++) {
if (_templates[i] && _templates[i]->getType() == type)
return _templates[i]->clone();
}
return NULL;
}

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)

Continue your C++ journey:

Visit the Glossary for definitions of: