Mastering C++ Inheritance, Polymorphism, and Exceptions

Understanding Object-Oriented Inheritance

Inheritance is a core pillar of Object-Oriented Programming (OOP) that allows a new class (derived class) to inherit properties and behaviors (data members and member functions) from an existing class (base class). This promotes code reusability and establishes an “is-a” relationship between classes.

1. Base Class vs. Derived Class

  • Base Class (Parent/Super Class): The existing class whose properties and methods are inherited.
  • Derived Class (Child/Sub Class): The new class that inherits from the base class. It can also add its own unique features or override existing ones.

Accessing Base Class Members

How a derived class accesses base class members depends on the Access Specifier used during inheritance:

class Derived : access_specifier Base { ... };
Base Class MemberPublic InheritanceProtected InheritancePrivate Inheritance
PublicStays PublicBecomes ProtectedBecomes Private
ProtectedStays ProtectedBecomes ProtectedBecomes Private
PrivateInaccessibleInaccessibleInaccessible

⚠️ Note: Private members of a base class are never directly accessible in the derived class. They can only be accessed via public or protected member functions of the base class.

2. Types of Inheritance

OOP supports multiple ways to structure relationships between classes.

1. Multilevel Inheritance

A class is derived from another derived class, creating a linear chain of inheritance.

  • Structure: A → B → C
  • Example: Vehicle → Car → SportsCar

2. Multiple Inheritance

A single derived class inherits from more than one base class.

  • Structure: A and B → C
  • Example: Teacher and Researcher → Professor

3. Hierarchical Inheritance

Multiple derived classes inherit from a single, common base class.

  • Structure: A → B and A → C
  • Example: Animal → Dog and Cat

4. Hybrid Inheritance

A combination of two or more types of inheritance. A classic example is combining hierarchical and multiple inheritance, often forming a “Diamond” structure.

3. The Diamond Problem & Virtual Base Class

When you combine hierarchical and multiple inheritance, you can run into a famous issue called The Diamond Problem.

The Problem

Imagine Class A has a member variable x.

  • Class B and Class C both inherit from A.
  • Class D inherits from both B and C.

Because of this, Class D receives two separate copies of Class A’s members (one via B, one via C). If you try to access x from an object of Class D, the compiler gets confused about which copy you mean, resulting in an ambiguity error.

The Solution: Virtual Base Class

To fix this, we use the virtual keyword when inheriting. This ensures that only one instance of the base class is shared among the child classes.

class A { public: int x; };

// Using virtual inheritance
class B : virtual public A { ... };
class C : virtual public A { ... };

class D : public B, public C { ... }; // D now has exactly ONE copy of x

4. Abstract Class

An Abstract Class is a class designed specifically to act as a blueprint for other classes. You cannot create an object (instantiate) of an abstract class directly.

  • How it’s made: In C++, a class becomes abstract if it contains at least one Pure Virtual Function.
  • Purpose: It enforces a contract. Any derived class must provide an implementation for the pure virtual functions, or it will also become an abstract class.
class Shape { // Abstract Class
public:
    // Pure virtual function
    virtual void draw() = 0; 
};

class Circle : public Shape {
public:
    // Overriding the pure virtual function
    void draw() override {
        // Code to draw a circle
    }
};

Polymorphism and Its Types

Polymorphism—meaning “many forms”—is the ability of a message, data, or object to be processed in more than one form. In C++, it is broadly divided into two types: Compile-time and Runtime polymorphism.

1. Compile-Time Polymorphism (Static Binding)

The compiler selects the appropriate function or operator at compile time based on the arguments passed.

  • Function Overloading: Multiple functions share the same name but have different parameter lists.
  • Operator Overloading: Giving special meanings to standard operators (+, -, <<, etc.) when applied to user-defined data types.

2. Runtime Polymorphism (Dynamic Binding)

The function call is resolved at runtime based on the actual object being pointed to. This is achieved using Virtual Functions and pointers/references.

Virtual vs. Pure Virtual Functions

FeatureVirtual FunctionPure Virtual Function
DefinitionA member function in a base class that you expect to override.A virtual function with no implementation that must be overridden.
Syntaxvirtual void display() { … }virtual void display() = 0;
Object CreationYou can create instances of the class.Class becomes Abstract and cannot be instantiated.
RequirementOverriding is optional.Overriding is mandatory.

Exception Handling in C++

Exception handling is a mechanism designed to handle runtime errors so that the program doesn’t crash unexpectedly.

The Exception Handling Model

C++ uses a structured, synchronous exception-handling model based on three main constructs: try, throw, and catch.

  • try block: Wraps the segment of code where an exception might occur.
  • throw statement: Used to intentionally signal the occurrence of an error.
  • catch block: Placed after a try block to intercept and handle the thrown exception.
#include <iostream>
#include <stdexcept>

int divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Division by zero error!");
    }
    return a / b;
}

int main() {
    try {
        int result = divide(10, 0);
        std::cout << "Result: " << result << std::endl;
    } 
    catch (const std::runtime_error& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

Advanced Exception Handling

1. Order of Catch Blocks

The compiler executes the first block that matches the type of the exception.

⚠️ Critical Rule: Always place catch blocks for derived (specific) exception classes before base (generic) exception classes.

2. Catching All Exceptions (catch(…))

The catch-all handler, denoted by three dots (…), must always be placed last in your chain of catch blocks.

3. Nested Try Blocks

You can place a try-catch block inside another. Use the throw; statement to rethrow an exception to the outer layer.

4. Handling Uncaught Exceptions

If no matching catch block is found, the runtime calls std::terminate(). You can customize this behavior using std::set_terminate().