Lesson 16 • Advanced
Advanced OOP in C++
By the end of this lesson you'll be able to share data across a class with static members, grant trusted access with friend, tame multiple inheritance and the diamond problem, query an object's real type at runtime with dynamic_cast, lock hierarchies down with final/override, and correctly manage resources with the rule of three/five.
What You'll Learn
- Share state across every object with static members & methods
- Grant controlled private access using friend functions and classes
- Use multiple inheritance and solve the diamond problem with virtual bases
- Query an object's real type safely with dynamic_cast and RTTI
- Lock down hierarchies with final and catch typos with override
- Manage resources correctly with the rule of three (and five)
virtual functions from Classes & Objects. This lesson builds straight on top of those ideas.💡 Real-World Analogy
Think of a class as a company. Each employee (object) has their own desk and tasks (instance members). But the company has one shared noticeboard everyone reads — that's a static member: it belongs to the company, not to any single person. A friend is the external auditor you deliberately give a key to the private filing cabinet. Multiple inheritance is an employee reporting to two managers — and the diamond problem is when both managers ultimately report to the same director: do you get one director or two? virtual inheritance guarantees there's only ever one director.
1. Static Members & Methods
A static member belongs to the class itself, not to any individual object — there's exactly one shared copy no matter how many objects you create. A static method can be called on the class (BankAccount::howMany()) without an object, but it can only touch static data because it has no this. One catch beginners always hit: a static data member must be defined once outside the class. Read this worked example, run it, then you'll write your own.
Worked example: a shared account counter
One static counter is shared by every object; run it and watch it climb.
#include <iostream>
using namespace std;
class BankAccount {
static int accountCount; // ONE shared copy for the whole class
int balance; // each object has its OWN balance
public:
BankAccount(int opening) : balance(opening) {
++accountCount; // every new account bumps the shared counter
}
// A static method belongs to the class, not to any one object.
// It can only touch static members (there's no 'this').
static int howMany() { retur
...Your turn. The program below is almost complete — fill in the three blanks marked ___ using the // 👉 hints, then run it.
🎯 Your turn: count your players
Fill in the ___ blanks so the static counter reports 2 players.
#include <iostream>
using namespace std;
class Player {
static int playerCount; // shared across ALL players
string name;
public:
// 🎯 YOUR TURN — fill in the ___ blanks, then press "Try it Yourself".
Player(string n) : name(n) {
// 1) Increase the shared counter by one each time a Player is made
___; // 👉 ++playerCount;
}
// 2) Write a STATIC method that returns the count
static int count() { return ___; } // 👉 playerCount
...2. friend Functions & Classes
Normally private members are off-limits to the outside world. A friend declaration is you deliberately granting one specific function — or an entire class — permission to reach inside. It's a controlled exception, not a leak: the class still decides exactly who gets in. The classic use is letting a printing helper or a tightly-paired class read your internals without exposing them to everyone.
Worked example: a friend function and a friend class
A free function and an Auditor class both read a private code.
#include <iostream>
using namespace std;
class Vault {
int secretCode; // private — normally untouchable from outside
public:
Vault(int code) : secretCode(code) {}
// Grant ONE function access to our private members:
friend void reveal(const Vault& v);
// Grant a whole class access:
friend class Auditor;
};
// 'reveal' is a free function, yet it can read secretCode:
void reveal(const Vault& v) {
cout << "Vault code is: " << v.secretCode << endl; // 4242
}
clas
...3. Multiple Inheritance & the Diamond Problem
C++ lets a class inherit from more than one base at once. That power creates the diamond problem: when two bases (Printer and Scanner) both inherit the same ancestor (Device), a class inheriting both would get two copies of Device — so any member like serial becomes ambiguous. The fix is virtual inheritance: write class Printer : virtual public Device on both sides, and the compiler keeps a single shared Device. With a virtual base, the most-derived class is responsible for constructing it.
Worked example: solving the diamond with virtual inheritance
Device is built once; serial is unambiguous.
#include <iostream>
using namespace std;
// Base shared by both sides of the diamond.
class Device {
protected:
string serial;
public:
// 'virtual public' keeps ONLY ONE Device inside MultiFunctionPrinter.
Device(string sn) : serial(sn) {
cout << "Device(" << sn << ") built" << endl;
}
virtual ~Device() = default; // virtual dtor for safe deletion
string getSerial() const { return serial; }
};
class Printer : virtual public Device { // note: virtual
public:
...4. dynamic_cast, RTTI, final & override
RTTI (Run-Time Type Information) lets your program ask, while it's running, what an object's real type is. The tool is dynamic_cast: cast a base pointer down to a derived pointer and, if the object really is that type, you get a usable pointer — otherwise you get nullptr (no crash), which you test with an if. It only works on polymorphic types (those with at least one virtual function), because the check reads the vtable. Two safety keywords complete the picture: override makes the compiler verify you actually overrode a base method (catching signature typos), and final forbids any further overriding or subclassing.
Worked example: dynamic_cast, override & final
Safely detect a Dog at runtime; Cat is sealed with final.
#include <iostream>
using namespace std;
// A type is "polymorphic" once it has a virtual function — that's what
// makes dynamic_cast and RTTI possible (they read the hidden vtable).
class Animal {
public:
virtual void speak() const { cout << "..." << endl; }
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override { cout << "Woof" << endl; } // override = checked
void fetch() const { cout << "Dog fetches the ball" << endl; }
};
// 'fina
...Now you try. inspect receives a Shape* and should call radius() only when the shape is really a Circle. Fill in the two blanks:
🎯 Your turn: detect a Circle at runtime
Use dynamic_cast and check the result before calling radius().
#include <iostream>
using namespace std;
class Shape {
public:
virtual double area() const = 0; // pure virtual -> polymorphic type
virtual ~Shape() = default;
};
class Circle : public Shape {
double r;
public:
Circle(double radius) : r(radius) {}
double area() const override { return 3.14159 * r * r; }
double radius() const { return r; }
};
class Square : public Shape {
double s;
public:
Square(double side) : s(side) {}
double area() const override { ret
...5. The Rule of Three / Five
When a class owns a resource (heap memory, a file handle, a socket), the compiler's automatic copy behaviour is wrong — it copies the pointer, so two objects end up sharing and then double-freeing the same memory. The rule of three says: if you write any one of the destructor, copy constructor, or copy assignment, you almost certainly need all three. Modern C++ adds two more — the move constructor and move assignment — which cheaply steal a resource instead of copying it; together that's the rule of five. (The happiest path is the rule of zero: use std::string/std::vector/std::unique_ptr so you write none of them.)
Worked example: a resource-owning Buffer (rule of five)
See deep copy vs move; the moved-from object is left empty but valid.
#include <iostream>
#include <cstring>
using namespace std;
// This class OWNS a raw resource (heap memory), so it must manage copying
// and moving itself — the "rule of five".
class Buffer {
char* data;
size_t size;
public:
// Constructor — acquire the resource
Buffer(const char* s) : size(strlen(s)) {
data = new char[size + 1];
strcpy(data, s);
}
// 1) Destructor — release the resource
~Buffer() { delete[] data; }
// 2) Copy constructor — DEEP
...Common Errors (and the fix)
- "request for member 'x' is ambiguous" (diamond ambiguity): a class inherited the same base twice and now has two copies of
x. Make the inheritancevirtual public Baseon both intermediate classes so only one copy exists. - Forgetting to construct the virtual base: with virtual inheritance the most-derived class must call the base constructor itself — e.g.
MFP(sn) : Device(sn), Printer(sn), Scanner(sn). Leave outDevice(sn)and you get a "no matching constructor" error (or the wrong default). - "cannot dynamic_cast … (target is not pointer or reference to complete type)" / it just won't compile: you used
dynamic_caston a non-polymorphic type. Add at least onevirtualfunction (avirtual ~Base() = default;is enough) so RTTI exists. - Double free / corrupted heap (rule-of-three violation): you wrote a destructor that
deletes a pointer but let the compiler generate the copy constructor — two objects now own the same pointer and both free it. Provide a deep-copying copy constructor and copy assignment (or delete them). - "marked 'override' but does not override": your signature doesn't match the base (often a missing
const). Fix the signature — this error is exactly whyoverrideexists.
📋 Quick Reference
| Concept | Syntax | Result |
|---|---|---|
| Static data member | static int count; | One copy shared by all objects |
| Define static member | int C::count = 0; | Required once, outside the class |
| Friend function | friend void f(const C&); | f may read private members |
| Virtual inheritance | class D : virtual public B | One shared copy of B (diamond fix) |
| Safe downcast | dynamic_cast<Dog*>(a) | Dog* if it is one, else nullptr |
| Checked override | void f() const override; | Compiler verifies it overrides |
| Seal a class/method | class Cat final | No further subclassing/overriding |
| Rule of five | ~C(); C(const C&); C& operator=(…); C(C&&); … | Manage copy + move + destroy |
Pro Tips
- 💡 Prefer the rule of zero: reach for
std::vector,std::string, andstd::unique_ptrso you don't have to write any of the five special functions yourself. - 💡 Always add
override: it costs nothing and turns a silent "new method" bug into a compile error. - 💡 Prefer composition over multiple inheritance: "has-a" is usually clearer than juggling two base classes and a diamond.
- 💡 Reach for
dynamic_castsparingly: if you're testing the type a lot, avirtualmethod on the base is often the cleaner design.
Frequently Asked Questions
Q: When should I use a static member instead of a global variable?
Use a static member when the data belongs to a class but is shared by every object of that class — like a counter of how many objects exist. It lives inside the class's scope and access rules (public/private), so it's tidier and safer than a loose global variable floating in the file.
Q: Is friend a hole in encapsulation? Should I avoid it?
friend deliberately grants one specific function or class access to your private members — it's a controlled exception, not a leak. Use it sparingly for tightly-coupled helpers (operator<< for printing, or a paired class). If you reach for friend constantly, your design probably needs rethinking.
Q: Why do I need 'virtual' inheritance for the diamond problem?
Without virtual, a class that inherits the same base twice (once down each side of the diamond) gets two separate copies of that base — so a member like serialNumber becomes ambiguous. Marking the inheritance 'virtual' tells the compiler to keep just one shared copy, removing the ambiguity.
Q: Why does dynamic_cast return nullptr instead of crashing?
dynamic_cast on a pointer is the safe, checked cast: at runtime it asks 'is this object really that derived type?'. If yes you get a usable pointer; if no you get nullptr, which you test with an if. That's why it only works on polymorphic types (those with at least one virtual function) — RTTI needs the vtable to do the check.
Q: What's the difference between the rule of three and the rule of five?
The rule of three says: if you write any one of the destructor, copy constructor, or copy assignment, you almost certainly need all three (because you're managing a resource). The rule of five adds the move constructor and move assignment, which let modern C++ steal resources cheaply instead of copying. Best of all is the rule of zero: design so you need to write none of them.
Mini-Challenge: Counted Widgets
No blanks this time — just a brief and an outline. Combine a static counter with a friend printer, build it, run it, and check your output against the example in the comments.
🎯 Mini-Challenge: build counted widgets
Use a static counter plus a friend function to print each widget's id.
#include <iostream>
using namespace std;
// 🎯 MINI-CHALLENGE: Counted widgets with a friend printer
//
// 1. Make a class Widget with:
// - a STATIC int 'total' shared by all widgets
// - a private int 'id'
// - a constructor that sets id = ++total (so ids are 1, 2, 3...)
// - a STATIC method count() returning total
// 2. Add a FRIEND function void show(const Widget& w) that prints the id
// (a friend may read the private 'id').
// 3. In main: create 3 widgets, show() each, t
...🎉 Lesson Complete
- ✅
staticmembers and methods give a class one shared copy of data (define static data once outside the class) - ✅
friendgrants one function or class controlled access toprivatemembers - ✅ Multiple inheritance can hit the diamond problem;
virtual public Basekeeps a single shared base - ✅
dynamic_cast+ RTTI safely test an object's real type (polymorphic types only;nullptron failure) - ✅
overridecatches signature typos;finalseals classes and methods - ✅ The rule of three/five keeps resource-owning classes from double-freeing — or use the rule of zero
- ✅ Next lesson: Templates Deep Dive — variadic templates, SFINAE, and compile-time metaprogramming
Sign up for free to track which lessons you've completed and get learning reminders.