Module 04: Polymorphism, Abstract Classes, and Interfaces
Key Concepts:
- Virtual functions
- Runtime polymorphism
- Virtual destructors
- Abstract classes (pure virtual)
- Interfaces
- Deep copy with polymorphism
Why This Matters
Section titled “Why This Matters”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) Animalclass Cat : public Animal { }; // A Cat IS-A(n) AnimalBecause a Dog IS-A Animal, you can treat Dogs as Animals:
Animal* pet = new Dog(); // Valid! A Dog IS-A AnimalThis 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*.
Understanding Polymorphism Operators
Section titled “Understanding Polymorphism Operators”This module introduces virtual functions and abstract classes. Here’s the syntax explained:
The virtual Keyword (Function Modifier)
Section titled “The virtual Keyword (Function Modifier)”virtual void makeSound() { /* ... */ }virtualbefore 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 callDog::makeSound()if it actually points to a Dog
The = 0 Syntax (Pure Virtual)
Section titled “The = 0 Syntax (Pure Virtual)”virtual void makeSound() = 0; // Pure virtual function= 0makes 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
The * Operator (Polymorphic Pointers)
Section titled “The * Operator (Polymorphic Pointers)”Animal* pet = new Dog(); // Pointer to base, object is derivedpet->makeSound(); // Calls Dog::makeSound() (if virtual)*with polymorphism: declare pointers to base class, but point to derived objects->accesses members through the pointervirtualensures the correct (derived) function is called- Without
virtual, you’d always get the base class version
The & Operator (Polymorphic References)
Section titled “The & Operator (Polymorphic References)”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)
The delete Operator with Polymorphism
Section titled “The delete Operator with Polymorphism”Animal* pet = new Dog();delete pet; // Without virtual destructor, Dog destructor never runs!deleteon 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 petcalls~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
The () Operator (Cast)
Section titled “The () Operator (Cast)”dynamic_cast<Dog*>(animal); // Safe runtime castdynamic_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)
1. The Problem Without Polymorphism
Section titled “1. The Problem Without 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()2. Virtual Functions
Section titled “2. Virtual Functions”The Solution
Section titled “The Solution”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 runtimeHow Virtual Works
Section titled “How Virtual Works”// 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 calledVirtual Table Visualization
Section titled “Virtual Table Visualization”┌─────────────────────────────────────────────────────────────────┐│ 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 ││ │└─────────────────────────────────────────────────────────────────┘3. Virtual Destructors (CRITICAL!)
Section titled “3. Virtual Destructors (CRITICAL!)”The Problem
Section titled “The Problem”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!The Solution
Section titled “The Solution”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.
4. Abstract Classes
Section titled “4. Abstract Classes”What is an Abstract Class?
Section titled “What is an Abstract Class?”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 callmakeSound()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
Pure Virtual Functions
Section titled “Pure Virtual Functions”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 classAnimal* ptr; // OK: can have pointer to abstract classptr = new Dog(); // OK: can point to concrete derived classConcrete Derived Classes
Section titled “Concrete Derived Classes”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)Partially Abstract Classes
Section titled “Partially Abstract Classes”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};5. Interfaces
Section titled “5. Interfaces”What is an Interface?
Section titled “What is an Interface?”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;};Implementing an Interface
Section titled “Implementing an Interface”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);};6. Deep Copy with Polymorphism
Section titled “6. Deep Copy with Polymorphism”The Problem
Section titled “The Problem”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 pointerAnimal* original = new Dog();Animal* copy = new Animal(*original); // Creates Animal, not Dog!The Clone Pattern
Section titled “The Clone Pattern”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!Clone Pattern: Full Implementation
Section titled “Clone Pattern: Full Implementation”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); }};Memory Management with Clone
Section titled “Memory Management with Clone”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]; }};7. Arrays of Base Class Pointers
Section titled “7. Arrays of Base Class Pointers”Creating and Managing Polymorphic Arrays
Section titled “Creating and Managing Polymorphic Arrays”// 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 behaviorfor (int i = 0; i < 4; i++) { animals[i]->makeSound(); // Calls correct version}
// CRITICAL: Must delete each elementfor (int i = 0; i < 4; i++) { delete animals[i]; // Virtual destructor ensures proper cleanup}Why Virtual Destructor is Critical
Section titled “Why Virtual Destructor is Critical”// WITHOUT virtual destructor:Animal* pet = new Dog(); // Dog allocates Braindelete pet; // Only ~Animal() called - Brain leaked!
// WITH virtual destructor:Animal* pet = new Dog(); // Dog allocates Braindelete pet; // ~Dog() called first (deletes Brain), then ~Animal()8. Factory Pattern (MateriaSource)
Section titled “8. Factory Pattern (MateriaSource)”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; }};Factory Usage
Section titled “Factory Usage”IMateriaSource* src = new MateriaSource();
// Teach the factory what it can createsrc->learnMateria(new Ice());src->learnMateria(new Cure());
// Factory creates new instancesAMateria* ice = src->createMateria("ice"); // New Ice objectAMateria* cure = src->createMateria("cure"); // New Cure objectAMateria* unknown = src->createMateria("fire"); // NULL - not learned
delete src;Key Factory Pattern Points
Section titled “Key Factory Pattern Points”- Decouples creation from usage: Client doesn’t need to know concrete types
- Uses clone(): New objects are copies of templates
- Memory ownership: Factory owns templates, caller owns created objects
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;}Quick Reference
Section titled “Quick Reference”Virtual Function Syntax
Section titled “Virtual Function Syntax”virtual void method(); // Virtual (can override)virtual void method() = 0; // Pure virtual (must override)void method(); // Non-virtual (hides, doesn't override)Abstract Class
Section titled “Abstract Class”- Has at least one pure virtual function
- Cannot be instantiated
- Can have data members and non-pure methods
Interface
Section titled “Interface”- Only pure virtual functions
- Virtual destructor
- No data members (typically)
- Defines a contract
Memory Safety Checklist
Section titled “Memory Safety Checklist”- 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)
Related Concepts
Section titled “Related Concepts”Continue your C++ journey:
- Previous: Module 03: Inheritance - Review base/derived classes
- Next: Module 05: Exceptions - Learn error handling with try/catch/throw
Key Terms from This Module
Section titled “Key Terms from This Module”Visit the Glossary for definitions of: