Inheritance in C++: Public, Protected, Private, and Virtual Inheritance

New to C++? This guide explains Inheritance in C++ with relatable analogies, clean code, and “Try it yourself” challenges after each section to build muscle memory. Think of inheritance like family traits: a child inherits features from parents, but can also have unique qualities—code works the same way.

In simple terms, Inheritance in C++ lets one class (the derived/child class) reuse and extend another class (the base/parent class). It reduces duplication, encourages reuse, and builds clean hierarchies where common behavior lives in one place.

🔹 What is Inheritance in C++?

Inheritance allows a derived class to acquire attributes and methods from a base class. The derived class can add new members or override behavior (when using virtual functions). Analogy: a “Car” is a “Vehicle”—it gets wheels and movement from Vehicle, and adds features like honk or AC.

  • Base (parent) class: the class being inherited from.
  • Derived (child) class: the class that extends/uses the base’s features.
  • “is‑a” rule: only use inheritance when the relationship truly is “is‑a.”

Try it yourself

  • Write 3 examples of true “is‑a” relationships (e.g., “Student is a Person”).
  • Write 2 examples that are not “is‑a” (e.g., “Car has an Engine”)—these are composition, not inheritance.

🔹 Basic Syntax and Public Inheritance

The general syntax is class Derived : public Base { ... };. Public inheritance means the base’s public interface stays public in the derived class, and protected stays protected. This is the most common and recommended mode for modeling “is‑a” relationships in Inheritance in C++.

#include <algorithm>
#include <iostream>
using namespace std;
// Base class
class Animal {
public:
    void eat() const { cout << "Eating...\n"; }
    void sleep() const { cout << "Sleeping...\n"; }
};
// Derived class using public inheritance
class Dog : public Animal {
public:
    void bark() const { cout << "Woof!\n"; }
};
int main() {
    Dog d;
    d.eat();   // inherited
    d.sleep(); // inherited
    d.bark();  // own method
}

Output

Eating...
Sleeping...
Woof!

Try it yourself

  • Create a Cat class derived from Animal with a meow() method and call all methods.
  • Change Dog to inherit protected and observe which members remain accessible from main().

🔹 Access Modes: public, protected, private Inheritance

In Inheritance in C++, the inheritance mode affects how base members appear in the derived class’s interface:

  • public: base public → public; base protected → protected.
  • protected: base public/protected → protected.
  • private: base public/protected → private (default for class if unspecified).
#include <iostream>
using namespace std;
class Base {
public:
    void pub() {}
protected:
    void pro() {}
private:
    void pri() {}
};
class Dpub : public Base {
    void test() {
        pub();  // OK
        pro();  // OK
        // pri(); // ERROR: private in Base
    }
};
class Dpro : protected Base {
    void test() {
        pub(); // OK (now protected)
        pro(); // OK (protected)
    }
};
class Dpri : private Base {
    void test() {
        pub(); // OK (now private)
        pro(); // OK (now private)
    }
};

Try it yourself

  • Create objects of Dpub, Dpro, Dpri and test which inherited members are callable from main().
  • Explain in a comment why public inheritance is preferred for real “is‑a” relationships.

🔹 Method Overriding and Polymorphism

To change base behavior in the derived class, mark base methods virtual and use override in derived methods. Polymorphism lets base pointers call the correct derived implementation at runtime—key to flexible Inheritance in C++.

#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Animal {
public:
    virtual ~Animal() = default; // virtual destructor for polymorphic base
    virtual void speak() const { cout << "Some sound\n"; }
};
class Dog : public Animal {
public:
    void speak() const override { cout << "Woof!\n"; }
};
class Cat : public Animal {
public:
    void speak() const override { cout << "Meow!\n"; }
};
int main() {
    vector<unique_ptr<Animal>> zoo;
    zoo.push_back(make_unique<Dog>());
    zoo.push_back(make_unique<Cat>());
    for (const auto& a : zoo)
        a->speak(); // Woof!, Meow!
}

Output

Woof!
Meow!

Try it yourself

  • Add a Cow class overriding speak(). Insert into the vector and verify output.
  • Remove virtual from the base destructor briefly (for learning) and observe risky behavior—then restore it.

🔹 Types of Inheritance

Inheritance in C++ supports multiple patterns; choose the one that matches the domain relationship and keeps code clear.

  • Single: one base → one derived.
  • Multilevel: chain of inheritance (A → B → C).
  • Multiple: one derived inherits from multiple bases.
  • Hierarchical: multiple derived classes share one base.
  • Hybrid: a combination of the above (e.g., multiple + multilevel).
#include <iostream>
using namespace std;
// Single
class Vehicle { /* ... */ };
class Car : public Vehicle { /* ... */ };
// Multilevel
class RacingCar : public Car { /* ... */ };
// Multiple
class Flyable { public: void fly() const {} };
class Floatable { public: void afloat() const {} };
class AmphibiousPlane : public Flyable, public Floatable {};
// Hierarchical
class Truck : public Vehicle {};
class Bike : public Vehicle {};

Use multiple inheritance with care—ambiguities can arise if two bases provide the same member names. Prefer interfaces (pure virtual bases) or composition if practical.

Try it yourself

  • Create VehicleCarElectricCar (multilevel). Add a method at each level and call them all.
  • Implement a small multiple inheritance example and resolve name clashes using scope resolution (e.g., Base1::method()).

🔹 The Diamond Problem and Virtual Inheritance

In a diamond, a class inherits the same base through multiple paths, creating duplicate base subobjects. Virtual inheritance shares one common base to avoid duplication and ambiguity.

#include <iostream>
using namespace std;
class Animal {
public:
    void live() const { cout << "Living...\n"; }
};
class Mammal : virtual public Animal {};
class Bird : virtual public Animal {};
class Bat : public Mammal, public Bird {};
int main() {
    Bat b;
    b.live(); // OK: only one Animal base via virtual inheritance
}

Output

Living...

Mark intermediate bases with virtual to ensure only one shared base instance exists. This prevents ambiguity and saves memory.

Try it yourself

  • Remove virtual and observe compilation errors or ambiguity; then restore it.
  • Add a member to Animal and access it from Bat to confirm a single shared base.

🔹 Constructors, Destructors, and Initialization Order

Construction happens base‑first, then members, then the derived class body. Destruction reverses the order. Always initialize base classes in the derived class initializer list for clarity and correctness in Inheritance in C++.

#include <iostream>
using namespace std;
class Base {
public:
    Base() { cout << "Base()\n"; }
    ~Base() { cout << "~Base()\n"; }
};
class Derived : public Base {
public:
    Derived() { cout << "Derived()\n"; }
    ~Derived() { cout << "~Derived()\n"; }
};
int main() {
    Derived d;
}

Output

Base()
Derived()
~Derived()
~Base()

If the base requires parameters, pass them from the derived constructor’s initializer list: Derived() : Base(args) {}.

Try it yourself

  • Add constructor parameters to Base and forward them from Derived using an initializer list.
  • Print messages in constructors/destructors to visualize exact order.

🔹 Protected Members and Derived Access

protected members are hidden from outside code but accessible to derived classes. Use them sparingly when children must interact with base internals; otherwise favor private with clear public/protected hooks.

#include <iostream>
using namespace std;
class Vehicle {
protected:
    int speed{0};
    void setSpeed(int s) { speed = s < 0 ? 0 : s; }
public:
    int getSpeed() const { return speed; }
};
class Car : public Vehicle {
public:
    void turbo() { setSpeed(speed + 50); } // allowed: protected in base
};
int main() {
    Car c;
    c.turbo();
    cout << c.getSpeed() << "\n";
}

Output

50

Try it yourself

  • Create Truck that caps speed at 120 using setSpeed inside a custom method.
  • Demonstrate that outside code cannot access speed directly.

🔹 using, override, and final

The using Base::Base; declaration can inherit constructors from a base (use with care). Use override to ensure correct overriding and final to prevent further overrides or inheritance on a class or method.

#include <iostream>
using namespace std;
class Base {
public:
    virtual void run() { /* ... */ }
};
class Child : public Base {
public:
    void run() override { /* child behavior */ } // checked by compiler
};
class Leaf final : public Child {
public:
    // class Leaf cannot be derived further (final)
    void run() override { /* final leaf behavior */ } 
};

Try it yourself

  • Mark a method final in a mid‑level class and try overriding it below—see the compiler error.
  • Use using Base::Base; to inherit base constructors and construct the child with a base‑style argument.

🔹 Mini Project: Employees with Polymorphic Pay

Combine Inheritance in C++ and polymorphism: compute pay differently for hourly versus salaried employees through a common interface, enabling clean, extensible code.

#include <iomanip>
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Employee {
public:
    virtual ~Employee() = default;
    virtual double pay() const = 0;
    virtual const char* kind() const = 0;
};
class Hourly : public Employee {
    double rate;
    double hours;
public:
    Hourly(double r, double h) : rate(r), hours(h) {}
    double pay() const override { return rate * hours; }
    const char* kind() const override { return "Hourly"; }
};
class Salaried : public Employee {
    double monthly;
public:
    explicit Salaried(double m) : monthly(m) {}
    double pay() const override { return monthly; }
    const char* kind() const override { return "Salaried"; }
};
int main() {
    vector<unique_ptr<Employee>> staff;
    staff.push_back(make_unique<Hourly>(20.0, 160)); // 20/hr * 160h
    staff.push_back(make_unique<Salaried>(5000.0)); // monthly fixed
    cout << fixed << setprecision(2);
    for (const auto& e : staff) {
        cout << e->kind() << 
        " pay: $" << e->pay() << "\n";
    }
}

Output

Hourly pay: $3200.00
Salaried pay: $5000.00

Try it yourself

  • Add a Commissioned employee with base salary plus percentage of sales.
  • Extend the program to sum total payroll using a loop over Employee pointers.

🔹 Best Practices for Inheritance in C++

  • Use inheritance only for true “is‑a” relationships; otherwise prefer composition.
  • Keep base class interfaces small, stable, and meaningful.
  • Mark virtual methods with override in derived classes and provide a virtual base destructor.
  • Avoid deep, complicated hierarchies; favor simple, testable layers.
  • Use virtual inheritance only when needed (e.g., to resolve diamond problems).

🔹 Common Pitfalls to Avoid

  • Using inheritance to reuse code when the relationship isn’t “is‑a” (leads to brittle designs).
  • Forgetting a virtual destructor in polymorphic bases (can leak resources).
  • Overriding without override (typos silently create new methods).
  • Exposing base internals via protected when a clear public hook would be safer.
  • Unnecessary multiple inheritance causing ambiguities; consider interfaces or composition.

🔹 FAQs: Inheritance in C++

Q1. What is the difference between public, protected, and private inheritance?
Public inheritance preserves the base’s public/protected visibility, protected turns them protected, and private turns them private. Public is the normal choice for “is‑a” models.

Q2. When should inheritance be used instead of composition?
Use inheritance for a true “is‑a” relationship (e.g., a Circle is a Shape). Use composition for “has‑a” (e.g., a Car has an Engine).

Q3. Why is a virtual destructor important?
In polymorphic bases, deleting via a base pointer must call the derived destructor; marking the base destructor virtual guarantees proper cleanup.

Q4. What does override do?
It asks the compiler to confirm the method really overrides a virtual base method, catching errors early (like mismatched signatures).

Q5. What is virtual inheritance?
It ensures there’s only one shared instance of a base when inherited along multiple paths (solves the diamond problem).

Q6. Can constructors be inherited?
Yes, with using Base::Base;, but use carefully and still ensure derived class invariants are enforced.

🔹 Wrapping Up

Inheritance in C++ helps model natural hierarchies, reuse behavior, and enable polymorphism. Stick to true “is‑a” relationships, keep base interfaces clean, and use virtual, override, and virtual inheritance thoughtfully. Practice the “Try it yourself” tasks to strengthen understanding and confidence.

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.