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
Managerobject is constructed, theEmployeeconstructor runs first, thenManager’s own body — not the construction of two separate objects, but two phases of a single object. Employee* base = &mdoes not produce a copy;baseandmcarry the same address, because under single, non-virtual inheritance the base subobject stands at the start of the derived object.- Passing
mto anEmployee¶meter 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 theEmployeesubobject and the fieldsManageritself 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, thenEmployee’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.