Skip to content

Module 04 Tutorial

Prerequisites: Review Module 04 Concepts first.

Module 04 introduces polymorphism - the ability to treat derived class objects through base class pointers while still calling the correct overridden functions. You’ll also learn about abstract classes and interfaces.


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;
}