TeqStudy ยท C++ Track ยท Phase 06

Inheritance & Polymorphism

Building new classes on top of existing ones, virtual functions and true runtime polymorphism, abstract classes and interfaces, and the virtual-destructor rule that prevents a real, common memory leak.

๐Ÿงฌ ๐ŸŽญ ๐ŸŽจ ๐Ÿ’Ž ๐Ÿ
Course Progress
0%
MODULE 01

๐Ÿงฌ Inheritance โ€” The Big Idea

Phase 5 built one self-contained class at a time. This phase covers building NEW classes that reuse and extend existing ones โ€” one of the defining ideas of object-oriented programming.

๐Ÿ• A Concrete Problem

Suppose you need both Dog and Cat classes โ€” both need a name, an age, and an eat() method, but each needs its own distinct makeSound(). Writing two completely separate classes duplicates the shared parts. Inheritance solves this: define the shared parts once, in a base class, and let specific classes extend it.

๐Ÿ”— A Base Class and a Derived Class
C++
class Animal {
public:
    std::string name;
    int age;

    void eat() {
        std::cout << name << " is eating." << std::endl;
    }
};

class Dog : public Animal {   // Dog INHERITS from Animal
public:
    void bark() {
        std::cout << name << " says woof!" << std::endl;
    }
};

Dog automatically gets name, age, AND eat() from Animal, without rewriting any of it โ€” it only needs to define what's genuinely NEW or different about being a Dog, which here is just bark().

๐Ÿ“– The Vocabulary
TermMeaning
Base class (or parent/superclass)Animal โ€” the class being inherited FROM
Derived class (or child/subclass)Dog โ€” the class doing the inheriting
"Is-a" relationshipA Dog IS AN Animal โ€” inheritance should model a genuine "is-a" relationship, not just convenient code reuse
๐Ÿง  Quick Check
If Dog inherits publicly from Animal, and Animal has a public eat() method, what happens when you create a Dog object?
MODULE 02

๐Ÿšง Protected Members

Phase 5's private keyword blocks ALL outside access โ€” including derived classes. This module introduces a third access level, built specifically for inheritance.

๐Ÿšซ The Problem with private in a Base Class
C++ โ€” this won't compile
class Animal {
private:
    int age;
};

class Dog : public Animal {
public:
    void birthday() {
        age++;   // COMPILER ERROR โ€” age is private to Animal, even Dog can't touch it!
    }
};

Even though Dog inherits from Animal, private means genuinely NO outside code can access it โ€” and a derived class counts as outside code for this purpose. This is often more restrictive than you actually want.

๐Ÿ”“ protected โ€” Visible to Derived Classes, Hidden From Everyone Else
C++
class Animal {
protected:
    int age;   // derived classes CAN access this
};

class Dog : public Animal {
public:
    void birthday() {
        age++;   // works fine now โ€” protected allows this
    }
};

Dog rex;
rex.age = 5;   // STILL a compiler error โ€” protected blocks OUTSIDE access, same as private

protected sits between public and private: accessible from the class itself AND any class that inherits from it, but still blocked from genuinely outside code โ€” the exact middle ground inheritance often needs.

๐Ÿง  Quick Check
What's the key difference between protected and private access?
MODULE 03

๐Ÿ—๏ธ Constructors in Derived Classes

If Animal has a constructor that sets up name and age, how does Dog make sure that setup still happens correctly?

๐Ÿ“ž Calling the Base Class's Constructor Explicitly
C++
class Animal {
protected:
    std::string name;
    int age;
public:
    Animal(std::string n, int a) : name(n), age(a) {}
};

class Dog : public Animal {
public:
    std::string breed;

    Dog(std::string n, int a, std::string b) : Animal(n, a), breed(b) {}
    // Animal(n, a) explicitly calls the base class's constructor
};

Dog's constructor calls Animal(n, a) in ITS OWN initializer list โ€” explicitly delegating the base class's setup to the base class's own constructor, rather than duplicating that logic. This is the standard, correct pattern: let each class handle initializing its own part.

๐Ÿ”ข The Order Things Actually Happen In

When you create a Dog object, C++ ALWAYS runs the base class's constructor FIRST, then the derived class's constructor body โ€” guaranteeing the Animal portion of the object is fully set up before Dog-specific setup runs, which is exactly the order that makes logical sense (you can't specialize a foundation that doesn't exist yet).

๐Ÿง  Quick Check
When a Dog object is created, in what order do the Animal and Dog constructors run?
MODULE 04

โœ๏ธ Overriding Member Functions

What if a derived class needs to change HOW an inherited function behaves, not just add new functions of its own?

โœ๏ธ Redefining a Function in the Derived Class
C++
class Animal {
public:
    void makeSound() {
        std::cout << "Some generic animal sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() {   // overrides Animal's version
        std::cout << "Woof!" << std::endl;
    }
};

Dog rex;
rex.makeSound();   // prints "Woof!" โ€” Dog's version is used

Defining a function in Dog with the EXACT same name (and parameters) as one in Animal overrides it โ€” when called on a Dog object, Dog's version runs instead of Animal's.

โš ๏ธ A Real Limitation This Simple Approach Has
C++ โ€” a genuine surprise
Animal* ptr = new Dog();   // a Dog, accessed through an Animal pointer
ptr->makeSound();          // prints "Some generic animal sound" โ€” NOT "Woof!"
โš ๏ธ
This is a genuinely important, surprising gapEven though ptr actually points to a real Dog object, calling makeSound() through an Animal* pointer uses ANIMAL's version, not Dog's โ€” because C++ decides which version to call based on the pointer's declared type, not the object's actual type, unless you explicitly tell it otherwise. That's exactly what the next module's virtual keyword fixes.
๐Ÿง  Quick Check
Without using virtual, if you call an overridden function through a BASE CLASS pointer that actually points to a derived object, which version runs?
MODULE 05

๐ŸŽญ virtual Functions โ€” True Polymorphism

The fix for last module's surprise, and the mechanism behind one of OOP's most powerful ideas: treating different types uniformly through a shared base type.

โœจ Adding virtual โ€” One Word Changes Everything
C++
class Animal {
public:
    virtual void makeSound() {   // virtual!
        std::cout << "Some generic animal sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Woof!" << std::endl;
    }
};

Animal* ptr = new Dog();
ptr->makeSound();   // NOW prints "Woof!" โ€” the ACTUAL object's type is used

Marking makeSound() as virtual in the base class tells C++ "figure out which version to call based on the object's REAL type, at runtime โ€” not just the pointer's declared type." This is called polymorphism ("many forms") โ€” the same call, ptr->makeSound(), genuinely behaves differently depending on what kind of object ptr actually points to.

๐Ÿท๏ธ override โ€” Catching Mistakes on Purpose

Adding override to a derived class's function tells the compiler "I intend for this to override a virtual base-class function โ€” verify that's actually true." If you make a typo in the function name or parameters, override turns what would otherwise be a silent, confusing bug (accidentally creating a NEW, unrelated function instead of overriding) into a clear compiler error instead.

๐ŸŽฏ Why Polymorphism Is Genuinely Powerful

Imagine an array of Animal* pointers, actually holding a mix of Dog, Cat, and Bird objects. With virtual functions, you can loop through and call makeSound() on every single one, identically โ€” and each object automatically responds with ITS OWN correct behavior, without your loop needing to know or check which specific type each one actually is. This is a genuinely different, more flexible style of code than constantly checking types with if/else chains.

๐Ÿง  Quick Check
What does marking a base class function as virtual actually change?
MODULE 06

๐ŸŽจ Abstract Classes & Pure Virtual Functions

Some base classes are genuinely too incomplete to represent a real object on their own. This module shows how to enforce that formally, rather than just by convention.

๐Ÿค” A Base Class That Shouldn't Be Created Directly

Does it make sense to create a bare Shape object? Not really โ€” a "shape" with no specific type is too vague to have a real, meaningful area(). But Circle and Rectangle, both being SPECIFIC shapes, genuinely do have well-defined areas. C++ lets you formally forbid creating a Shape directly, while still requiring every derived class to provide its own area().

๐Ÿšซ Pure Virtual Functions โ€” = 0
C++
class Shape {
public:
    virtual double area() = 0;   // pure virtual โ€” no implementation at all
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() override {
        return 3.14159 * radius * radius;
    }
};

The = 0 makes area() a pure virtual function โ€” Shape declares that every real shape MUST have an area(), without providing any implementation itself. A class containing at least one pure virtual function becomes an abstract class.

๐Ÿšง Abstract Classes Can't Be Instantiated Directly
C++
Shape s;          // COMPILER ERROR โ€” Shape is abstract
Circle c(5.0);    // fine โ€” Circle provides area(), so it's a complete, real class

The compiler itself enforces this โ€” attempting Shape s; is a genuine error, not just bad practice. This is exactly the "formal" enforcement mentioned at the top of this module, replacing what would otherwise just be a documented convention.

๐Ÿง  Quick Check
What makes a class "abstract" in C++?
MODULE 07

๐Ÿงน Virtual Destructors

A genuinely important, easy-to-miss rule connecting Phase 5's destructors, Phase 4's memory management, and this phase's polymorphism.

โš ๏ธ A Real, Serious Bug Without This
C++ โ€” a genuine memory leak
class Animal {
public:
    ~Animal() { std::cout << "Animal destroyed" << std::endl; }   // NOT virtual!
};

class Dog : public Animal {
public:
    int* data = new int[100];
    ~Dog() { delete[] data; std::cout << "Dog destroyed" << std::endl; }
};

Animal* ptr = new Dog();
delete ptr;   // ONLY prints "Animal destroyed" โ€” Dog's destructor NEVER RUNS!
โš ๏ธ
This is exactly Module 4's problem, applied to destructorsDeleting through a base class pointer, without a virtual destructor, only runs the BASE class's destructor โ€” Dog's destructor (and its cleanup of data) never executes at all, causing a genuine memory leak (Phase 4 Module 7), directly caused by this specific inheritance pitfall.
โœ… The Fix โ€” Mark the Base Destructor virtual
C++
class Animal {
public:
    virtual ~Animal() { std::cout << "Animal destroyed" << std::endl; }
};

Animal* ptr = new Dog();
delete ptr;   // NOW correctly prints "Dog destroyed" then "Animal destroyed"
๐Ÿ’ก
A genuine, well-known rule of thumbIf a class has ANY virtual functions at all (meaning it's likely to be used polymorphically, through base-class pointers), its destructor should almost always be virtual too โ€” this single habit prevents an entire, common class of real memory leaks.
๐Ÿง  Quick Check
Why does deleting a Dog object through a non-virtual Animal* pointer cause a real problem?
MODULE 08

๐Ÿ’Ž Multiple Inheritance & the Diamond Problem

C++ allows a class to inherit from MORE than one base class at once โ€” genuinely powerful, but with one well-known, real complication worth understanding.

๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง Inheriting From Two Classes at Once
C++
class Swimmer {
public:
    void swim() { std::cout << "Swimming" << std::endl; }
};

class Flyer {
public:
    void fly() { std::cout << "Flying" << std::endl; }
};

class Duck : public Swimmer, public Flyer {
    // Duck gets BOTH swim() and fly()
};

A Duck genuinely IS a swimmer AND a flyer โ€” multiple inheritance lets it gain behavior from both, rather than forcing an awkward single-parent hierarchy.

๐Ÿ’Ž The Diamond Problem โ€” A Real, Named Issue
Animal / \ Swimmer Flyer \ / Duck

If BOTH Swimmer and Flyer inherit from a common Animal base, Duck ends up with TWO separate copies of Animal's data โ€” one via each path โ€” creating genuine ambiguity about which copy a reference to an inherited Animal member actually means. This shape, drawn out, literally looks like a diamond, giving the problem its name.

โš ๏ธ
C++'s actual fix โ€” virtual inheritanceDeclaring class Swimmer : virtual public Animal (and the same for Flyer) tells C++ to share a SINGLE copy of Animal across both paths, resolving the ambiguity. This is genuinely advanced, rarely-needed territory โ€” the key beginner takeaway is recognizing the shape of the problem, not memorizing the fix syntax.
๐Ÿง  Quick Check
What specifically causes the "diamond problem" in multiple inheritance?
MODULE 09

๐Ÿ”Œ Interfaces in C++

C++ doesn't have a dedicated "interface" keyword like some other languages โ€” but Module 6's abstract classes already give you everything needed to build the exact same idea.

๐Ÿ”Œ What an "Interface" Really Means

An interface is a contract: "any class that implements me guarantees it provides these specific functions." It defines WHAT a class can do, without saying anything about HOW. In C++, this is simply an abstract class (Module 6) where EVERY function is pure virtual โ€” no shared implementation at all, purely a contract.

๐Ÿ“ A Pure Interface, C++ Style
C++
class Drawable {
public:
    virtual void draw() = 0;
    virtual ~Drawable() {}   // Module 7's rule โ€” virtual destructor
};

class Circle : public Drawable {
public:
    void draw() override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

Any class implementing Drawable guarantees it has a working draw() โ€” that's the entire contract. Multiple, genuinely unrelated classes (a Circle, a Button, an Icon) can all implement Drawable, letting code that only cares about "can this be drawn?" work with all of them uniformly, via polymorphism (Module 5).

๐Ÿง  Quick Check
How is an "interface" typically implemented in C++, given there's no dedicated interface keyword?
MODULE 10 ๐Ÿ

๐Ÿ Capstone โ€” A Shape Hierarchy

Every idea from this phase, combined: an abstract base class, pure virtual functions, overriding, a virtual destructor, and genuine polymorphism working through a collection of mixed types.

๐ŸŽฏ The Goal

Build an abstract Shape class with pure virtual area() and name() methods, two derived classes (Circle and Rectangle), and a function that computes the total area of a mixed collection of shapes โ€” without ever checking each shape's specific type.

๐Ÿ’ป A Reference Solution
main.cpp
#include <iostream>
#include <vector>

class Shape {
public:
    virtual double area() = 0;
    virtual std::string name() = 0;
    virtual ~Shape() {}
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() override { return 3.14159 * radius * radius; }
    std::string name() override { return "Circle"; }
};

class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() override { return width * height; }
    std::string name() override { return "Rectangle"; }
};

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle(5.0));
    shapes.push_back(new Rectangle(4.0, 6.0));

    double totalArea = 0;
    for (Shape* s : shapes) {
        std::cout << s->name() << " area: " << s->area() << std::endl;
        totalArea += s->area();
    }
    std::cout << "Total area: " << totalArea << std::endl;

    for (Shape* s : shapes) {
        delete s;   // virtual destructor ensures correct cleanup for each type
    }
    return 0;
}

(This uses std::vector as a resizable collection โ€” Phase 7 covers it properly. For now, just recognize push_back as "add an item.")

๐Ÿ” Why It's Built This Way
  • Shape is genuinely abstract (Module 6) โ€” you can never accidentally create a bare, meaningless Shape.
  • The loop calling s->area() works identically for every shape type โ€” real polymorphism (Module 5), with zero type-checking logic anywhere.
  • virtual ~Shape() (Module 7) guarantees each derived object's cleanup runs correctly when deleted through the base Shape* pointer.
๐Ÿ Extend It Yourself

Add a Triangle

Add a Triangle class (base ร— height / 2) to the hierarchy. Notice you don't need to touch the loop in main() at all โ€” this is precisely the real-world payoff of polymorphism: new types can be added without modifying code that already works with the shared base interface.

Roadmap