C++ Inheritance and Polymorphism Quiz: Virtual Functions Test
Test your C++ polymorphism with 14 questions and three programs on inheritance, virtual functions, virtual destructors, slicing and the vtable.
- Course: C++ study plan
- Module: Inheritance and polymorphism
- Kind: Checkpoint — cleared at 70%
- Reading time: 25 min
- Runtime: Clang 18 · C++20
Checkpoint — Inheritance and polymorphism is the checkpoint that closes the Inheritance and polymorphism module: a graded quiz and whole-program exercises, passed at 70%.
Instructions
This checkpoint covers the whole module: deriving with public and protected and the fixed order of construction and destruction; virtual, override and final and the two places dispatch does not reach; pure virtual functions, interface classes and a container of std::unique_ptr<Base>; virtual destructors and slicing; the vtable, dynamic_cast, typeid and std::variant; and is-a against has-a.
How it works. Fourteen questions and three programs. You need 70% on the questions and every program accepted to clear the module. You can retake it as often as you like; your best score counts.
Before you start, make sure you can answer these from memory:
- In what order do the base subobject, the derived class's members and the derived constructor body run — and in what order are they destroyed?
- What does
overridecatch that the compiler would otherwise accept silently? - Why does a virtual call inside a base constructor run the base version?
- What happens when
deleteruns through aBase*whose destructor is not virtual? - What does
void f(Base b)do to aDerivedargument? - What does
dynamic_cast<T*>return on failure, and what doesdynamic_cast<T&>do instead? - Why is
Square : Rectanglewith setters a broken design?
The three programs are a bank of polymorphic accounts held through std::unique_ptr, an expression tree built on an abstract Expr, and a plugin registry whose load and unload trace pins the construction and destruction order. Read each input format before writing a class.
Common questions
What happens when delete runs through a Base* whose destructor is not virtual?
Undefined behaviour. In practice only the base destructor runs: the derived destructor is skipped, so the derived members are never destroyed and whatever they own leaks. Give every polymorphic base a virtual ~Base() = default;.
What does void f(Base b) do to a Derived argument?
It slices it: the parameter is a new Base copy-constructed from the argument's base part, the derived members are dropped, and virtual calls inside f run the base versions. Take const Base& instead to keep the object's dynamic type.
What does dynamic_cast<T*> return on failure?
A null pointer, which the caller tests with if (auto* t = dynamic_cast<T*>(p)). The reference form, dynamic_cast<T&>, cannot return a null reference, so it throws std::bad_cast instead.
Why does a virtual call inside a base constructor run the base version?
While the base constructor runs, the derived part of the object has not been constructed, so its dynamic type is still the base: each constructor points the vptr at its own class's vtable before its body runs. The derived override becomes reachable only once the derived constructor takes over.
Exercises
Accounts at month end
Write an abstract Account(id, opening) with a virtual destructor, a protected balance_, non-virtual deposit, withdraw (returns false and changes nothing when the amount exceeds the balance), id() and balance(), and two pure virtual functions: std::string kind() const and void monthEnd().
Savings(id, opening, ratePercent):kind()issavings;monthEnd()addsbalance_ * rate / 100(integer arithmetic).Checking(id, opening, minimum, fee):kind()ischecking;monthEnd()subtractsfeewhen the balance is belowminimum.
Hold the accounts in a std::vector<std::unique_ptr<Account>> in input order and look them up by id with a linear search.
Input: n, then n account lines — savings <id> <opening> <rate> or checking <id> <opening> <minimum> <fee> — then commands until the input ends: deposit <id> <x>, withdraw <id> <x>, monthend. Ids always exist. Output: rejected <id> for each refused withdrawal, in order; at the end one line per account in input order: <id> <kind> <balance>.
2
savings s1 1000 5
checking c1 200 500 25
deposit s1 200
withdraw c1 300
monthend
withdraw s1 5000
monthend
prints
rejected c1
rejected s1
s1 savings 1323
c1 checking 150An expression tree
Build a tree of polymorphic nodes from a prefix expression and evaluate it.
Expr is abstract: a virtual destructor, virtual long long eval() const = 0 and virtual std::string show() const = 0. Num holds a value. Binary is an abstract intermediate class holding two std::unique_ptr<Expr> children; it implements eval and show once in terms of two new pure virtual functions, char symbol() const and long long combine(long long a, long long b) const, which Add, Sub and Mul supply. show renders (<left> <symbol> <right>).
Write a recursive std::unique_ptr<Expr> parse(std::istream& in) that reads one token: an operator (+, -, *) parses two sub-expressions; anything else is a number. Parse the left child, then the right — in two statements, because C++ does not fix the order in which two arguments of one call are evaluated.
Input: one prefix expression per line until the input ends. Numbers fit in long long; results do too. Output: <show()> = <eval()> per line.
+ 3 * 4 5
- 10 4
7
prints
(3 + (4 * 5)) = 23
(10 - 4) = 6
7 = 7Plugins loaded and unloaded
Write Plugin(name): its constructor prints register <name>, its virtual destructor prints unregister <name>, and virtual std::string status() const = 0 makes it abstract. Derive Renderer and Audio; each constructor prints <kind> <name> ready after the base has printed, each destructor (marked override) prints <kind> <name> stopped before the base does, and status() returns renderer <name> drawing or audio <name> playing.
Hold the plugins in a std::vector<std::unique_ptr<Plugin>>. When the input ends, unload every remaining plugin from the most recent back to the first, then print done.
Input: commands until the input ends: load renderer <name>, load audio <name>, status (one line per loaded plugin, oldest first), unload (the most recent; prints nothing loaded when none). Output: the trace.
load renderer gl
load audio alsa
status
unload
status
prints
register gl
renderer gl ready
register alsa
audio alsa ready
renderer gl drawing
audio alsa playing
audio alsa stopped
unregister alsa
renderer gl drawing
renderer gl stopped
unregister gl
doneIn this module: Inheritance and polymorphism
- Inheritance basics — deriving, protected and the order of construction
- Virtual functions — static type, dynamic type and override
- Abstract classes and interfaces — programming to a contract
- Virtual destructors and slicing — the two ways a hierarchy loses data
- RTTI and the vtable — how dispatch works and when to ask an object its type
- Composition versus inheritance — is-a, has-a and the shapes to avoid
- Checkpoint — Inheritance and polymorphism (this lesson)
← Composition versus inheritance — is-a, has-a and the shapes to avoid · Overloading rules — members, free functions and the compound-first idiom →