Public inheritance, protected members, virtual functions and override, abstract classes, virtual destructors, object slicing, final, dynamic_cast, and when to prefer composition.
Derive a class from a base, call the base constructor, and predict construction and destruction order
Use virtual and override so a call through a base pointer or reference runs the derived version
Write an abstract interface with pure virtual functions and give every polymorphic base a virtual destructor
Spot object slicing and avoid it by holding polymorphic objects through references or smart pointers
Use final and dynamic_cast appropriately, and choose composition over inheritance when "has-a" fits better
01
Public inheritance
✓
Inheritance lets a new class (the derived class) start from an existing one (the base class): class Manager : public Employee. A Manager has every member an Employee has, plus its own. Public inheritance models an is-a relationship: a manager is an employee, so anywhere an Employee& is expected, a Manager can be passed.
A derived object contains a complete base object inside it. So construction runs base first, then the derived members, then the derived constructor body; destruction runs in exactly the reverse order. The derived constructor chooses which base constructor to call in its member initializer list (Module 07).
#include<iostream>#include<string>classEmployee{public:Employee(std::string name,int salary):name_(std::move(name)),salary_(salary){
std::cout <<" Employee ctor\n";}~Employee(){ std::cout <<" Employee dtor\n";}const std::string&name()const{return name_;}intsalary()const{return salary_;}private:
std::string name_;int salary_;};classManager:publicEmployee{public:Manager(std::string name,int salary,int reports):Employee(std::move(name), salary),reports_(reports){// base first
std::cout <<" Manager ctor\n";}~Manager(){ std::cout <<" Manager dtor\n";}intreports()const{return reports_;}private:int reports_;};voidprintBadge(constEmployee& e){// accepts any Employee, including a Manager
std::cout <<"badge: "<< e.name()<<'\n';}intmain(){
std::cout <<"building:\n";Managerm("Meera",150000,6);
std::cout << m.name()<<" earns "<< m.salary()<<" and has "<< m.reports()<<" reports\n";printBadge(m);
std::cout <<"leaving main:\n";}
Outputcompiled & run with real C++
building:
Employee ctor
Manager ctor
Meera earns 150000 and has 6 reports
badge: Meera
leaving main:
Manager dtor
Employee dtor
Your turn
Add a third level, class Director : public Manager, with its own constructor and destructor messages. Predict the six lines before you run it.
public, protected and private inheritance
The keyword after the colon controls how the base's public members look from outside. public inheritance keeps them public and is what you want for is-a relationships almost every time. private inheritance (the default for class, which is why you should always write public) hides them and means "implemented in terms of"; composition usually says that more clearly.
02
protected members
✓
A derived class does not get access to its base's private members. They exist inside the derived object, but only the base's own functions can touch them. That is deliberate: the base keeps control of its invariant. protected is the middle ground: accessible to the class and to classes derived from it, but not to outside code.
Access
The class itself
Derived classes
Everyone else
public
yes
yes
yes
protected
yes
yes
no
private
yes
no
no
Error you will hit
'balance_' is a private member of 'Account' (from a derived class)
main.cpp:13:26: error: 'balance_' is a private member of 'Account'
13 | void addInterest() { balance_ += balance_ / 20; }
| ^
main.cpp:7:9: note: declared private here
7 | int balance_;
| ^
Why the compiler said that
Savings inherits balance_ (every Savings object contains one), but private means only Account's own member functions may use it. Clang reports the same error again for the second use on line 13. (using Account::Account; is unrelated: it inherits the base constructors.)
The fix
Prefer giving the base a protected (or public) member function that derived classes call, so the base still validates every change. Making the data member itself protected also compiles, but then every derived class can break the invariant.
Polymorphism means one call site, many behaviours: a function that takes a Shape& calls area(), and a circle computes a circle's area while a square computes a square's. That needs dynamic dispatch: choosing the function by the object's real (dynamic) type at runtime, not the declared (static) type of the pointer or reference.
In C++ you opt in per function with virtual in the base class. A derived class then overrides it with a function of exactly the same signature, and should say so with override, which makes the compiler check that it really does override something. Without virtual, the call is bound at compile time by the static type, and the base version runs. Dispatch only happens through a pointer or reference; calling on an object by value always uses its own type.
#include<iostream>#include<memory>#include<string>#include<vector>classNotifier{public:virtual~Notifier()=default;virtual std::string send(const std::string& msg)const{// virtual: dispatchedreturn"log: "+ msg;}
std::string channel()const{return"generic";}// not virtual};classEmailNotifier:publicNotifier{public:
std::string send(const std::string& msg)constoverride{return"email: "+ msg;}
std::string channel()const{return"email";}// hides, does not override};classSmsNotifier:publicNotifier{public:
std::string send(const std::string& msg)constoverride{return"sms: "+ msg.substr(0,11);}};voidalert(constNotifier& n,const std::string& msg){
std::cout << n.send(msg)<<" ["<< n.channel()<<"]\n";}intmain(){
std::vector<std::unique_ptr<Notifier>> targets;
targets.push_back(std::make_unique<Notifier>());
targets.push_back(std::make_unique<EmailNotifier>());
targets.push_back(std::make_unique<SmsNotifier>());for(constauto& t : targets)alert(*t,"disk almost full");}
Outputcompiled & run with real C++
log: disk almost full [generic]
email: disk almost full [generic]
sms: disk almost [generic]
send is virtual, so each object's own version runs. channel is not, so a call through Notifier& always runs Notifier::channel, even for an EmailNotifier.
VisualizeDynamic dispatch through a base referenceStep 1 / 4
1void alert(const Notifier& n, const std::string& msg){
An EmailNotifier is created. Because its class has virtual functions, the object carries a hidden pointer to its class's table of virtual functions (the vtable).
Variables now
e
EmailNotifier (vptr -> EmailNotifier vtable)
All 4 steps as a table
Step
Line
What happened
Variables now
1
5
An EmailNotifier is created. Because its class has virtual functions, the object carries a hidden pointer to its class's table of virtual functions (the vtable).
e = EmailNotifier (vptr -> EmailNotifier vtable)
2
6
e binds to const Notifier& n. Nothing is copied; n refers to the whole EmailNotifier.
n = refers to estatic type = Notifierdynamic type = EmailNotifier
3
2
send is virtual: the program looks it up through the object's vtable at runtime and finds EmailNotifier::send.
n = refers to ecalled = EmailNotifier::send
4
2
channel is not virtual: the compiler already chose Notifier::channel from the static type Notifier. No lookup happens.
n = refers to ecalled = Notifier::channel
Error you will hit
non-virtual member function marked 'override' hides virtual member function
main.cpp:12:19: error: non-virtual member function marked 'override' hides virtual member function
12 | double area() override { return side_ * side_; }
| ^
main.cpp:6:20: note: hidden overloaded virtual function 'Shape::area' declared here: different qualifiers ('const' vs unqualified)
6 | virtual double area() const { return 0; }
| ^
Why the compiler said that
The base function is area() const; the derived one is area() without const. To the compiler those are different signatures, so this is a brand-new function, not an override, and calls through a Shape& would still return 0. override turned that silent bug into a compile error, and the note even names the difference.
The fix
Match the signature exactly, including const. Always write override on overriding functions so mismatches are caught.
C++
1
doublearea()constoverride{return side_ * side_;}
04
Pure virtual functions and abstract classes
✓
Sometimes the base class has no sensible implementation: what is the area of a generic "shape"? Declare the function pure virtual with = 0. A class with at least one pure virtual function is abstract: you cannot create objects of it, only of derived classes that override every pure virtual function. A class made only of pure virtual functions (plus a virtual destructor) is C++'s version of an interface, like an interface in Java or C#.
main.cpp:8:11: error: variable type 'Shape' is an abstract class
8 | Shape s;
| ^
main.cpp:4:20: note: unimplemented pure virtual method 'area' in 'Shape'
4 | virtual double area() const = 0;
| ^
Why the compiler said that
A Shape object would have no area() to call. The same error appears for a derived class that forgot to override one of the pure virtual functions: for a Circle that implements area but not perimeter, clang says variable type 'Circle' is an abstract class with the note unimplemented pure virtual method 'perimeter' in 'Circle'.
The fix
Create a concrete derived class, and hold it through a Shape&, Shape* or std::unique_ptr<Shape>. If the error is about your derived class, implement the function the note names.
C++
1
std::unique_ptr<Shape> s = std::make_unique<Circle>(2.0);
05
Virtual destructors
✓
When you delete an object through a base-class pointer (which is exactly what a std::unique_ptr<Base> does), the destructor call is dispatched like any other member function. If the base destructor is not virtual, only the base destructor runs, and the derived part is never cleaned up. Formally it is undefined behaviour. The rule is simple: any class meant to be used polymorphically needs a virtual destructor, usually just virtual ~Base() = default;.
C++main.cpp
1234567891011121314151617181920
#include<iostream>#include<memory>#include<string>classLogger{public:virtual~Logger(){ std::cout <<"~Logger\n";}// virtualvirtualvoidlog(const std::string& msg){ std::cout << msg <<'\n';}};classFileLogger:publicLogger{public:~FileLogger()override{ std::cout <<"~FileLogger: flush and close file\n";}voidlog(const std::string& msg)override{ std::cout <<"[file] "<< msg <<'\n';}};intmain(){
std::unique_ptr<Logger> logger = std::make_unique<FileLogger>();
logger->log("started");}// unique_ptr deletes through Logger*: both destructors run, derived first
Outputcompiled & run with real C++
[file] started
~FileLogger: flush and close file
~Logger
Error you will hit
delete called on non-final class that has virtual functions but non-virtual destructor
$ c++ -std=c++20 -Wall main.cpp && ./a.out
main.cpp:19:5: warning: delete called on non-final 'Logger' that has virtual functions but non-virtual destructor [-Wdelete-non-abstract-non-virtual-dtor]
19 | delete logger;
| ^
1 warning generated.
[file] started
~Logger
Why the compiler said that
The warning only appears with -Wall; a plain compile is silent. The run shows the damage: log was dispatched to FileLogger, but the destructor was not, so ~FileLogger never ran and the "file" was never flushed or closed. Any members FileLogger had (strings, vectors, handles) were never destroyed either. The same happens with std::unique_ptr<Logger>, and then there is not even a delete line to warn about.
The fix
Make the base destructor virtual. Then every destructor in the chain runs, most-derived first.
Polymorphism needs a pointer or reference. If you copy a derived object into a base-class value, for example by passing it by value, assigning it to a base variable, or storing it in a std::vector<Base>, only the base part is copied. The derived members are "sliced off", and the copy is a genuine base object whose virtual calls go to the base versions. It compiles without a warning.
#include<iostream>#include<memory>#include<string>#include<vector>classAnimal{public:virtual~Animal()=default;virtual std::string sound()const{return"...";}};classDog:publicAnimal{public:
std::string sound()constoverride{return"Woof";}};voidbyValue(Animal a){ std::cout <<"by value: "<< a.sound()<<'\n';}voidbyReference(constAnimal& a){ std::cout <<"by reference: "<< a.sound()<<'\n';}intmain(){Dog rex;byValue(rex);// copies only the Animal part: slicedbyReference(rex);// refers to the whole Dog
std::vector<Animal> sliced;
sliced.push_back(rex);// stores an Animal, not a Dog
std::cout <<"vector<Animal>: "<< sliced[0].sound()<<'\n';
std::vector<std::unique_ptr<Animal>> zoo;
zoo.push_back(std::make_unique<Dog>());
std::cout <<"vector<unique_ptr<Animal>>: "<< zoo[0]->sound()<<'\n';}
Outputcompiled & run with real C++
by value: ...
by reference: Woof
vector<Animal>: ...
vector<unique_ptr<Animal>>: Woof
Your turn
Make Animal abstract by changing sound to = 0. Now byValue and std::vector<Animal> stop compiling. That is a strong reason to make polymorphic bases abstract: slicing becomes impossible.
Rule of thumb
Pass polymorphic objects as const Base& (or Base&), store them as std::unique_ptr<Base>, and never pass or store them by value. Some teams go further and delete the base's copy operations so slicing cannot compile.
07
final and dynamic_cast
✓
final stops further inheritance. On a class (class CardPayment final : public Payment) nobody can derive from it; on a virtual function (int fee() const final) nobody can override it further. It documents a design decision and lets the compiler skip the virtual lookup when it knows the exact type.
dynamic_cast<Derived*>(basePtr) asks at runtime "is this object really a Derived?" It returns a valid pointer if so and nullptr if not (the reference form, dynamic_cast<Derived&>, throws std::bad_cast instead). It only works on polymorphic classes, those with at least one virtual function. A chain of dynamic_casts to decide what to do is usually a sign that the behaviour should be a virtual function instead; use it for the occasional genuinely type-specific extra.
main.cpp:12:28: error: base 'CardPayment' is marked 'final'
12 | class PremiumCard : public CardPayment {};
| ^
main.cpp:7:7: note: 'CardPayment' declared here
7 | class CardPayment final : public Payment {
| ^ ~~~~~
main.cpp:16:14: error: no member named 'fee' in 'PremiumCard'
16 | return p.fee();
| ~ ^
Why the compiler said that
The author of CardPayment declared it final, so deriving from it is rejected. The second error is fallout from the first: once the base is refused, PremiumCard has no members at all. Fix the first error of a cascade and the rest usually disappear.
The fix
Derive from the non-final base (Payment) instead, or, if you own CardPayment and extension is really intended, remove final.
Multiple inheritance, the diamond, and composition
✓
A C++ class may have several base classes: class Copier : public Scanner, public Printer. It works well for inheriting several interfaces (abstract classes with no data). It gets awkward when two bases share a common base of their own, the diamond: by default a Copier then contains two separate Device sub-objects, and any use of a Device member is ambiguous.
Error you will hit
non-static member 'id' found in multiple base-class subobjects
C++
1234567891011121314
#include<iostream>structDevice{int id =0;};structScanner:Device{};structPrinter:Device{};structCopier:Scanner,Printer{};intmain(){Copier c;
c.id =7;
std::cout << c.id <<'\n';}
main.cpp:12:7: error: non-static member 'id' found in multiple base-class subobjects of type 'Device':
struct Copier -> Scanner -> Device
struct Copier -> Printer -> Device
12 | c.id = 7;
| ^
main.cpp:4:9: note: member found by ambiguous name lookup
4 | int id = 0;
| ^
Why the compiler said that
There really are two ids in a Copier, one reached through Scanner and one through Printer, and the compiler lists both paths. It cannot know which you mean. (Clang reports line 13 the same way.)
The fix
If the device should exist once, inherit it virtually: struct Scanner : virtual Device and struct Printer : virtual Device. Better still, ask whether inheritance is the right tool at all.
C++main.cpp
123456789101112131415
#include<iostream>structDevice{int id =0;Device(){ std::cout <<"Device ctor\n";}};structScanner:virtualDevice{};// virtual inheritance: share one DevicestructPrinter:virtualDevice{};structCopier:Scanner,Printer{};intmain(){Copier c;
c.id =7;// no longer ambiguous
std::cout <<"id "<< c.id <<", sizeof(Device) "<<sizeof(Device)<<'\n';}
Outputcompiled & run with real C++
Device ctor
id 7, sizeof(Device) 4
Only one Device ctor line: the most-derived class (Copier) constructs the single shared Device. Virtual inheritance adds hidden pointers and some complexity, which is why most codebases avoid diamonds with data.
Composition over inheritance
Inheritance is the tightest coupling C++ has: the derived class depends on the base's protected members, constructor and every future change. Before inheriting, ask "is-a or has-a?" A car has an engine; it is not an engine. Composition means holding the other object as a member and calling it. It is easier to change, test and reason about, and it is the default choice in most modern C++ codebases; inheritance is kept for genuine is-a relationships where you need polymorphism.
Checkout does not inherit from a gateway; it holds one behind an interface. Swapping in a real gateway, or a different fake in a unit test, needs no change to Checkout. This is the Strategy pattern, covered with the other classic patterns in Design Patterns.
Base / derived class
In class D : public B, B is the base and D is derived. A D contains a complete B.
protected
Access for the class and classes derived from it, but not for outside code.
Virtual function
A member function whose call through a base pointer or reference is dispatched to the object's real type at runtime.
override
Marks a function as overriding a base virtual function. The compiler errors if it does not.
Dynamic dispatch
Choosing which function to run at runtime from the object's dynamic type, usually through a vtable.
vtable
A per-class table of virtual function pointers. Each polymorphic object holds a hidden pointer to its class's table.
Pure virtual function
A virtual function declared = 0, with no implementation required in the base.
Abstract class
A class with at least one pure virtual function. It cannot be instantiated.
Virtual destructor
A destructor declared virtual, so deleting through a base pointer runs every destructor in the chain.
Object slicing
Copying a derived object into a base value, which keeps only the base part and loses the derived behaviour.
final
On a class: no class may derive from it. On a virtual function: no further overrides.
dynamic_cast
A checked runtime cast within a polymorphic hierarchy. Returns nullptr (pointers) or throws std::bad_cast (references) on failure.
Virtual inheritance
class B : virtual A: all paths to A in a diamond share one A sub-object.
Composition
Building a class by holding other objects as members (has-a) instead of inheriting from them (is-a).
Quick check
Base has virtual void f() and void g() (not virtual). Derived defines both. What runs for Base& r = derivedObj; r.f(); r.g();?
f is virtual, so the call is dispatched on the object's real type: Derived::f. g is not virtual, so it is chosen at compile time from the reference's static type Base: Base::g.
Quick check
Why should a base class that is used polymorphically have a virtual destructor?
Without virtual, delete basePtr calls only ~Base, the derived part is never destroyed, and the behaviour is undefined. Clang warns about it with -Wall when it can see the delete.
What is the difference between virtual and pure virtual functions in C++?
A virtual function has an implementation in the base class that derived classes may override. A pure virtual function (= 0) has no required implementation, makes the class abstract so it cannot be instantiated, and must be overridden by every concrete derived class.
What is object slicing in C++?
Copying a derived object into a variable of the base type, for example by passing it by value or storing it in std::vector<Base>. Only the base part is copied, so derived data and overridden behaviour are lost. Pass polymorphic objects by reference and store them as std::unique_ptr<Base> to avoid it.
Should I use inheritance or composition in C++?
Prefer composition (holding an object as a member) unless the relationship is truly is-a and you need to use the derived type through a base pointer or reference. A common middle ground is composition with an abstract interface, so implementations can be swapped without inheritance between concrete classes.
Finish the C++ handbook, then get hired
Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.