C++ Inheritance - Complete Master Guide
Fundamentals of Inheritance
Inheritance is an object-oriented feature that lets a new class (derived) reuse and extend the data and behavior of an existing class (base). The derived class inherits what is common and can add or customize features of its own.
In C++, inheritance primarily supports:
1. Code Reusability: Share common functionality without duplication
2. Class Hierarchy: Organize related types into logical families
3. Polymorphism: Enable dynamic dispatch via virtual functions
Syntax uses a colon after the derived class name, followed by an access specifier (public
/protected
/private
) and the base class name.
#include <iostream>
#include <string>
#include <utility>
class Employee {
protected:
std::string name;
double salary;
public:
Employee(std::string n, double s) : name(std::move(n)), salary(s) {}
virtual void display() const {
std::cout << name << " earns $" << salary << '\n';
}
virtual ~Employee() = default; // polymorphic base: virtual destructor
};
class Manager : public Employee {
std::string department;
public:
Manager(std::string n, double s, std::string d)
: Employee(std::move(n), s), department(std::move(d)) {}
void display() const override {
std::cout << name << " (" << department
<< " manager) earns $" << salary << '\n';
}
};
Default Inheritance Access (class vs struct)
If no access specifier is provided after the colon, classes inherit privately by default, while structs inherit publicly by default.
Member default access also differs: class members are private by default; struct members are public by default.
class A {};
class B : A {}; // private inheritance by default
struct S {};
struct T : S {}; // public inheritance by default
Access Control Deep Dive
C++ inheritance access specifiers affect the visibility of inherited members in the derived class and to clients using a derived object:
- public: Keeps base public members public and base protected members protected
- protected: Turns base public into protected; protected remain protected
- private: Turns base public and protected into private
Base private members are never directly accessible in the derived class (they still exist and can be accessed via base public/protected interfaces).
Upcasting rules: An implicit conversion from Derived to Base (viewing a derived object as its base) is public only with public inheritance; with protected/private inheritance, such conversions are not accessible to ordinary client code.
Base Member | Public Inheritance | Protected Inheritance | Private Inheritance |
---|---|---|---|
private | Inaccessible | Inaccessible | Inaccessible |
protected | Protected | Protected | Private |
public | Public | Protected | Private |
Construction/Destruction Sequence
Object lifetime in an inheritance chain follows a strict order:
1. Construction: Base subobjects are constructed first, then derived (top-down).
2. Destruction: Derived is destroyed first, then base (bottom-up).
Use virtual destructors in polymorphic base classes to ensure correct cleanup when deleting through a base pointer.
#include <iostream>
class Base {
public:
Base() { std::cout << "Base constructed\n"; }
virtual ~Base() { std::cout << "Base destroyed\n"; }
};
class Derived : public Base {
int* resource;
public:
Derived() : resource(new int[100]) {
std::cout << "Derived constructed\n";
}
~Derived() override {
delete[] resource;
std::cout << "Derived destroyed\n";
}
};
// Usage
// Base* obj = new Derived();
// delete obj; // calls ~Derived() then ~Base() due to virtual destructor
Base constructed Derived constructed Derived destroyed Base destroyed
Method Overriding & Virtual Functions
Virtual functions enable runtime polymorphism. Mark overrides explicitly with override
to catch signature mismatches.
Key points:
- The base function must be virtual (or override/final) to dispatch dynamically.
- The overriding function must match the base signature, including const/ref/noexcept and reference qualifiers; return types may be covariant.
- Access control (public/protected/private
) is not part of the signature; a derived override may change accessibility (e.g., make a protected virtual public).
- Use final
to prevent further overriding when appropriate.
class Shape {
public:
virtual double area() const = 0; // pure virtual
virtual void scale(double factor) { /* default behavior */ }
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
explicit Circle(double r) : radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
void scale(double factor) override {
radius *= factor;
}
};
Name Hiding vs. Overriding
Introducing a function with the same name in a derived class hides all overloads of that name from the base (even if the signatures differ). Use using Base::func;
to bring base overloads into scope when you want overloading rather than hiding.
struct Base {
virtual void f(int);
void f(double);
};
struct Derived : Base {
using Base::f; // prevent hiding; keep overload set
void f(int) override; // proper override
};
Multiple Inheritance & Virtual Inheritance
C++ supports multiple inheritance. When two paths inherit the same base (the “diamond” problem), virtual inheritance ensures only one shared base subobject exists.
Use virtual inheritance when a common base must not be duplicated in derived-most objects.
struct A { int id; };
struct B : virtual A {};
struct C : virtual A {};
struct D : B, C {
D() { id = 42; } // OK: only one A subobject
};
Advanced Topics
Abstract Classes: Have at least one pure virtual function (= 0
) and cannot be instantiated.
Interface Classes: Abstract classes that contain only pure virtual functions and no data; model contracts.
RTTI: Use dynamic_cast
and typeid
for safe downcasting in polymorphic hierarchies.
CRTP: Curiously Recurring Template Pattern for static (compile-time) polymorphism without virtual dispatch.
Best Practices Checklist
- □ Declare a virtual destructor in any class intended for polymorphic use.
- □ Prefer public inheritance to model 'is-a' interfaces; otherwise consider composition.
- □ Mark every intended override with
override
; usefinal
to stop further overrides. - □ Keep hierarchies shallow and coherent; avoid deep and wide inheritance trees.
- □ Clearly document the role (interface vs implementation) of each base class.
- □ Use composition for code reuse when an 'is-a' relationship does not hold.
- □ Define abstract bases (pure virtual) to express clear contracts.
- □ Avoid name hiding: use
using
to import base overloads when needed. - □ Avoid object slicing: pass and store polymorphic objects by reference or pointer, not by value.