๐งฌ 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.
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.
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().
| Term | Meaning |
|---|---|
| Base class (or parent/superclass) | Animal โ the class being inherited FROM |
| Derived class (or child/subclass) | Dog โ the class doing the inheriting |
| "Is-a" relationship | A Dog IS AN Animal โ inheritance should model a genuine "is-a" relationship, not just convenient code reuse |
๐ง 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.
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.
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.
๐๏ธ 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?
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.
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).
โ๏ธ Overriding Member Functions
What if a derived class needs to change HOW an inherited function behaves, not just add new functions of its own?
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.
Animal* ptr = new Dog(); // a Dog, accessed through an Animal pointer ptr->makeSound(); // prints "Some generic animal sound" โ NOT "Woof!"
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.๐ญ 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.
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.
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.
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.
๐จ 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.
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().
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.
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.
๐งน Virtual Destructors
A genuinely important, easy-to-miss rule connecting Phase 5's destructors, Phase 4's memory management, and this phase's polymorphism.
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!
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.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"
๐ 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.
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.
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.
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.๐ 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.
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.
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).
๐ 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.
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.
#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.")
Shapeis genuinely abstract (Module 6) โ you can never accidentally create a bare, meaninglessShape.- 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 baseShape*pointer.
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.