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.
Exercise 00: Polymorphism
Section titled “Exercise 00: Polymorphism”Subject Analysis
Section titled “Subject Analysis”Create an Animal base class with Dog and Cat derived classes:
- Animal has a
typeattribute andmakeSound()method - Dog says “Woof!”, Cat says “Meow!”
- The key: When using an
Animal*pointer to aDog, callingmakeSound()should output “Woof!”, not a generic sound
Also create “Wrong” versions to demonstrate what happens without polymorphism.
Approach Strategy
Section titled “Approach Strategy”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.
Progressive Code Building
Section titled “Progressive Code Building”Stage 1 - Base class with virtual functions:
#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;};
#endifStage 2 - Derived class overriding:
#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#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):
class WrongAnimal {protected: std::string _type;
public: WrongAnimal(); ~WrongAnimal(); // NOT virtual!
void makeSound() const; // NOT virtual! std::string getType() const;};Line-by-Line Explanation
Section titled “Line-by-Line Explanation”| Line | Code | Why |
|---|---|---|
virtual ~Animal() | Virtual destructor | Ensures derived destructor is called through base pointer |
virtual void makeSound() const | Virtual function | Enables runtime polymorphism |
void makeSound() const (in Dog) | Override | Provides Dog-specific implementation |
_type = "Dog" | Set type in derived | Identify the actual type |
Common Pitfalls
Section titled “Common Pitfalls”1. Forgetting virtual keyword
// WRONG - no polymorphismclass Animal {public: void makeSound() const; // Not virtual};
Animal* pet = new Dog();pet->makeSound(); // Calls Animal::makeSound(), NOT Dog!
// RIGHT - polymorphicclass Animal {public: virtual void makeSound() const;};2. Non-virtual destructor causes memory leaks
// WRONGclass Animal {public: ~Animal(); // NOT virtual};
Animal* pet = new Dog();delete pet; // Only ~Animal() called! ~Dog() skipped!
// RIGHTclass Animal {public: virtual ~Animal(); // Virtual destructor};Testing Tips
Section titled “Testing Tips”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;}Final Code
Section titled “Final Code”#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”Subject Analysis
Section titled “Subject Analysis”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.
Approach Strategy
Section titled “Approach Strategy”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.
Progressive Code Building
Section titled “Progressive Code Building”Stage 1 - Brain class:
#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#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):
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::Dog() : Animal() { _type = "Dog"; _brain = new Brain(); // Allocate Brain std::cout << "Dog constructed" << std::endl;}
// Deep copy - create NEW BrainDog::Dog(const Dog& other) : Animal(other) { _brain = new Brain(*other._brain); // Copy Brain content std::cout << "Dog copy constructed" << std::endl;}
// Deep copy assignmentDog& 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;}Line-by-Line Explanation
Section titled “Line-by-Line Explanation”| Line | Code | Why |
|---|---|---|
Brain* _brain | Pointer to Brain | Dynamic allocation needed |
_brain = new Brain() | Allocate in constructor | Each Dog gets its own Brain |
_brain = new Brain(*other._brain) | Deep copy | Create NEW Brain with same content |
delete _brain | In assignment | Free OLD Brain before new allocation |
delete _brain | In destructor | Clean up memory |
Common Pitfalls
Section titled “Common Pitfalls”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 BrainDog::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 Braindelete 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;}
// RIGHTDog& Dog::operator=(const Dog& other) { delete _brain; // Free old _brain = new Brain(*other._brain); return *this;}Testing Tips
Section titled “Testing Tips”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;}Final Code
Section titled “Final Code”#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;}Exercise 02: Abstract Class
Section titled “Exercise 02: Abstract Class”Subject Analysis
Section titled “Subject Analysis”Make Animal abstract - it should not be instantiable directly.
- Rename
AnimaltoAAnimal(theAprefix indicates abstract) - Make
makeSound()a pure virtual function AAnimal a;should now be a compile error
Approach Strategy
Section titled “Approach Strategy”Step 1 - Add = 0 to make pure virtual
virtual void makeSound() const = 0; // Pure virtualStep 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
Progressive Code Building
Section titled “Progressive Code Building”#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;};
#endifLine-by-Line Explanation
Section titled “Line-by-Line Explanation”| Line | Code | Why |
|---|---|---|
= 0 | Pure virtual specifier | Makes class abstract |
AAnimal | A prefix | Convention indicating abstract class |
Common Pitfalls
Section titled “Common Pitfalls”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 classDog d;AAnimal* ptr = new Dog();Testing Tips
Section titled “Testing Tips”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;}Final Code
Section titled “Final Code”#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;};
#endifExercise 03: Interface & Recap
Section titled “Exercise 03: Interface & Recap”Subject Analysis
Section titled “Subject Analysis”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
Approach Strategy
Section titled “Approach Strategy”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 ownershipunequip(): Character releases ownership (but doesn’t delete!)createMateria(): Caller owns the returned materia
Progressive Code Building
Section titled “Progressive Code Building”Stage 1 - AMateria base class:
#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);};
#endifStage 2 - Concrete materia (Ice):
#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#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:
#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;};
#endifStage 4 - Character implementation:
#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#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:
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}Line-by-Line Explanation
Section titled “Line-by-Line Explanation”| Line | Code | Why |
|---|---|---|
class ICharacter; | Forward declaration | Avoid circular dependency |
virtual ~ICharacter() {} | Virtual destructor | Required for interfaces |
= 0 | Pure virtual | Makes it interface |
return new Ice(*this) | Clone pattern | Create copy without knowing type |
_inventory[idx] = NULL | unequip | Don’t delete - subject requirement |
Common Pitfalls
Section titled “Common Pitfalls”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 inventoryvoid 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 materiafor (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 clonereturn _templates[i]->clone();Testing Tips
Section titled “Testing Tips”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;}Final Code
Section titled “Final Code”#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;}