Inheritance in C++: Access, Constructors, Overriding and the Pitfalls That Compile

How C++ inheritance works and where it fails quietly: default private bases, hidden overloads, a missing virtual destructor, and arrays of derived types.

A blue house with a green extension built onto its side on the same foundation, representing a C++ derived class that keeps everything in its base class and adds its own members.

Delete a derived object through a pointer to a base class that has no virtual destructor, and the program prints nothing unusual and exits with status 0. The derived destructor never ran, and the 65 bytes its string member allocated were never freed. GCC and Clang compiled it without a warning under -Wall -Wextra. Most of what goes wrong with inheritance in C++ looks like this: the syntax takes one line, and the rules that make it safe are invisible until you know where they are.

This guide covers inheritance in C++ from the first derived class to the rules that keep a hierarchy correct: public, protected and private inheritance, the order constructors and destructors run in, inheriting constructors, name hiding and the scope operator, virtual, override and final, and two mistakes that no compiler flags. Multiple inheritance and the diamond problem have their own guide. Every program was compiled and run for this article on Ubuntu 24.04 with GCC 13.3 and Clang 18.1.3 under -std=c++17 and -std=c++20 with -Wall -Wextra -pedantic, and runs under AddressSanitizer and UndefinedBehaviorSanitizer; the examples that must not compile are shown with the errors both compilers gave. All output is captured verbatim, and the code is in a GitHub repository whose build repeats these checks on each commit.

The Short Answer

What is inheritance in C++? Defining a new class from an existing one: class Manager : public Employee. The derived class contains everything the base class has and adds its own members, and a Manager can be used wherever an Employee& or Employee* is expected.

What is the default inheritance in C++? Private for a class, public for a struct. class D : B makes every public member of B private in D, which is rarely what was meant, so write public explicitly.

Does a base class need a virtual destructor? Yes, if objects of a derived class will ever be deleted through a pointer to the base. Without one, that delete is undefined behavior ([expr.delete]/3); in the test below, the derived destructor silently never ran.

What Is Inheritance in C++?

Inheritance in C++ is a way to define a class (the derived class) in terms of an existing class (the base class). The derived class contains a base-class subobject with all of the base’s members, can add data and functions, and can override the base’s virtual functions. With public inheritance, a derived object can be used wherever a reference or pointer to the base is expected.

The vocabulary varies between languages, so it helps to fix it once:

C++ termAlso calledExample
Base classParent class, superclassEmployee
Derived classChild class, subclassManager
Public inheritance“is-a” relationshipclass Manager : public Employee
OverridingReplacing a virtual function in a derived classdouble fee(double) const override
Base-class subobjectThe base part inside a derived objectThe Employee inside every Manager

Inheritance as an object-oriented idea, with the same example in Java and Python, is covered in inheritance in object-oriented programming. The rest of this page is about how C++ implements it.

A First Derived Class

A Manager is an Employee with a list of direct reports:

// basics.cpp - a derived class reuses its base class's members and adds its own.
#include <iostream>
#include <string>
#include <utility>
#include <vector>

class Employee {
public:
    Employee(std::string name, double salary) : name_(std::move(name)), salary_(salary) {}

    const std::string& name() const { return name_; }
    double salary() const { return salary_; }
    void raise(double percent) { salary_ += salary_ * percent / 100.0; }

private:
    std::string name_;
    double salary_;
};

// A Manager is an Employee, plus a list of direct reports.
class Manager : public Employee {
public:
    Manager(std::string name, double salary) : Employee(std::move(name), salary) {}

    void add_report(const Employee& e) { reports_.push_back(e.name()); }
    std::size_t team_size() const { return reports_.size(); }

private:
    std::vector<std::string> reports_;
};

void print_badge(const Employee& e) {          // accepts a Manager too
    std::cout << "badge: " << e.name() << '\n';
}

int main() {
    Employee dev("Ada", 5000);
    Manager lead("Grace", 7000);

    lead.add_report(dev);
    lead.raise(10);                            // inherited from Employee

    std::cout << lead.name() << " earns " << lead.salary()
              << " and manages " << lead.team_size() << " person\n";
    print_badge(dev);
    print_badge(lead);
}

Output:

Grace earns 7700 and manages 1 person
badge: Ada
badge: Grace

Three things happened without any code in Manager asking for them:

  • lead.raise(10) and lead.name() are inherited. Manager declares neither; they are Employee‘s public member functions, and public inheritance keeps them public. The salary went from 7000 to 7700.
  • The base part is built first. Manager‘s constructor passes the name and salary to Employee(std::move(name), salary) in its initializer list. That is the only way to initialize Employee‘s private members, because Manager cannot touch them directly.
  • A Manager is accepted where an Employee is expected. print_badge(const Employee&) took both objects. Taking the parameter by reference matters: passed by value, the Manager would be copied into a plain Employee, which is called slicing and is shown in object-oriented programming in C++.

If the base class has no default constructor, the derived constructor must call one explicitly. Leaving it out is a compile error, and Clang’s message says exactly why:

// missing_base_ctor.cpp - must not compile: Base has no default constructor,
// so Derived's constructor has to call one of Base's constructors.
#include <string>

class Base {
public:
    explicit Base(std::string name) : name_(name) {}
private:
    std::string name_;
};

class Derived : public Base {
public:
    Derived() {}               // which Base constructor? none can be called
};

int main() { Derived d; }

GCC first, then Clang:

tests/compile-fail/missing_base_ctor.cpp:14:15: error: no matching function for call to 'Base::Base()'
tests/compile-fail/missing_base_ctor.cpp:14:5: error: constructor for 'Derived' must explicitly initialize the base class 'Base' which does not have a default constructor

Access Control: public, protected and private Inheritance

Two separate things control access in a hierarchy. The member’s access (public, protected, private in the base) decides what the derived class can reach. The inheritance mode (public, protected, private before the base name) decides what everyone else can reach through the derived class.

// access.cpp - what a derived class can reach in its base class.
#include <iostream>

class Base {
public:
    int pub = 1;
protected:
    int prot = 2;
private:
    int priv = 3;
    friend void show_private(const Base&);
};

void show_private(const Base& b) { std::cout << "friend sees priv = " << b.priv << '\n'; }

class Derived : public Base {
public:
    void show() const {
        std::cout << "Derived sees pub = " << pub << ", prot = " << prot << '\n';
        // priv is not accessible here: see tests/compile-fail/private_member.cpp
    }
};

struct FromStruct : Base {};          // struct: public inheritance by default
class FromClass : Base {              // class: private inheritance by default
public:
    int read_pub() const { return pub; }   // still visible inside the class
};

int main() {
    Derived d;
    d.show();
    std::cout << "outside sees d.pub = " << d.pub << '\n';
    show_private(d);

    FromStruct s;
    std::cout << "struct-derived: s.pub = " << s.pub << '\n';
    FromClass c;
    std::cout << "class-derived: c.read_pub() = " << c.read_pub() << '\n';
}

Output:

Derived sees pub = 1, prot = 2
outside sees d.pub = 1
friend sees priv = 3
struct-derived: s.pub = 1
class-derived: c.read_pub() = 1

Derived read pub and prot but not priv. A private member is private to the class that declares it, including from its own derived classes; only the class and its friends can see it, which is how show_private read it. Reaching for a private member from a derived class does not compile:

// private_member.cpp - must not compile: a derived class cannot read its
// base class's private members.
class Base {
private:
    int secret = 42;
};

class Derived : public Base {
public:
    int peek() const { return secret; }
};

int main() { return Derived{}.peek(); }
tests/compile-fail/private_member.cpp:10:31: error: 'int Base::secret' is private within this context
tests/compile-fail/private_member.cpp:10:31: error: 'secret' is a private member of 'Base'

The inheritance mode then changes how inherited members look from outside:

Member in basepublic inheritanceprotected inheritanceprivate inheritance
publicpublic in derivedprotected in derivedprivate in derived
protectedprotected in derivedprotected in derivedprivate in derived
privatenot accessiblenot accessiblenot accessible

Public inheritance is the one that means “is-a” and allows a Derived& to convert to a Base& anywhere; it is what you want almost every time. Private inheritance means “is implemented in terms of”: the derived class uses the base internally, outside code cannot convert it to the base, and a private member object usually expresses the same thing more clearly. Protected inheritance is rare.

The default is the trap. FromClass in the program above was declared class FromClass : Base, with no access specifier, so it inherits privately: pub is still usable inside the class (read_pub() returned 1) but not from outside. The same mistake in a separate file:

// default_private_base.cpp - must not compile: `class D : B` inherits
// privately, so B's public members are private in D.
class Base {
public:
    int value = 1;
};

class Derived : Base {};       // no access specifier: private inheritance

int main() {
    Derived d;
    return d.value;
}
tests/compile-fail/default_private_base.cpp:12:14: error: 'int Base::value' is inaccessible within this context
tests/compile-fail/default_private_base.cpp:12:14: error: 'value' is a private member of 'Base'

struct FromStruct : Base inherited publicly, because a struct defaults to public for both members and bases. The way encapsulation uses private and protected within one class is covered in encapsulation in C++.

Constructor and Destructor Order

Construction goes from the base outward; destruction reverses it exactly:

// order.cpp - the order of construction and destruction in a class hierarchy.
#include <iostream>
#include <string>
#include <utility>

struct Note {
    std::string text;
    explicit Note(std::string t) : text(std::move(t)) { std::cout << "  construct " << text << '\n'; }
    ~Note() { std::cout << "  destroy   " << text << '\n'; }
};

struct Base {
    Note base_member{"Base member"};
    Base() { std::cout << "  Base constructor body\n"; }
    virtual ~Base() { std::cout << "  Base destructor body\n"; }
};

struct Derived : Base {
    Note derived_member{"Derived member"};
    Derived() { std::cout << "  Derived constructor body\n"; }
    ~Derived() override { std::cout << "  Derived destructor body\n"; }
};

int main() {
    std::cout << "creating:\n";
    {
        Derived d;
        std::cout << "destroying:\n";
    }
}

Output:

creating:
  construct Base member
  Base constructor body
  construct Derived member
  Derived constructor body
destroying:
  Derived destructor body
  destroy   Derived member
  Base destructor body
  destroy   Base member

The order is fixed by the language, not by the order you write initializers in:

  1. Base classes, in the order they are listed in the class declaration.
  2. Data members, in the order they are declared.
  3. The constructor body.

Destruction runs the destructor body, then the members in reverse order, then the bases. The practical consequence: when the Base constructor runs, the Derived part does not exist yet, so a virtual function called from a base constructor runs the base version, not the override. Constructors and destructors in general, including which ones the compiler generates, are in constructors and destructors in C++.

Inheriting Constructors

A derived class that adds behavior but no data often just forwards its constructors. Since C++11, using Base::Base; does that for every base constructor at once:

// inheriting_ctors.cpp - `using Base::Base;` gives a derived class the base
// class's constructors (C++11).
#include <iostream>
#include <string>
#include <utility>

class Connection {
public:
    explicit Connection(std::string host, int port = 443)
        : host_(std::move(host)), port_(port) {}
    std::string address() const { return host_ + ":" + std::to_string(port_); }

private:
    std::string host_;
    int port_;
};

class LoggedConnection : public Connection {
public:
    using Connection::Connection;      // both constructor forms, no boilerplate
    void log() const { std::cout << "connected to " << address() << '\n'; }
};

int main() {
    LoggedConnection a("example.com");
    LoggedConnection b("localhost", 8080);
    a.log();
    b.log();
}

Output:

connected to example.com:443
connected to localhost:8080

LoggedConnection declares no constructor, yet accepted both ("example.com") and ("localhost", 8080), default argument included. Members the derived class adds are initialized as if by its default constructor: from their default member initializers, or left default-initialized without one (cppreference). Copy and move constructors are not inherited this way. So this fits classes like LoggedConnection that add functions, not state.

Name Hiding and the Scope Operator

A member function in a derived class hides every base-class function with the same name, whatever the parameters. It does not overload them:

// hiding.cpp - a function in a derived class hides every base-class function
// with the same name, whatever their parameters.
#include <iostream>
#include <string>

struct Printer {
    void print() const { std::cout << "Printer::print()\n"; }
    void print(const std::string& s) const { std::cout << "Printer::print(\"" << s << "\")\n"; }
};

struct ColorPrinter : Printer {
    void print(int color) const { std::cout << "ColorPrinter::print(" << color << ")\n"; }
};

struct FixedPrinter : Printer {
    using Printer::print;              // bring the base overloads back
    void print(int color) const { std::cout << "FixedPrinter::print(" << color << ")\n"; }
};

int main() {
    ColorPrinter c;
    c.print(3);
    c.Printer::print();                // the scope operator reaches the hidden one
    c.Printer::print("draft");

    FixedPrinter f;
    f.print(3);
    f.print();
    f.print("final");
}

Output:

ColorPrinter::print(3)
Printer::print()
Printer::print("draft")
FixedPrinter::print(3)
Printer::print()
Printer::print("final")

ColorPrinter declared only print(int), and that one declaration hid both Printer::print() and Printer::print(const std::string&). The scope operator still reaches them: c.Printer::print() named the base version explicitly. Calling c.print() without it fails, and Clang’s message points at the hidden function:

// hidden_overload.cpp - must not compile: print(int) in the derived class
// hides Printer::print(), so the call without arguments finds no match.
struct Printer {
    void print() const {}
};

struct ColorPrinter : Printer {
    void print(int) const {}
};

int main() {
    ColorPrinter c;
    c.print();
}
tests/compile-fail/hidden_overload.cpp:13:12: error: no matching function for call to 'ColorPrinter::print()'
tests/compile-fail/hidden_overload.cpp:13:7: error: too few arguments to function call, expected 1, have 0; did you mean 'Printer::print'?

FixedPrinter shows the fix: using Printer::print; brings the base overloads into the derived class’s scope, so all three versions of print are candidates again and normal overload resolution picks one. Neither print here is virtual, so this is hiding, not overriding: which function runs depends only on the static type of the object or reference.

Virtual Functions, override and final

To let a derived class replace a base-class function, so that a call through a base reference or pointer runs the derived version, the function must be virtual:

// final_and_override.cpp - virtual functions, override and final.
#include <iostream>
#include <memory>
#include <vector>

class Payment {
public:
    virtual ~Payment() = default;
    virtual double fee(double amount) const { return amount * 0.02; }
    virtual const char* name() const { return "payment"; }
};

class Card : public Payment {
public:
    double fee(double amount) const override { return 0.30 + amount * 0.029; }
    const char* name() const override { return "card"; }
};

// final: no class can derive from BankTransfer.
class BankTransfer final : public Payment {
public:
    double fee(double) const override { return 1.00; }
    const char* name() const final { return "bank transfer"; }
};

int main() {
    std::vector<std::unique_ptr<Payment>> methods;
    methods.push_back(std::make_unique<Payment>());
    methods.push_back(std::make_unique<Card>());
    methods.push_back(std::make_unique<BankTransfer>());

    for (const auto& m : methods)
        std::cout << m->name() << " fee on 100.00: " << m->fee(100.0) << '\n';
}

Output:

payment fee on 100.00: 2
card fee on 100.00: 3.2
bank transfer fee on 100.00: 1

The loop only knows about Payment, and each m->fee(100.0) ran the version belonging to the object’s real type. Three keywords do the work:

  • virtual in the base class makes the function overridable and the call dispatched at run time.
  • override on the derived function asks the compiler to confirm it really overrides something. A mismatch, such as a missing const, becomes an error instead of a new, unrelated function that only a warning (-Woverloaded-virtual) may point out.
  • final on a function stops further overriding; on a class, it stops further derivation. BankTransfer is final, so struct Wire : BankTransfer {}; is rejected:
tests/compile-fail/derive_from_final.cpp:8:8: error: cannot derive from 'final' base 'BankTransfer' in derived type 'Wire'
tests/compile-fail/derive_from_final.cpp:8:15: error: base 'BankTransfer' is marked 'final'

How virtual calls work underneath (the vtable and the hidden vptr in each object), pure virtual functions and abstract classes are covered in the virtual functions guide.

The Virtual Destructor Rule

A base class that is used polymorphically needs a virtual destructor. This program deletes one derived object correctly and, when given an argument, one incorrectly:

// virtual_dtor.cpp - deleting a derived object through a base pointer.
// Without a virtual destructor in the base, this is undefined behavior.
#include <iostream>
#include <memory>
#include <string>

struct Plain {                         // no virtual destructor
    ~Plain() { std::cout << "  ~Plain\n"; }
};
struct PlainChild : Plain {
    std::string buffer = std::string(64, 'x');   // owns heap memory
    ~PlainChild() { std::cout << "  ~PlainChild\n"; }
};

struct Safe {
    virtual ~Safe() { std::cout << "  ~Safe\n"; }
};
struct SafeChild : Safe {
    std::string buffer = std::string(64, 'x');
    ~SafeChild() override { std::cout << "  ~SafeChild\n"; }
};

int main(int argc, char*[]) {
    std::cout << "virtual destructor:\n";
    { std::unique_ptr<Safe> p = std::make_unique<SafeChild>(); }

    if (argc > 1) {                    // the broken case runs only on request
        std::cout << "no virtual destructor:\n";
        Plain* p = new PlainChild;
        delete p;                      // undefined behavior
    }
}

Output (normal run):

virtual destructor:
  ~SafeChild
  ~Safe

Through std::unique_ptr<Safe>, both destructors ran, derived first. With the argument, the second case printed only ~Plain: delete p called Plain‘s destructor because that is the static type, ~PlainChild never ran, and its 64-character std::string was never freed. The program exited with status 0 on both compilers. AddressSanitizer caught it, but the two compilers’ builds reported different problems (GCC first, then Clang; addresses shortened):

==PID==ERROR: AddressSanitizer: new-delete-type-mismatch on 0x... in thread T0:
  size of the allocated type:   32 bytes;
  size of the deallocated type: 1 bytes.
==PID==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 65 byte(s) in 1 object(s) allocated from:
SUMMARY: AddressSanitizer: 65 byte(s) leaked in 1 allocation(s).

GCC’s report is the precise one: 32 bytes were allocated as a PlainChild and freed as a 1-byte Plain. Clang’s build reported the consequence, the string buffer that was never released (64 characters plus the terminator). Neither compiler warned at compile time, because Plain has no virtual functions. Give Plain a virtual function and both do: GCC with -Wdelete-non-virtual-dtor and Clang with -Wdelete-non-abstract-non-virtual-dtor, both in -Wall.

The rule from the C++ Core Guidelines (C.35): a base class destructor should be either public and virtual, or protected and non-virtual. The second form makes delete through a base pointer a compile error instead of undefined behavior.

Arrays of Derived Objects Through a Base Pointer

A pointer to a derived object converts to a pointer to its base implicitly, and that conversion compiles for arrays too. Indexing the result does not do what it appears to:

// array_pitfall.cpp - a pointer to Base must not walk an array of Derived.
// p[1] steps sizeof(Base) bytes, which is not where the second Derived starts.
#include <iostream>

struct Point {
    int x = 0, y = 0;
};
struct Point3 : Point {
    int z = 0;
};

void print_all(const Point* points, int n) {
    for (int i = 0; i < n; ++i)
        std::cout << "  (" << points[i].x << ", " << points[i].y << ")\n";
}

int main() {
    Point3 pts[3];
    for (int i = 0; i < 3; ++i) { pts[i].x = i + 1; pts[i].y = (i + 1) * 10; pts[i].z = -1; }

    std::cout << "sizeof(Point) = " << sizeof(Point) << ", sizeof(Point3) = " << sizeof(Point3) << '\n';
    std::cout << "through Point3 objects:\n";
    for (const Point3& p : pts) std::cout << "  (" << p.x << ", " << p.y << ")\n";
    std::cout << "through a Point*:\n";
    print_all(pts, 3);                 // compiles: Point3* converts to Point*
}

Output:

sizeof(Point) = 8, sizeof(Point3) = 12
through Point3 objects:
  (1, 10)
  (2, 20)
  (3, 30)
through a Point*:
  (1, 10)
  (-1, 2)
  (20, -1)
A Point* cannot walk an array of Point3 Point3 pts[3] in memory (4-byte ints), and what array_pitfall.cpp read through a Point* As Point3 objects: 12-byte steps pts[0] pts[1] pts[2] 1 x 10 y -1 z 2 x 20 y -1 z 3 x 30 y -1 z p[0] (1, 10) p[1] (-1, 2) p[2] (20, -1) Through a Point*: 8-byte steps the Point part of each Point3 (x, y) the member Point3 adds (z) p[i] means *(p + i), and p + i moves i * sizeof(Point) = 8 bytes. The objects are 12 bytes apart, so p[1] starts at pts[0].z. The conversion from Point3* to Point* needs no cast, and neither compiler nor sanitizer reported it. Measured with GCC 13.3 and Clang 18.1.3, x86-64: sizeof(Point) = 8, sizeof(Point3) = 12.
A Point* steps 8 bytes; the Point3 objects are 12 bytes apart. Only the first read lands on a whole object. The second and third straddle two objects, which is why the program printed (-1, 2) and (20, -1), and nothing reported it.

print_all indexes with points[i], which steps i * sizeof(Point) = 8 bytes, but the Point3 objects are 12 bytes apart. So the second “point” it printed was pts[0].z and pts[1].x, and the third was pts[1].y and pts[1].z. The behavior is undefined ([expr.add]/6), both compilers compiled it without a warning, and AddressSanitizer and UndefinedBehaviorSanitizer reported nothing, because every read stayed inside the array. Pass a std::span<const Point3> (C++20) or a std::vector<Point3>&, which carry the element type, or store pointers to the base in a container if the elements are of different types.

Inheritance or Composition?

Public inheritance is a strong statement: every Derived is usable as a Base, forever. When that is not true, a member object usually serves better.

QuestionInheritanceComposition (a member object)
Relationshipis-a: a Manager is an Employeehas-a: a Car has an Engine
Used through a base reference or pointer?Yes, that is the pointNo
Reuses implementationYes, but exposes the base’s public interfaceYes, and exposes only what you forward
Changing the base laterAffects every derived classAffects one member
Typical fitA family of types handled through one interfaceReusing a component inside a class

Prefer composition when you only want to reuse code, and reserve public inheritance for types that callers will genuinely treat as the base type. A class that inherits from several bases at once is a separate design question; multiple inheritance in C++ covers it, with the diamond problem and virtual base classes.

Key Takeaways

  • Write public explicitly. class D : B inherits privately; the base’s public members became inaccessible from outside.
  • Private base members stay private to the base, even from derived classes. Use protected sparingly, or go through the base’s public functions.
  • Bases are constructed first and destroyed last. Call a base constructor from the initializer list when the base has no default constructor.
  • A derived function hides every base function with the same name. using Base::f; restores the overloads; Base::f() calls one explicitly.
  • Mark overrides override, and use final to close a class or function.
  • Give a polymorphic base a virtual destructor. Without one, the derived destructor never ran and the program still exited with status 0.
  • Never index an array of derived objects through a base pointer. The reads were wrong and no tool reported them.

Frequently Asked Questions

Conclusion

Inheritance in C++ is one line of syntax and a handful of rules that the compiler applies whether you remember them or not: the default access, the construction order, hiding, the static type that delete uses, the stride a pointer steps by. Most of the failures on this page compiled cleanly, and two of them ran to a normal exit. The habits that prevent them are short: write public, mark override, make polymorphic destructors virtual, and pass hierarchies around by reference or pointer to a single object.

The C++ programming tutorials are collected in the C++ section.

Source Code and Tests

inheritance

All programs on this page are in the oop/inheritance directory of the MYCPLUS C++ examples repository.

Build and test:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
bash tests/run_tests.sh g++ c++17
bash tests/sanitizers.sh g++

What the build checks. On Linux, with GCC and Clang under C++17 and C++20, it builds every program with -Wall -Wextra -pedantic -Werror, runs it and compares its output with this page, and checks that the five compile-fail examples are rejected with the expected error. A sanitizer job confirms that the programs run without an AddressSanitizer or UndefinedBehaviorSanitizer report (including array_pitfall, whose undefined behavior the sanitizers do not detect) and that the missing virtual destructor is reported. A macOS job runs the same tests with Apple Clang, and a Windows job builds every program with MSVC at /W4 /WX and compares the output. The plain-run exit status of the broken destructor case and the compiler warnings for a polymorphic base were checked locally and are not part of the build.

Scroll to Top