Module 04: Polymorphism, Abstract Classes, and Interfaces
Key Concepts:
- Virtual functions
- Runtime polymorphism
- Virtual destructors
- Abstract classes (pure virtual)
- Interfaces
- Deep copy with polymorphism
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. Module 04 Exercise Structure
Section titled “8. Module 04 Exercise Structure”ex00: Polymorphism
Section titled “ex00: Polymorphism”class Animal {protected: std::string _type;public: Animal(); Animal(const Animal& other); Animal& operator=(const Animal& other); virtual ~Animal();
virtual void makeSound() const; std::string getType() const;};
class Dog : public Animal {public: Dog(); Dog(const Dog& other); Dog& operator=(const Dog& other); ~Dog();
void makeSound() const; // Barks};
class Cat : public Animal {public: Cat(); Cat(const Cat& other); Cat& operator=(const Cat& other); ~Cat();
void makeSound() const; // Meows};
// Also implement WrongAnimal/WrongCat without virtual// to demonstrate the differenceex01: Deep Copy
Section titled “ex01: Deep Copy”class Brain {public: std::string ideas[100];
Brain(); Brain(const Brain& other); Brain& operator=(const Brain& other); ~Brain();};
class Dog : public Animal {private: Brain* _brain; // Dynamically allocatedpublic: Dog(); Dog(const Dog& other); // Must deep copy brain Dog& operator=(const Dog& other); // Must deep copy brain ~Dog(); // Must delete brain};
// Test deep copy:Dog original;Dog copy = original;// Modifying copy's brain should NOT affect original's brainex02: Abstract Class
Section titled “ex02: Abstract Class”// Make Animal abstract (cannot instantiate)class Animal {public: virtual void makeSound() const = 0; // Pure virtual // ...};
Animal pet; // ERROR: Animal is abstractAnimal* ptr = new Dog(); // OKex03: Interfaces (Materia System)
Section titled “ex03: Interfaces (Materia System)”class AMateria {protected: std::string _type;public: AMateria(std::string const& type); virtual ~AMateria();
std::string const& getType() const; virtual AMateria* clone() const = 0; virtual void use(ICharacter& target);};
class Ice : public AMateria {public: Ice(); Ice(const Ice& other); Ice& operator=(const Ice& other); ~Ice();
AMateria* clone() const; void use(ICharacter& target);};
class Cure : public AMateria { // Similar to Ice};
class Character : public ICharacter {private: std::string _name; AMateria* _inventory[4];public: // Implement all ICharacter methods // Handle equip/unequip memory carefully!};
class MateriaSource : public IMateriaSource { // Learn and create Materias};9. Common Patterns
Section titled “9. Common Patterns”Factory Pattern (MateriaSource)
Section titled “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
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)