Skip to content
academia.sh

Lesson 08 / 14

Inheritance

The measurement of whether the base subobject a derived object carries inside it stands at the same address, and is part of a single construction/destruction sequence.

Published Updated

In this lesson

  • Defines a derived class's base subobject as a part of the object
  • Measures with an address comparison that the base and derived object share the same address
  • Observes that the constructor and destructor call order is two phases of a single object
Contents

The previous topic measured how an object constructs, copies, and releases its own resource; the class stood alone, not tied to another class. This topic addresses how one class is built on top of another: inheritance places one class inside another. The lesson’s measurement question is the same — which object does this expression work with — but now the object itself has two layers: a base subobject and the derived object wrapping it. This lesson asks: is the base subobject a separate object from the derived object, or a part of it?

The Base Subobject and the Shared Address

Manager derives from Employee. When a Manager is constructed, the Employee constructor runs first, then Manager’s own body. Naming m with a pointer of the base type (base) — does this produce a copy, or does it carry the same address? This needs to be measured with an address comparison.

// inheritance_base.cpp -- derived object's base subobject and shared address
#include <iostream>
#include <string>

class Employee {
public:
    explicit Employee(std::string name) : name_(name) {
        std::cout << "Employee constructor: " << name_ << "\n";
    }
    ~Employee() {
        std::cout << "Employee destructor: " << name_ << "\n";
    }
protected:
    std::string name_;
};

class Manager : public Employee {
public:
    Manager(std::string name, int team_no) : Employee(name), team_no_(team_no) {
        std::cout << "Manager constructor: team " << team_no_ << "\n";
    }
    ~Manager() {
        std::cout << "Manager destructor\n";
    }
private:
    int team_no_;
};

int main() {
    Manager m("Aylin", 4);
    Employee* base = &m;

    std::cout << std::boolalpha;
    std::cout << "same address for m and base "
              << (static_cast<void*>(&m) == static_cast<void*>(base)) << "\n";

    return 0;
}
Employee constructor: Aylin
Manager constructor: team 4
same address for m and base true
Manager destructor
Employee destructor: Aylin

PS1. The Manager constructor calls the Employee constructor first (“Employee constructor: Aylin”), then runs its own body (“Manager constructor: team 4”) — this is not two separate objects being constructed in sequence, it is a single object constructed in two phases. The base = &m assignment does not produce a copy, it only names the same address with a pointer of the base type: the comparison static_cast<void*>(&m) == static_cast<void*>(base) confirms this, true. On exit, as m goes out of scope, the destruction order follows construction exactly in reverse — first Manager’s own destructor, then Employee’s — again the two-phase teardown of a single object. (M08/K02 asked which type resolved the call; here the question has changed — it is not the call, but the object the expression points to that is measured, and while Java has no object slicing, assigning to a base type by value in C++ constructs a new and incomplete object; here there is only pointer access, no slicing.) The class is part: what base points to is not an object independent of m, it is the base subobject inside m.

Access Through a Base Reference: Reaching the Part, Not a Separate Object

Passing a Manager to a function with an Employee& parameter asks the same question: does the reference parameter bind to an object separate from m, or to the base part inside m?

// inheritance_reference.cpp -- access through a base reference reaches the same part
#include <iostream>
#include <string>

class Employee {
public:
    explicit Employee(std::string name) : name_(name) {}
    std::string name() const { return name_; }
protected:
    std::string name_;
};

class Manager : public Employee {
public:
    Manager(std::string name, int team_no) : Employee(name), team_no_(team_no) {}
    int team_no() const { return team_no_; }
private:
    int team_no_;
};

bool is_same_object(const Employee& e, const Manager& m) {
    return static_cast<const void*>(&e) == static_cast<const void*>(&m);
}

void label(const Employee& e) {
    std::cout << "name seen by the reference " << e.name() << "\n";
}

int main() {
    Manager m("Baris", 7);

    label(m);

    std::cout << std::boolalpha;
    std::cout << "whether the reference parameter points to the same object as m "
              << is_same_object(m, m) << "\n";

    return 0;
}
name seen by the reference Baris
whether the reference parameter points to the same object as m true

PS2. In the call label(m), m binds to an Employee&; the name() call reads the name_ field of the Employee part inside m and prints “Baris” — without ever touching the team_no_ that Manager added. In the call is_same_object(m, m), the first parameter binds m to Employee&, the second parameter binds the same m to Manager&; comparing the addresses of the two references gives true. Reference binding does not construct a copy here either — the parameter binds to the object named in the call itself (more precisely, to that object’s base part). The class is again part: what the Employee& parameter points to is not an object living outside m, it is the base subobject inside m.

The Size of an Object That Grows: A Part Enlarges the Base Object

Address equality shows the base subobject stands at the start of m; a size comparison shows the base subobject occupies space inside m, in the same memory region, not in a separate box.

// inheritance_size.cpp -- a derived object's size encloses the base part
#include <iostream>
#include <string>

class Employee {
public:
    explicit Employee(std::string name) : name_(name) {}
protected:
    std::string name_;
};

class Manager : public Employee {
public:
    Manager(std::string name, int team_no) : Employee(name), team_no_(team_no) {}
private:
    int team_no_;
};

int main() {
    std::cout << std::boolalpha;
    std::cout << "whether Manager's size is at least Employee plus an int "
              << (sizeof(Manager) >= sizeof(Employee) + sizeof(int)) << "\n";
    std::cout << "whether Manager's size is larger than Employee's "
              << (sizeof(Manager) > sizeof(Employee)) << "\n";
    return 0;
}
whether Manager's size is at least Employee plus an int true
whether Manager's size is larger than Employee's true

PS3. sizeof(Manager) carries both the full size of the Employee subobject and the team_no_ field Manager itself adds — both comparisons are true. This confirms the address equality of the first two measurements at the level of memory layout: the base subobject does not stand inside m in a separate, independently allocated box; as a physical portion of m, it occupies space within the same memory block as Manager’s own fields. The class is part: the size of a Manager object includes the size of the Employee part it carries, not as an additional reference or pointer to it.

Public, Protected, and Private Inheritance: Same Address, Different External Openness

Three classes derive from the same Base with different access specifiers: PublicDerived with public, ProtectedDerived with protected, PrivateDerived with private inheritance. Can all three access Base’s protected member from within themselves? And from outside, which one alone can be converted to Base*?

// inheritance_access_specifiers.cpp -- public protected private inheritance carry the same address, provide different external openness
#include <iostream>

class Base {
protected:
    int value_ = 42;
};

class PublicDerived : public Base {
public:
    int read() const { return value_; }
};

class ProtectedDerived : protected Base {
public:
    int read() const { return value_; }
};

class PrivateDerived : private Base {
public:
    int read() const { return value_; }
};

int main() {
    PublicDerived pub;
    ProtectedDerived prot;
    PrivateDerived priv;

    std::cout << "read() in public inheritance " << pub.read() << "\n";
    std::cout << "read() in protected inheritance " << prot.read() << "\n";
    std::cout << "read() in private inheritance " << priv.read() << "\n";

    Base* base_pub = &pub;
    std::cout << std::boolalpha
              << "whether the externally obtained base* address matches pub in public inheritance "
              << (static_cast<void*>(base_pub) == static_cast<void*>(&pub)) << "\n";

    return 0;
}
read() in public inheritance 42
read() in protected inheritance 42
read() in private inheritance 42
whether the externally obtained base* address matches pub in public inheritance true

PS19. All three classes’ read() function accesses value_ from within the class’s own body, and all three print 42 — the access specifier (public, protected, private) does not change access from within the class itself, because a protected member is already accessible from the deriving class’s own body. What changes is visibility from outside: Base* base_pub = &pub; compiles directly only because PublicDerived derives with public inheritance — the same conversion from ProtectedDerived or PrivateDerived could not present the base subobject to the outside as Base*. The address comparison is again true: the access specifier does not change PublicDerived‘s memory layout or the base subobject’s address, it only restricts which pointer type can reach that address from outside. The class is part: the value_ that read() operates on is always the class’s own base part regardless of which access specifier it was derived with — the access specifier chooses whether to make it visible, it does not change the object.

A Three-Layer Constructor Chain

The first measurement showed a two-layer chain (Employee → Manager). When the chain extends to a third layer (A → B → C), how does the construction and destruction order expand?

// inheritance_three_layer_chain.cpp -- three-layer constructor chain from the most base to the most derived
#include <iostream>

class A {
public:
    A() { std::cout << "A constructor\n"; }
    ~A() { std::cout << "A destructor\n"; }
};

class B : public A {
public:
    B() { std::cout << "B constructor\n"; }
    ~B() { std::cout << "B destructor\n"; }
};

class C : public B {
public:
    C() { std::cout << "C constructor\n"; }
    ~C() { std::cout << "C destructor\n"; }
};

int main() {
    std::cout << "-- construction --\n";
    {
        C c;
        std::cout << "-- end of scope, destruction begins --\n";
    }
    return 0;
}
-- construction --
A constructor
B constructor
C constructor
-- end of scope, destruction begins --
C destructor
B destructor
A destructor

PS20. C c; constructs a single object, but this construction splits into three phases: first the most base A, then B, then C’s own body — no matter how many layers the chain has, the order is always most base to most derived. Destruction runs in exactly the reverse order at end of scope: first C, then B, then A. This is a generalization of the two-phase construction/destruction from the first measurement — a C object actually consists of three nested parts (the A subobject, the B subobject growing inside it, and C’s own fields growing inside that), and each part is constructed only after the part before it completes. The class is part: C is a single object, but that object has three layers, and each layer is built on top of the previous one.

Assigning by Value to the Base Slices the Object: A Separate Copy

Previous measurements always accessed the base part through a pointer or a reference — the address was always the same. When a Manager object is assigned by value to a variable of type Employee, does the address stay the same, or does an independent object emerge?

// inheritance_slicing.cpp -- assigning by value to the base slices the object, producing a separate independent copy
#include <iostream>
#include <string>

class Employee {
public:
    explicit Employee(std::string name) : name_(std::move(name)) {}
    std::string name() const { return name_; }
    void set_name(std::string name) { name_ = std::move(name); }
protected:
    std::string name_;
};

class Manager : public Employee {
public:
    Manager(std::string name, int team_no) : Employee(std::move(name)), team_no_(team_no) {}
    int team_no() const { return team_no_; }
private:
    int team_no_;
};

int main() {
    Manager m("Deniz", 9);
    Employee sliced = m;

    std::cout << std::boolalpha;
    std::cout << "whether sliced and m carry the same address "
              << (static_cast<void*>(&sliced) == static_cast<const void*>(&m)) << "\n";

    sliced.set_name("Other");
    std::cout << "m's name after sliced's name is changed " << m.name() << "\n";
    std::cout << "sliced object's name " << sliced.name() << "\n";

    return 0;
}
whether sliced and m carry the same address false
m's name after sliced's name is changed Deniz
sliced object's name Other

PS21. Employee sliced = m; does not construct a pointer or a reference, it constructs a new Employee object — Employee’s copy constructor copies name_ from m’s base part, and Manager’s added team_no_ is not carried at all, because Employee’s own definition has no such field. The address comparison confirms this: false — sliced is a separate object living at its own address, independent of m. When sliced’s name is changed, m’s name stays Deniz: the two objects are independent of each other. This is a different class from the rest of the lesson: while pointer and reference access always reach the same base part, assignment by value (slicing) produces a new, independent copy of type Employee — and this copy does not carry the fields Manager added, only the part Employee defines.

Overload Selection by Static Type, Object Identity Does Not Change

The info function is overloaded for both Employee& and Manager&. When the same m object is passed to info once as Manager& and once through an Employee& reference, which overload is selected — and do both work with the same object?

// inheritance_overload_comparison.cpp -- overload selection by static type, object identity fixed at run time
#include <iostream>
#include <string>

class Employee {
public:
    explicit Employee(std::string name) : name_(std::move(name)) {}
    std::string name_;
};

class Manager : public Employee {
public:
    Manager(std::string name, int team_no) : Employee(std::move(name)), team_no_(team_no) {}
    int team_no_;
};

void info(const Employee& e) {
    std::cout << "info(Employee&) called, name " << e.name_ << "\n";
}

void info(const Manager& m) {
    std::cout << "info(Manager&) called, name " << m.name_ << ", team " << m.team_no_ << "\n";
}

int main() {
    Manager m("Kaan", 3);
    Employee& base_ref = m;

    info(m);
    info(base_ref);

    std::cout << std::boolalpha
              << "whether both calls work with the same m object "
              << (static_cast<const void*>(&m) == static_cast<const void*>(&base_ref)) << "\n";

    return 0;
}
info(Manager&) called, name Kaan, team 3
info(Employee&) called, name Kaan
whether both calls work with the same m object true

PS22. info(m) selects info(Manager&) by looking at m‘s static type (Manager) at compile time; info(base_ref) selects info(Employee&) by looking at base_ref‘s static type (Employee&) — neither is virtual, the selection is made entirely at compile time, based on the expression’s static type. This is on the same axis as the question measured in M08/K02: which overload is selected is a compile-time decision. But this lesson measures a different question: which object does the selected function work with? The address comparison is true — the object info(Manager&) works with as m and the object info(Employee&) works with as base_ref point to exactly the same object. Overload selection determines which function is called based on the static type; object identity stays the same regardless of this selection. The class is part: two different overloads work on the same object’s base part.

The Bounding Measurement

The inheritance measured in this lesson is single and non-virtual: Manager derives from a single base class, and no function was virtual, so the observation that the base subobject stands at the start of m and its address coincides with m’s is bound to these conditions — under multiple inheritance (the Multiple Inheritance and the Diamond Problem lesson), the second and later base subobjects may stand at a different offset, and their addresses may not coincide with m‘s. The measurement also does not cover the case where Employee exists on its own, without a Manager — it measured only the Employee part inside a Manager object. Which function a call through a base pointer or reference runs — whether the call looks at the static type or the real type — is not this lesson’s subject; the next lesson measures that.

Summary

  • When a Manager object is constructed, the Employee constructor runs first, then Manager’s own body — not the construction of two separate objects, but two phases of a single object.
  • Employee* base = &m does not produce a copy; base and m carry the same address, because under single, non-virtual inheritance the base subobject stands at the start of the derived object.
  • Passing m to an Employee& parameter also binds to the same address — reference binding does not construct a copy, the class is part.
  • sizeof(Manager) carries both the full size of the Employee subobject and the fields Manager itself adds; the base subobject does not stand in a separate box, it stands in a physical portion of the same object.
  • Destruction order follows construction in reverse: first Manager’s own destructor, then Employee’s — again the two-phase teardown of a single object.

Next Step

In this lesson the base subobject’s address always coincided with m‘s, because no function was virtual and the function to be called was determined at compile time, by looking at the expression’s static type. The next lesson, Virtual Functions and Vtables, measures how a call through a base pointer or reference determines which function runs based on the object’s real type — and this time what is measured is not address equality, but that the same expression always works with the same object at run time.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close