Understanding Inheritance, Polymorphism, and Virtual Mechanisms in C++

Inheritance Access Levels

A derived class inherits all members from its base class except constructors, destructors, and assignment operators. While private members of the base are inherited, they remain hidden from the derived class and cannot be accessed directly.

class Parent {
public:
    int x;
protected:
    int y;
private:
    int z;
};

class Child : public Parent {
public:
    int w;
};

void checkSize() {
    std::cout << sizeof(Child) << std::endl;
}

Object Construction and Relationships

Creating a distinct base class object and a derived class object does not establish a hierarchical relationship between those two specific instances. The derived object implicitly contains its own base subobject constructed during the derived object's creation.

class Parent {
public:
    int x;
protected:
    int y;
private:
    int z;
};

class Child : public Parent {
public:
    int w;
};

void checkInstances() {
    Child child;
    Parent parent;

    child.x = 10;
    parent.x = 20;

    std::cout << "Child instance x: " << child.x << std::endl;
    std::cout << "Parent instance x: " << parent.x << std::endl;
}

When a derived object is instantiated, the compiler implicitly constructs the base portion first. Destruction occurs in reverce order.

While a derived type can often be treated as a base type (upcasting), the reverse is not allowed. Direct conversion between sibling derived classes is also invalid.

class Creature {
public:
    virtual void makeSound() {}
};

class Feline : public Creature {
public:
    void makeSound() override {}
};

class Canine : public Creature {
public:
    void makeSound() override {}
};

void processSound(Creature& c) {
    c.makeSound();
}

void testCasts() {
    Creature creature;
    Feline feline;
    Canine canine;

    processSound(creature);
    processSound(feline);
    // processSound(canine); // Error if trying to pass canine where Feline is expected without cast
}

Handling Member Name Conflicts

If a derived class defines a member with the same name as one in the base class, the base class member is hidden. Use the scope resolution operator to access the base version.

Multiple Inheritance and the Diamond Problem

In scenarios where a class inherits from multiple classes that share a common ancestor, ambiguity arises (the Diamond Problem). The descendant holds multiple copies of the base ancestor's data.

class LivingBeing {
public:
    int lifespan;
};

class Bird : public LivingBeing {};
class Fish : public LivingBeing {};

class FlyingFish : public Bird, public Fish {};

int main() {
    FlyingFish ff;
    // ff.lifespan = 10; // Error: Ambiguous
    ff.Bird::lifespan = 5;
    ff.Fish::lifespan = 10;
    return 0;
}

Virtual Inheritance

Virtual inheritance ensures only one instance of the shared base class exists.

class LivingBeing {
public:
    int lifespan;
};

class Bird : virtual public LivingBeing {};
class Fish : virtual public LivingBeing {};

class FlyingFish : public Bird, public Fish {};

int main() {
    FlyingFish ff;
    ff.lifespan = 15; // Now unambiguous
    std::cout << "Lifespan: " << ff.lifespan << std::endl;
    return 0;
}

Internally, the compiler uses virtual base pointers (vbptr) in the intermediate classes that point to virtual base tables. These tables contain offsets to locate the single shared instance of the base class member.

Static vs Dynamic Polymorphism

Static Polymorphism (Early Binding)

Function calls are resolved at compile time based on the type of the pointer or reference.

class Vehicle {
public:
    void move() {
        std::cout << "Vehicle moving" << std::endl;
    }
};

class Car : public Vehicle {
public:
    void move() {
        std::cout << "Car driving" << std::endl;
    }
};

void operate(Vehicle& v) {
    v.move(); // Calls Vehicle::move regardless of actual object type
}

void testStatic() {
    Car myCar;
    operate(myCar);
}

Dynamic Polymorphism (Late Binding)

Using virtual functions allows the decision of which function to call to be made at runtime based on the object's actual type.

class Vehicle {
public:
    virtual void move() {
        std::cout << "Vehicle moving" << std::endl;
    }
};

class Car : public Vehicle {
public:
    void move() override {
        std::cout << "Car driving" << std::endl;
    }
};

void operate(Vehicle& v) {
    v.move(); // Calls Car::move if v refers to a Car
}

void testDynamic() {
    Car myCar;
    operate(myCar);
}

Polymorphism in Practice

Consider a calculator. Without polymorphism, adding new operations requires modifying existing logic.

class BasicCalc {
public:
    int calculate(char op) {
        if (op == '+') return val1 + val2;
        if (op == '-') return val1 - val2;
        return 0;
    }
    int val1, val2;
};

With polymorphism, you extend functionality by adding new classes.

class BaseCalc {
public:
    virtual int compute() { return 0; }
    int val1, val2;
};

class Adder : public BaseCalc {
    int compute() override { return val1 + val2; }
};

class Subtractor : public BaseCalc {
    int compute() override { return val1 - val2; }
};

void testPolyCalc() {
    BaseCalc* calc = new Adder;
    calc->val1 = 100;
    calc->val2 = 50;
    std::cout << "Result: " << calc->compute() << std::endl;
    delete calc;

    calc = new Subtractor;
    calc->val1 = 100;
    calc->val2 = 50;
    std::cout << "Result: " << calc->compute() << std::endl;
    delete calc;
}

Benefits include clearer structure, better readability, and easier maintenance.

Pure Virtual Functions and Abstract Classes

Virtual and Pure Virtual Destructors

When deleting a derived object via a base pointer, a non-virtual destructor will only invoke the base destructor, leading to resource leaks in the derived class.

class BaseDrink {
public:
    virtual ~BaseDrink() {}
    virtual void prepare() = 0;
};

class Tea : public BaseDrink {
    char* blend;
public:
    Tea(const char* name) { blend = new char[strlen(name)+1]; strcpy(blend, name); }
    ~Tea() { delete[] blend; }
    void prepare() override { std::cout << "Brewing " << blend << std::endl; }
};

void testDestruction() {
    BaseDrink* drink = new Tea("Earl Grey");
    drink->prepare();
    delete drink; // Correctly calls Tea::~Tea() then BaseDrink::~BaseDrink()
}

A pure virtual destructor requires a definition outside the class:

class BaseDrink {
public:
    virtual ~BaseDrink() = 0;
    virtual void prepare() = 0;
};

BaseDrink::~BaseDrink() {
    std::cout << "Base cleaned up" << std::endl;
}

Tags: C++ Object-Oriented Programming Inheritance Polymorphism Virtual Functions

Posted on Sun, 12 Jul 2026 17:10:36 +0000 by keenlearner