Updated on 29 Jun, 202657 mins read 54 views

Introduction

Imagine someone asks you to build software for a zoo.

You identity the following animals:

  • Dog
  • Cat
  • Elephant
  • Lion
  • Tiger
  • Horse

You begin writing classes.

class Dog
{
public:
    void eat() {}
    void sleep() {}
    void breathe() {}
};

class Cat
{
public:
    void eat() {}
    void sleep() {}
    void breathe() {}
};

class Lion
{
public:
    void eat() {}
    void sleep() {}
    void breathe() {}
};

After writing only three classes, something feels wrong.

The code is almost identical.

If you continue, you will duplicate the same function dozens of times.

Now consider another example:

Vehicles.

  • Car
  • Bus
  • Truck
  • Motorcyle

Again:

class Car
{
public:
    void startEngine() {}
    void stopEngine() {}
};

class Bus
{
public:
    void startEngine() {}
    void stopEngine() {}
};

class Truck
{
public:
    void startEngine() {}
    void stopEngine() {}
};

The same pattern appears.

We have many objects that share common behavior.

This problem existed long before C++.

Early software systems often duplicated logic across multiple modules, making maintenance difficult and error-prone.

Inheritance was introduced to solve this problem.

Historical Context

To understand inheritance, we must first understand the programming world before object-oriented programming.

Procedural Programming Era

Languages like C focused on functions.

Data and behavior were separate.

For example:

struct Employee
{
    char name[50];
    int age;
};

void printEmployee(Employee* e);
void calculateSalary(Employee* e);
void promote(Employee* e);

As systems grew, many structures became similar.

For example:

Employee
Manager
Developer
Tester
Intern

Each structure contained common information:

Name
Age
ID

Programmers repeatedly copied fields and functions.

This led to:

  • Massive code duplication.
  • Inconsistent behavior.
  • Difficult maintenance.

The question became:

β€œWhy are we rewriting the same logic over and over?”

Object-oriented programming answered this with inheritance.

What Is Inheritance?

Definition

Inheritance is a mechanism by which one class acquires the properties and behaviors of another class.

The existing class is called the base class (or parent class).

The new class is called the derived class (or child class).

Textually:

           Animal
              β–²
              β”‚
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚                 β”‚
    Dog               Cat

Instead of writing common functionality multiple times.

class Animal
{
public:
    void eat()
    {
        std::cout << "Eating\n";
    }

    void sleep()
    {
        std::cout << "Sleeping\n";
    }
};

Dog inherits it:

class Dog : public Animal
{
};

Cat inherits it:

class Cat : public Animal
{
};

Usage:

Dog dog;
dog.eat();
dog.sleep();

Cat cat;
cat.eat();

Understanding Generalization

Many beginners think inheritance and generalization are identical.

They are related, but not the same.

This distinction is critical.

Generalization

Generalization is a design activity.

It happens during analysis.

We ask:

β€œWhat characteristics are common among these objects”

Consider:

Dog
Cat
Horse
Tiger

All of them:

  • Eat
  • Sleep
  • Breathe
  • Reproduce

We generalize them into:

Animal

Diagram:

Dog
Cat
Horse
Tiger

↓

Animal

Generalization is a way of discovering abstractions.

Inheritance

Inheritance is the programming mechanism used to implement that abstraction.

Generalization exists in the problem domain.

Inheritance exists in the programming language.

A useful mental model is:

Generalization
        ↓
Design Decision
        ↓
Inheritance
        ↓
Implementation

The β€œIs-A” Relationship

Inheritance represents an Is-A relationship.

Ask yourself:

β€œIs this object a specialized version of another object?”

Examples:

Dog IS-A Animal

Cat IS-A Animal

Bus IS-A Vehicle

Square IS-A Shape

CheckingAccount IS-A BankAccount

These are valid inheritance relationships.

Now consider:

Engine IS-A Car

No.

An engine is part of a car.

That's not inheritance.

It's composition.

Similarly:

Student IS-A University

Wrong.

A student belongs to a university.

Not inheritance.

Another example:

Keyboard IS-A Computer

Wrong.

The keyboard is part of a computer.

Always ask the Is-A question.

If the answer is "No,", inheritance is probably incorrect.

Mental Model: Specialization, Not Duplication

Many people learn inheritance as:

β€œA way to reuse code.”

While this is true, it is not the most important reason to use inheritance.

The deeper purpose is specialization.

Think of inheritance as answering the question:

β€œHow is this concept a more specialized version of another concept?”

Code reuse is a consequence – not the primary goal.

If your only reason for inheritance is β€œto avoid duplicate code”, you are likely to create brittle class hierarchies.

Professional designer first ask:

  1. Is there a true Is-A relationship?
  2. Does the derived class preserve the meaning of the base class?
  3. Will this hierarchy remain valid as the software evolves?

If the answer to any of these is β€œno”, inheritance is probably the wrong tool.

Basic Syntax of Inheritance

The general syntax is:

class DerivedClass : access_specifier BaseClass
{
    // Additional members
};

Example:

class Animal
{
public:
    void eat()
    {
        std::cout << "Eating...\n";
    }
};

class Dog : public Animal
{
};

Usage:

Dog dog;
dog.eat();

Output:

Eating...

Notice something interesting.

We never wrote an eat() function inside Dog.

Yet it works.

Why?

Because Dog inherits it from Animal.

Visualizing the Relationship

Although Dog contains no explicit members:

class Dog : public Animal
{
};

You can mentally think of it like this:

Dog

-----------------------

Inherited:

eat()

-----------------------

Or, if Animal also had data:

class Animal
{
protected:
    std::string name;
    int age;

public:
    void eat();
    void sleep();
};

Then Dog conceptually becomes:

Dog

--------------------------------

Inherited Data

name

age

--------------------------------

Inherited Behavior

eat()

sleep()

--------------------------------

The derived object contains the base object as part of its memory layout.

Constructors and Inheritance

One of the biggest misconceptions is:

β€œConstructors are inherited.”

They are not.

Consider:

class Animal
{
public:
    Animal()
    {
        std::cout << "Animal created\n";
    }
};

class Dog : public Animal
{
};

Usage:

Dog dog;

Output:

Animal created

Did Dog inherit the constructor?

No.

What happened?

Before constructing the Dog part, C++ first constructs the Animal part.

Construction order:

Create Dog

  ↓

Construct Animal

  ↓

Construct Dog

Think of building a house.

You don't build the roof first.

You build the foundation.

The base class is the foundation.

Constructor Chaining

Suppose:

class Animal
{
public:
    Animal()
    {
        std::cout << "Animal\n";
    }
};

class Dog : public Animal
{
public:
    Dog()
    {
        std::cout << "Dog\n";
    }
};

Execution:

Dog dog;

Output:

Animal
Dog

Always remember.

Base constructors execute before derived constructors.

Passing Parameters to the Base Class

Suppose the base class needs information.

class Animal
{
protected:
    std::string name;

public:
    Animal(std::string n)
        : name(n)
    {
    }
};

The derived class must explicitly call it.

class Dog : public Animal
{
public:
    Dog(std::string n)
        : Animal(n)
    {
    }
};

Usage:

Dog dog("Buddy");

Execution:

Animal("Buddy")

  ↓

Dog("Buddy")

The derived constructor initializes the base constructor.

Destruction Order

Construction proceeds.

Animal

 ↓

Dog

Destruction proceeds in reverse.

Dog

 ↓

Animal

Example:

class Animal
{
public:
    ~Animal()
    {
        std::cout << "Animal destroyed\n";
    }
};

class Dog : public Animal
{
public:
    ~Dog()
    {
        std::cout << "Dog destroyed\n";
    }
};

Output:

Dog destroyed

Animal destroyed

Why?

Because C++ destroys objects in the opposite order in which they were created.

Access Specifiers in Inheritance

Remember:

Classes already have access specifiers.

public

protected

private

Inheritance introduces another access specifier:

class Dog : public Animal

Notice the extra public.

That is called the inheritance mode.

C++ supports three modes:

public inheritance

protected inheritance

private inheritance

Each changes the accessibility of inherited members.

Public Imheritance

class Animal
{
public:
    void eat();

protected:
    int age;

private:
    int secret;
};

class Dog : public Animal
{
};

Accessibility becomes:

AnimalDog
publicpublic
protectedprotected
privateinaccessible

Nothing changes except that private members remain inaccessible.

This module a genuine Is-A relationship.

Example:

Dog dog;

dog.eat();

Protected Inheritance

class Dog : protected Animal
{
};

Transformation:

AnimalDog
publicprotected
protectedprotected
privateinaccessible

Notice:

Everything public becomes protected.

Outside users cannot access it.

Example:

Dog dog;
dog.eat(); // Error

Inside Dog, however:

class Dog : protected Animal
{
public:
    void test()
    {
        eat();
    }
};

This works.

Private Inheritance

class Dog : private Animal
{
};

Transformation:

AnimalDog
publicprivate
protectedprivate
privateinaccessible

Everything becomes private.

Outside code cannot access inherited members.

Member Functions in an Inheritance Hierarchy

So, far, we have inherited functions exactly as they were.

class Animal
{
public:
    void eat()
    {
        std::cout << "Animal is eating\n";
    }
};

class Dog : public Animal
{
};

Usage:

Dog dog;

dog.eat();

Output:

Animal is eating

The derived class simply inherited the function.

But what happens when the derived class wants different behavior?

Why Should a Derived Class Change Behavior?

Consider animals.

Every animal makes a sound.

But not every animal makes the same sound.

Animal
↓
Dog      β†’ Bark
Cat      β†’ Meow
Cow      β†’ Moo
Lion     β†’ Roar

Clearly,

Animal::makeSound()

cannot have one implementation that works for every animal.

Each dervied class must specialize the behavior/

This leads us to function overriding.

Function Overriding

Suppose we write

class Animal
{
public:
    void makeSound()
    {
        std::cout << "Some generic sound\n";
    }
};

class Dog : public Animal
{
public:
    void makeSound()
    {
        std::cout << "Bark\n";
    }
};

Usage:

Dog dog;

dog.makeSound();

Output:

Bark

Looks good.

Now let's try something interesting.

Animal animal;
animal.makeSound();

Output:

Some generic sound

Still good.

So, what's the problem?

The problem appears when we use base-class pointers.

The Surprising Behavior

Suppose:

Dog dog;

Animal* animal = &dog;
animal.makeSound();

What should happens?

Many would answer:

Bark

Actual output:

Some generic sound

Why?

Because the compiler only sees

Animal*

The derived implementation is ignored.

This surprises almost everyone.

Static Binding

This behavior is called static binding.

The compiler decides at compile time which function to call.

Compiler
  ↓
Pointer Type
  ↓
Function Selection

Since the pointer is:

Animal*

the compiler chooses

Animal::makeSound()

It never looks at the real object.

Why Is This a Problem?

Imagine a Zoo application.

Dog
Cat
Lion
Elephant
Horse

Suppose we store them as:

Animal*
std::vector<Animal*> animals;

Now we write:

for (Animal* animal : animals)
{
    animal->makeSound();
}

Expected output:

Bark
Meow
Roar
Trumpet
Neigh

Actual output:

Generic Sound
Generic Sound
Generic Sound
Generic Sound

Completely useless.

Inheritance alone cannot solve this.

Dynamic Dispatch

What we actually want is:

Animal*
 ↓
Look at actual object
 ↓
Call correct function

Instead of

Animal*
 ↓
Call Animal function

This mechanism is called Dynamic  Dispatch.

C++ implements it using the keyword: virtual

Virtual Functions

Now modify the base class:

class Animal
{
public:

    virtual void makeSound()
    {
        std::cout << "Generic Sound\n";
    }
};

Derived class

class Dog : public Animal
{
public:

    void makeSound() override
    {
        std::cout << "Bark\n";
    }
};

Usage:

Dog dog;
Animal* animal = &dog;
animal->makeSound();

Output:

Bark

Exactly what we wanted.

Why Does virtual Work?

Without virtual

Compiler

↓

Pointer Type

↓

Call Function

With virtual:

Compiler

↓

Object Type at Runtime

↓

Call Function

Notice the difference. The decision is delayed until the program is actually running.

Hence the name, Runtime Polymorphism.

The override Keyword

Modern C++ introduced override

Example:

class Animal
{
public:

    virtual void speak();
};

class Dog : public Animal
{
public:

    void speak() override;
};

Why use it?

Suppose you accidently write:

class Dog : public Animal
{
public:

    void speek() override;
};

Compiler:

Error

Without override, the compiler would silently create a new function.

This is one of the most useful safely features in modern C++.

Professional recommendation:

Always use override when overriding a virtual function.

Function Hiding

Function Hiding and Function Overriding are completely different:

Function hiding occurs when a function in a derived class has the same name as a function in the base class. The derived class function hides all base class functions with that name, even if the  parameter lists are different.

Consider:

#include <iostream>
using namespace std;

class Base {
public:
    void display() {
        cout << "Base display()" << endl;
    }

    void display(int x) {
        cout << "Base display(int): " << x << endl;
    }
};

class Derived : public Base {
public:
    void display(double x) {
        cout << "Derived display(double): " << x << endl;
    }
};

int main() {
    Derived d;

    d.display(3.14);   // Calls Derived::display(double)

    // d.display();    // Error: Base::display() is hidden
    // d.display(10);  // Error: Base::display(int) is hidden

    return 0;
}

Why does this happen?

When the compiler sees that Derived defines a function named display, it hides all display functions from Base, regardless of their signatures.

How to access the hidden base class functions

Use the using declaration:

#include <iostream>
using namespace std;

class Base {
public:
    void display() {
        cout << "Base display()" << endl;
    }

    void display(int x) {
        cout << "Base display(int): " << x << endl;
    }
};

class Derived : public Base {
public:
    using Base::display;   // Bring all Base::display overloads into scope

    void display(double x) {
        cout << "Derived display(double): " << x << endl;
    }
};

int main() {
    Derived d;

    d.display();      // Base display()
    d.display(10);    // Base display(int): 10
    d.display(3.14);  // Derived display(double)

    return 0;
}

Another way

You can explicitly call the base class version.

d.Base::display();
d.Base::display(10);

Calling Base-Class Implementations

Sometimes the derived class wants to extend behavior rather than replace it.

Example:

class Animal
{
public:

    virtual void move()
    {
        std::cout << "Moving\n";
    }
};

Derived class:

class Dog : public Animal
{
public:

    void move() override
    {
        Animal::move();

        std::cout << "Running\n";
    }
};

Output:

Moving
Running

Multi-Level Inheritance

Unlike many languages,

C++ allows:

class C : public A, public B
{
};

Example:

class Printer
{
};

class Scanner
{
};

class AllInOne :

    public Printer,

    public Scanner
{
};

This seems useful.

But it introduces one of the biggest problem in C++, The Diamond Problem.

The Diamond Problem

Imagine:

          Animal

         /      \

    Mammal      Bird

         \      /

        FlyingBat

Now suppose Animal contains age

Question: How many copies of age does FlyingBat have?

Two, One through Mammel and the other one through Bird.

Now:

Flying.age

becomes ambiguous.

Compiler Error, this is famous Diamond Problem.

Virtual Inheritance

C++ solves it using:

virtual

inheritance.

class Animal
{
};

class Mammal :

    virtual public Animal
{
};

class Bird :

    virtual public Animal
{
};

class FlyingBat :

    public Mammal,

    public Bird
{
};

Now there is only one Animal object.

Why Multiple Inheritance Is Rare

Although C++ supports it, large software systems use it sparingly.

Reasons:

  • Increased complexity
  • Ambiguous member access
  • Diamond Problem
  • Harder maintenance
  • Difficult mental model

Composition usually produces cleaner designs.

 

Buy Me A Coffee

Leave a comment

Your email address will not be published. Required fields are marked *