C++ Programming

Inheritance, Access Modes and Virtual Bases

PGCP-AC

1. Extending an Existing Class

Inheritance creates a new class from an existing class. The existing class is the base class and the new class is the derived class. A derived object contains a base-class subobject together with any state introduced by the derived class. The derived class can reuse accessible base behavior, add new operations and refine behavior where the design permits it.

class Account {
public:
    explicit Account(double opening) : balance_(opening) {}
    void deposit(double amount) {
        if (amount > 0) balance_ += amount;
    }
    double balance() const { return balance_; }
private:
    double balance_;
};

class SavingsAccount : public Account {
public:
    SavingsAccount(double opening, double rate)
        : Account(opening), interestRate_(rate) {}
    void addInterest() {
        deposit(balance() * interestRate_);
    }
private:
    double interestRate_;
};

Every SavingsAccount object has an Account part. Its constructor explicitly chooses an Account constructor before initializing interestRate_. Public inheritance also permits a SavingsAccount reference or pointer to be used where an Account reference or pointer is expected.

2. Inheritance Represents a Type Relationship

Public inheritance is strongest when the derived type is a specialized form of the base type. Code that accepts the base interface should remain meaningful when given a derived object. This property is called substitutability.

A savings account is an account, so public inheritance can express the relationship. A car has an engine; a car is not a specialized engine. That relationship normally belongs in a data member:

class Car {
    Engine engine_;
};

Composition gives the containing class an implementation object without claiming that the containing object has the same public meaning. Prefer composition for has-a and uses-a relationships. Use public inheritance when clients should intentionally treat the derived object as a base object.

3. Basic Declaration Syntax

The inheritance list follows the derived class name:

class Derived : public Base {
    // members of Derived
};

The word before Base is the inheritance access mode. A class may name more than one direct base, separated by commas. Each base can have its own access mode:

class ScannerPrinter : public Scanner, private Diagnostics {
};

For a class declaration, omitted inheritance access defaults to private. For a struct declaration, it defaults to public. Writing the mode explicitly makes the intended relationship easier to see.

4. Member Access and Member Existence

A base member's own access specifier controls who may name it directly:

  • public members may be named by all code with a suitable object;
  • protected members may be named inside the base and inside derived member functions, subject to protected-access rules;
  • private members may be named only by the base and its friends.

Private base data still exists inside every derived object. Inaccessibility does not remove storage and does not prevent the base member functions from using that data. A derived class should normally work through protected or public base operations rather than trying to expose base representation.

class Counter {
public:
    int value() const { return value_; }
protected:
    void increment() { ++value_; }
private:
    int value_ = 0;
};

class EventCounter : public Counter {
public:
    void record() {
        increment();       // accessible protected operation
        // ++value_;       // error: private in Counter
    }
};

5. Public Inheritance

With public inheritance, public base members remain public through the derived object and protected base members remain protected. Private base members remain inaccessible directly.

Member in the baseAccess through public inheritance
publicpublic
protectedprotected
privateinaccessible directly

The implicit conversion from a derived pointer or reference to an accessible unambiguous public base pointer or reference is available to ordinary client code. This conversion is called an upcast and is central to runtime polymorphism.

void printBalance(const Account& account);
SavingsAccount savings(1000, 0.05);
printBalance(savings);

The conversion does not create a separate Account object. The reference denotes the Account subobject inside savings.

6. Protected Inheritance

With protected inheritance, public and protected base members become protected in the derived class. Outside code cannot use them through a derived object, but the derived class and further derived classes can.

Member in the baseAccess through protected inheritance
publicprotected
protectedprotected
privateinaccessible directly

Protected inheritance represents implementation reuse that descendants may continue to use. It appears less often than public inheritance or composition because it hides the base interface from ordinary clients while propagating that implementation relationship to later derived classes.

7. Private Inheritance

With private inheritance, public and protected base members become private in the derived class. They are usable by the derived class itself but are not part of its public interface and are not directly accessible to further derived classes.

Member in the baseAccess through private inheritance
publicprivate
protectedprivate
privateinaccessible directly

Private inheritance can reuse an implementation and may permit protected access or overriding of virtual functions. Composition is usually simpler when those abilities are unnecessary. Ordinary client code cannot implicitly convert a privately derived object to its base.

Inheritance access and member access answer different questions. The public, protected or private labels inside Base describe Base's members. The access mode after the colon describes how the base interface is inherited into Derived.

8. Single, Multilevel and Hierarchical Inheritance

Single inheritance means a class has one direct base:

class Employee {};
class Manager : public Employee {};

Multilevel inheritance forms a chain:

class Person {};
class Employee : public Person {};
class Manager : public Employee {};

A Manager contains an Employee subobject, which itself contains a Person subobject. Construction begins at the most distant base and proceeds toward the most-derived class. Destruction reverses this path. Deep chains can make contracts and initialization harder to follow, so every level should represent a useful abstraction.

Hierarchical inheritance occurs when several derived classes share one base:

class Shape {};
class Circle : public Shape {};
class Rectangle : public Shape {};

The base holds the common contract or common state. Each derived class supplies its specialized part. When virtual functions are present, code can operate on different derived objects uniformly through Shape references or pointers.

9. Multiple and Hybrid Inheritance

Multiple inheritance gives a class more than one direct base:

class Printable {
public:
    void print() const {}
};

class Scannable {
public:
    void scan() const {}
};

class MultifunctionDevice : public Printable, public Scannable {};

The derived object contains a subobject for each nonvirtual direct base. Multiple inheritance is useful when the type must satisfy several independent interfaces or combine carefully designed base facilities. It also introduces possible name ambiguity, repeated bases, more complex initialization and more complex conversions.

Hybrid inheritance combines two or more structural forms. A design may have multiple inheritance at one level and hierarchical or multilevel inheritance elsewhere. The term describes the shape of the inheritance graph; it does not introduce a separate language mechanism. A diamond is a common hybrid pattern: two intermediate classes derive from one shared base and a final class derives from both intermediate classes.

10. Name Hiding

A declaration in a derived class can hide all base declarations having the same name, even when their parameter lists differ:

class Base {
public:
    void show(int) {}
    void show(double) {}
};

class Derived : public Base {
public:
    void show(const char*) {}
};

Derived d;
// d.show(10);             // Base overloads are hidden

A using-declaration can bring the base overload set into derived scope:

class Derived : public Base {
public:
    using Base::show;
    void show(const char*) {}
};

Name hiding is a compile-time lookup issue. It is different from virtual overriding, which selects behavior according to an object's dynamic type.

11. Ambiguous Names from Multiple Bases

If two bases provide equally suitable members with the same name, an unqualified call is ambiguous:

class Printer {
public:
    void status() const {}
};

class Scanner {
public:
    void status() const {}
};

class Device : public Printer, public Scanner {};

Device d;
d.Printer::status();
d.Scanner::status();

Scope qualification states which base implementation is intended. A derived wrapper can instead define one coherent operation and call the appropriate base operations internally. Virtual inheritance does not automatically resolve unrelated member-name conflicts.

12. Base-Class Construction

Before the derived constructor body begins, C++ constructs:

  1. virtual base subobjects;
  2. direct nonvirtual base subobjects, in their declaration order in the base list;
  3. data members, in their declaration order;
  4. the derived constructor body.

The derived initializer list selects constructors for its direct bases:

class Employee {
public:
    Employee(std::string name, int id)
        : name_(std::move(name)), id_(id) {}
private:
    std::string name_;
    int id_;
};

class Manager : public Employee {
public:
    Manager(std::string name, int id, int level)
        : Employee(std::move(name), id), level_(level) {}
private:
    int level_;
};

If the derived constructor does not name a direct base, C++ attempts to default-construct that base. Compilation fails if no accessible default constructor exists. A derived class initializes only its direct nonvirtual bases, not indirect bases.

13. Destruction Through a Hierarchy

Destruction reverses successful construction. The most-derived destructor body runs first, followed by member destruction in reverse declaration order, then nonvirtual direct bases in reverse base-list order and finally virtual bases.

When an object may be deleted through a base pointer, the base destructor must normally be virtual:

class Document {
public:
    virtual ~Document() = default;
};

class Report : public Document {
    std::vector<int> data_;
};

Document* p = new Report;
delete p;

Deleting a derived object through a base pointer whose destructor is nonvirtual causes undefined behavior. An interface intended for polymorphic use commonly declares a public virtual destructor. A base that forbids deletion through base pointers can use a protected nonvirtual destructor as a deliberate alternative.

14. The Diamond Problem

Consider a common Device base:

class Device {
public:
    explicit Device(int id) : id_(id) {}
    int id() const { return id_; }
private:
    int id_;
};

class Printer : public Device {
public:
    Printer(int id) : Device(id) {}
};

class Scanner : public Device {
public:
    Scanner(int id) : Device(id) {}
};

class Multifunction : public Printer, public Scanner {
public:
    Multifunction(int id) : Printer(id), Scanner(id) {}
};

Multifunction contains two Device subobjects: one through Printer and one through Scanner. A conversion from Multifunction to Device is ambiguous and an unqualified call to id is ambiguous. Qualifying Printer::id or Scanner::id chooses a path, but the object still holds two independent Device states.

15. Virtual Base Classes

Virtual inheritance makes the repeated base a single shared subobject:

class Printer : virtual public Device {
public:
    Printer(int id) : Device(id) {}
};

class Scanner : virtual public Device {
public:
    Scanner(int id) : Device(id) {}
};

class Multifunction : public Printer, public Scanner {
public:
    Multifunction(int id)
        : Device(id), Printer(id), Scanner(id) {}
};

Now Multifunction contains one Device virtual-base subobject shared by both inheritance paths. Both intermediate classes must specify virtual inheritance consistently for the diamond to share that base.

Virtual inheritance solves duplicated shared-base state and the related ambiguous base conversion. It does not merge unrelated same-named functions and does not make every multiple-inheritance design sound.

16. Who Initializes a Virtual Base?

The most-derived class initializes every virtual base. When a Multifunction object is created, Multifunction's Device initializer is effective. Device initializers written in Printer and Scanner are ignored for that complete-object construction.

Those intermediate initializers are still necessary if Printer or Scanner can be constructed as a most-derived object themselves. The rule ensures that exactly one constructor initializes the one shared virtual-base subobject.

Virtual bases are constructed before all nonvirtual bases, regardless of where their initializers appear. A class with a virtual base therefore needs to understand and satisfy that base's construction requirements.

17. Pointer and Reference Conversions

A pointer or reference to a derived object can convert to an accessible, unambiguous base. The pointer value may be adjusted because the base subobject need not begin at the same address as the complete object, especially with multiple or virtual inheritance.

A base-to-derived conversion is not automatically safe. A base object is not necessarily part of the claimed derived type. Runtime-checked downcasting of polymorphic types uses dynamic_cast, while compile-time casts require proof supplied by the programmer. Casts and runtime polymorphism are developed in the following chapter.

18. Object Slicing

Copying a derived object into a base object by value keeps only the base portion:

void store(Account account);

SavingsAccount savings(1000, 0.05);
store(savings);

The Account parameter has no storage for interestRate_. Slicing is a normal consequence of value construction, not a partial reference to the original object. Use a base reference or pointer when the function must preserve the complete object's dynamic identity:

void inspect(const Account& account);

Containers of base values also slice derived objects. Polymorphic collections normally store smart pointers to the base or use a value-oriented design with an explicit cloning or type-erasure mechanism.

19. Inheritance and Encapsulation

Derived classes should depend on the base contract rather than on its representation. Making all data protected allows descendants to alter invariants directly and creates tight coupling. Private base data plus carefully chosen protected operations usually preserves stronger control.

A base class should document which operations derived classes may call, which virtual operations they may override, which invariants every override must preserve, whether objects may be copied and whether deletion through the base interface is allowed.

Inheritance exposes design commitments. Changing a protected member or a virtual contract can affect every descendant, even when ordinary clients never saw that implementation detail.

20. Choosing an Inheritance Design

Use public inheritance when the derived object can honor every meaningful expectation of the base interface. Avoid deriving merely to obtain a few convenient functions. A Rectangle/Square hierarchy, for example, becomes problematic if the base contract permits width and height to change independently but the derived type cannot preserve that behavior.

Keep hierarchies shallow unless each level expresses a stable concept. Prefer small interface bases over large bases containing unrelated responsibilities. For shared implementation without a type relationship, place a helper object inside the class.

Use multiple inheritance most confidently for independent interface-like bases. When bases contain state, examine initialization, ambiguity, ownership, layout, copying and destruction carefully. Use virtual inheritance only when one logical base subobject must be shared; its added construction rules and pointer adjustments should solve a real structural requirement.

Inspect inheritance code in terms of complete objects and subobjects. Ask how many base subobjects exist, who initializes each one, which names are accessible, which conversions are allowed and in what order destruction occurs. These questions explain most behavior in both simple and complex hierarchies.

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.