Complete Reference · File 02 of 05
C++ Programming

OOP Mastery
Classes, Inheritance & Polymorphism

The complete object-oriented programming guide for C++. Every concept from class anatomy to virtual dispatch, from operator overloading to the Rule of Five — with full working code at every step. This is the file that separates programmers from software engineers.

Ch 1 · Classes & Objects Ch 2 · Constructors & Destructors Ch 3 · Encapsulation Ch 4 · Inheritance Ch 5 · Polymorphism & Virtual Ch 6 · Operator Overloading Ch 7 · Abstract Classes & Interfaces Ch 8 · Rule of Five Ch 9 · OOP Design Principles Ch 10 · Traps & Master Reference
Chapter 01
Classes & Objects — The Blueprint and the Instance

1.1   What is OOP? The Core Idea Foundation

Object-Oriented Programming is a paradigm where you model your program as a collection of objects — entities that bundle data (attributes) and behaviour (methods) together. Instead of writing functions that operate on separate data, you define types that know how to manage themselves.

The 4 Pillars

Encapsulation — hide internal state
Abstraction — expose only what's needed
Inheritance — reuse and extend types
Polymorphism — one interface, many forms

Class vs Object

A class is a blueprint — it defines what data and operations the type has. An object is an instance — actual memory allocated from that blueprint. One class → many objects.

Why OOP?

Models real-world entities naturally. Enables code reuse through inheritance. Reduces complexity by hiding implementation. Makes large codebases manageable through clear interfaces.

1.2   Anatomy of a Class Core

Motivation: Life Without Classes

Before OOP, programs were written as loose variables passed into standalone functions. To model a bank account, you'd have double balance1 = 1000; string owner1 = "Alice"; and pass them to withdraw(balance1, 50.0);. When you added a second account, you had to duplicate all variables. It quickly became unmanageable. Classes solve this by bundling the data (balance, owner) and the functions (withdraw) into a single, self-contained unit. You no longer pass loose data to functions; the object carries its own data and knows how to modify itself.

A class declaration has member variables (data) and member functions (methods). By default, class members are private — only accessible within the class itself.

class BankAccount { private: // Hidden from outside — accessible only within class std::string owner; double balance; int accountNumber; public: // Accessible from anywhere // Constructor — initializes the object BankAccount(std::string name, double initialBalance) { owner = name; balance = initialBalance; accountNumber = generateAccountNumber(); } // Member functions (methods) void deposit(double amount) { if (amount > 0) balance += amount; } bool withdraw(double amount) { if (amount > 0 && balance >= amount) { balance -= amount; return true; } return false; } // Getter — read-only access to private data double getBalance() const { return balance; } std::string getOwner() const { return owner; } void printStatement() const; // Declaration only — defined outside class private: int generateAccountNumber() { return rand() % 900000 + 100000; } }; // Define member function outside class — use ClassName:: scope operator void BankAccount::printStatement() const { std::cout << "Account: " << accountNumber << " | Owner: " << owner << " | Balance: $" << balance << "\n"; }
The const after a member function

A const after the parameter list (e.g., double getBalance() const) means this function does not modify any member variables. It promises read-only access. Always mark getter/inspection methods as const — it allows them to be called on const objects and communicates intent clearly.

1.3   Creating and Using Objects Core

// Stack allocation — object destroyed when it goes out of scope BankAccount acc1("Alice", 1000.0); BankAccount acc2{"Bob", 500.0}; // Brace initialization (preferred) acc1.deposit(200.0); acc1.withdraw(50.0); acc1.printStatement(); // Account: 123456 | Owner: Alice | Balance: $1150 // Heap allocation — object lives until explicitly deleted BankAccount* acc3 = new BankAccount("Carol", 2000.0); acc3->deposit(500.0); // Arrow operator for pointer to object acc3->printStatement(); delete acc3; // MUST free heap objects manually acc3 = nullptr; // Modern C++ — use unique_ptr instead (no manual delete needed) auto acc4 = std::make_unique<BankAccount>("Dave", 3000.0); acc4->deposit(100.0); // acc4 is automatically deleted when it goes out of scope // Array of objects BankAccount accounts[3] = { {"Eve", 100.0}, {"Frank", 200.0}, {"Grace", 300.0} }; for (auto& a : accounts) a.printStatement();

The this Pointer

Inside every non-static member function, this is an implicit pointer to the current object. It's used to disambiguate member variables from parameters of the same name, and to enable method chaining.

class Builder { int x, y; public: Builder& setX(int x) { this->x = x; // this->x = member variable, x = parameter return *this; // Return reference to self for chaining } Builder& setY(int y) { this->y = y; return *this; } void build() { std::cout << x << ", " << y << "\n"; } }; // Method chaining — each call returns *this Builder b; b.setX(10).setY(20).build(); // 10, 20

1.4   Static Members — Class-Level Data Core

Static members belong to the class itself, not to any individual object. All instances share a single copy of a static member. Use them for class-wide counters, shared configuration, or factory functions.

class Counter { private: int id; static int count; // Declaration — one copy shared by ALL objects public: Counter() { count++; id = count; } ~Counter() { count--; } static int getCount() { // Static method — no 'this' pointer return count; } int getId() const { return id; } }; // Definition of static member — must be OUTSIDE the class, in a .cpp file int Counter::count = 0; int main() { std::cout << Counter::getCount() << "\n"; // 0 Counter a, b, c; std::cout << Counter::getCount() << "\n"; // 3 { Counter d; std::cout << Counter::getCount() << "\n"; // 4 } // d destroyed here, destructor called std::cout << Counter::getCount() << "\n"; // 3 }
Static Member Must Be Defined Outside the Class

Declaring static int count; inside the class is only a declaration. You must provide a definition in exactly one .cpp file: int Counter::count = 0;. Forgetting this gives a linker error: "undefined reference to Counter::count". In C++17, you can use inline static int count = 0; inside the class to avoid this.

1.5   Chapter Traps & Key Points

TrapWrongCorrect
Calling non-const method on const objectconst BankAccount a; a.deposit(10); // errorMark read-only methods const; they work on const objects
Forgetting semicolon after classclass Foo { } // missing ;class Foo { }; // class definition must end with ;
Static member definitionStatic int declared but not defined outside classint MyClass::staticVar = 0; in .cpp (or inline static in C++17)
this in static methodUsing this inside a static methodStatic methods have no this pointer — they don't belong to an instance
Returning this by valueBuilder setX() { return *this; } (copies!)Builder& setX() { return *this; } (reference for chaining)
↑ back to top
Chapter 02
Constructors & Destructors — Birth, Life & Death of Objects

2.1   Constructors — Initializing Objects Core

A constructor is a special member function called automatically when an object is created. It has the same name as the class, no return type (not even void), and is responsible for initializing the object to a valid state.

class Rectangle { private: double width, height; public: // 1. Default constructor — no parameters Rectangle() { width = 0.0; height = 0.0; } // 2. Parameterized constructor Rectangle(double w, double h) { width = w; height = h; } // 3. PREFERRED: Member Initializer List — faster and required for const/refs Rectangle(double w, double h, bool validate) : width(w > 0 ? w : 0), // Initialize width before constructor body runs height(h > 0 ? h : 0) // Comma-separated list after colon { // Constructor body — runs AFTER member initializer list if (validate && (w <= 0 || h <= 0)) std::cout << "Warning: invalid dimensions\n"; } double area() const { return width * height; } }; Rectangle r1; // Default: 0×0 Rectangle r2(5.0, 3.0); // Parameterized: 5×3 Rectangle r3{4.0, 6.0, true}; // With validation
Member Initializer List — Why Prefer It

Without initializer list: members are default-initialized first, then assigned in the constructor body — two operations. With initializer list: members are initialized directly — one operation. For const members, reference members, and members with no default constructor, initializer list is the only option. Always prefer it.

Delegating Constructors (C++11)

class Point { double x, y, z; public: Point(double x, double y, double z) : x(x), y(y), z(z) {} // Delegating constructor — calls another constructor, reduces code duplication Point(double x, double y) : Point(x, y, 0.0) {} // Delegates to 3-arg version Point() : Point(0.0, 0.0, 0.0) {} // Delegates to 3-arg version };

explicit Keyword — Preventing Implicit Conversions

class Wrapper { public: explicit Wrapper(int val) : value(val) {} // explicit prevents auto-conversion private: int value; }; Wrapper w1(42); // OK: direct initialization Wrapper w2 = 42; // ERROR if explicit! Prevents: "int can be used as Wrapper" Wrapper w3{42}; // OK: brace initialization always fine // Without explicit, this would silently convert: // void f(Wrapper w); f(42); ← dangerous implicit conversion // With explicit, f(42) is a compile error. You must write f(Wrapper{42}).

2.2   Destructors — Cleaning Up Critical

A destructor is called automatically when an object is destroyed (goes out of scope, or delete is called on a heap object). It has the same name as the class prefixed with ~ and no parameters. Its job: release all resources acquired by the object — memory, file handles, network connections, locks.

class DynamicArray { private: int* data; int size; public: // Constructor: acquire resource (heap memory) DynamicArray(int n) : size(n) { data = new int[n]; // Allocate n ints on heap std::cout << "Constructed, allocated " << n << " ints\n"; } // Destructor: release resource ~DynamicArray() { delete[] data; // MUST free heap memory std::cout << "Destructed, freed memory\n"; } int& operator[](int i) { return data[i]; } int getSize() const { return size; } }; int main() { { DynamicArray arr(10); // Constructor called arr[0] = 42; } // Scope ends → destructor automatically called → memory freed // No memory leak! Destructor handled it. }
RAII — Resource Acquisition Is Initialization

This is the most important C++ idiom. Tie the lifetime of a resource to the lifetime of an object: acquire in the constructor, release in the destructor. When the object goes out of scope, the destructor runs — resource is always released. No manual cleanup. No leaks. This is how std::vector, std::fstream, std::unique_ptr, and mutexes all work.

Virtual Destructor Rule

If a class has virtual methods (is meant to be a base class), its destructor must be virtual. Without it, deleting a derived object through a base pointer only calls the base destructor — the derived destructor never runs — causing resource leaks. Rule: if a class has any virtual function, make the destructor virtual too.

2.3   Copy Constructor & Copy Assignment Critical

When you copy an object, C++ calls either the copy constructor (when creating a new object) or copy assignment operator (when assigning to an existing object). By default, C++ generates these as shallow copies — copying each member value directly.

class MyString { private: char* data; int len; public: // Constructor MyString(const char* str) { len = strlen(str); data = new char[len + 1]; strcpy(data, str); } // COPY CONSTRUCTOR — called when: MyString b(a); or MyString b = a; MyString(const MyString& other) { len = other.len; data = new char[len + 1]; // Deep copy: allocate NEW memory strcpy(data, other.data); // Copy the content, not just the pointer std::cout << "Copy constructor called\n"; } // COPY ASSIGNMENT OPERATOR — called when: existing_obj = other_obj; MyString& operator=(const MyString& other) { if (this == &other) return *this; // Self-assignment guard — CRITICAL delete[] data; // Free existing memory len = other.len; data = new char[len + 1]; strcpy(data, other.data); return *this; // Return *this for chain assignment (a=b=c) } ~MyString() { delete[] data; } void print() const { std::cout << data << "\n"; } };
Shallow Copy — The Pointer Sharing Disaster

If you manage heap memory and don't define a copy constructor, the compiler generates a shallow copy that copies the pointer value — both objects now point to the same heap memory. When either object is destroyed, it frees that memory. The other object now has a dangling pointer. When it's destroyed, it tries to free already-freed memory → double free → crash. Always define a deep copy when you own heap memory.

2.4   Chapter Traps & Key Points

TrapWrongCorrect
Initializer list orderMembers initialized in list order, NOT declaration orderMembers ALWAYS initialized in declaration order. Keep list in same order.
Missing self-assignment checkop= without if(this==&other) — deletes own data firstAlways check self-assignment before any delete in op=
Shallow copy with raw pointerDefault copy copies pointer address — shared ownershipDefine copy constructor + op= to deep copy heap data
Non-virtual destructor in baseBase* p = new Derived(); delete p; — derived destructor never calledvirtual ~Base() {} whenever the class is a base class
explicit constructorImplicit conversion: void f(A); f(42); silently constructs AMark single-param constructors explicit to prevent surprise conversions
↑ back to top
Chapter 03
Encapsulation & Access Control — Hiding the Wires

3.1   Access Specifiers — public, private, protected Core

SpecifierAccessible FromUse When
publicAnywhere — outside the class, derived classes, everywhereInterface: what the user of the class is meant to call
privateOnly within this class (not even derived classes)Implementation details: internal data and helper functions
protectedThis class AND derived classes onlyData/methods that subclasses need but external code shouldn't touch
class Person { private: std::string ssn; // Nobody outside gets this — not even subclasses double bankBalance; protected: std::string name; // Subclasses (Employee, Student) can access this int age; public: Person(std::string n, int a, std::string id, double bal) : name(n), age(a), ssn(id), bankBalance(bal) {} std::string getName() const { return name; } // Public getter int getAge() const { return age; } // No getter for ssn — that data should never leave the class }; class Employee : public Person { std::string department; public: void introduce() { std::cout << name << ", age " << age; // OK — protected access // std::cout << ssn; ERROR — ssn is private in Person } };
struct vs class — One Difference

In C++, struct and class are nearly identical. The only difference: struct members are public by default, class members are private by default. Convention: use struct for simple data containers (POD types), use class for types with significant behavior and invariants to protect.

3.2   Getters, Setters & Invariant Protection Core

The core payoff of encapsulation: by forcing access through methods, you can validate input, enforce invariants, and change the internal representation without breaking external code.

class Temperature { private: double celsius; // Internal representation: always in Celsius public: Temperature(double c) { setCelsius(c); // Reuse setter for validation even in constructor } // Getter double getCelsius() const { return celsius; } double getFahrenheit() const { return celsius * 9.0/5.0 + 32; } double getKelvin() const { return celsius + 273.15; } // Setter with invariant enforcement void setCelsius(double c) { if (c < -273.15) // Absolute zero — physically impossible throw std::invalid_argument("Temperature below absolute zero"); celsius = c; } void setFahrenheit(double f) { setCelsius((f - 32) * 5.0/9.0); // Convert and delegate } }; // Public interface hides internal representation completely: // We can change internal storage to Kelvin without changing the API!
Don't Write Mindless Getters/Setters

A class with a private member x and public getX()/setX() is not encapsulated — it's just bureaucracy. Real encapsulation means the class protects an invariant. Before adding a setter, ask: what rules must hold? Enforce those in the setter. If there are no rules, consider if the member needs to be private at all.

3.3   friend — Controlled Encapsulation Bypass Use Carefully

A friend declaration grants a specific external function or class access to private/protected members. It's an intentional, controlled bypass of encapsulation — useful for operator overloading and tightly coupled classes.

class Vector2D { private: double x, y; public: Vector2D(double x, double y) : x(x), y(y) {} // Friend function — NOT a member, but can access private x, y friend std::ostream& operator<<(std::ostream& os, const Vector2D& v); // Friend class — Matrix can access Vector2D's private members friend class Matrix; }; std::ostream& operator<<(std::ostream& os, const Vector2D& v) { os << "(" << v.x << ", " << v.y << ")"; // Accesses private x, y return os; } Vector2D v(3.0, 4.0); std::cout << v << "\n"; // (3, 4)
Overusing friend Breaks Encapsulation

Friendship is not inherited and not transitive. But overusing it defeats the purpose of encapsulation. If you find yourself adding many friend declarations, it's a sign your design needs rethinking. Use friend primarily for operator overloading (especially operator<<) and for pairs of tightly coupled classes that form a single logical unit.

3.4   Chapter Traps & Key Points

TrapWrongCorrect
Returning private data by non-const referencedouble& getBalance() — caller can modify private member!const double& getBalance() const — or return by value for small types
Protected data in large hierarchiesProtected data accessible to all subclasses — hard to reason aboutPrefer private data + protected getters/setters for controlled access
Mutable keyword abusemutable on data that changes logical state (breaks const correctness)mutable only for caching/lazy eval that doesn't affect observable state (e.g., cached hash)
↑ back to top
Chapter 04
Inheritance — Reuse, Extension & Hierarchies

4.1   Single Inheritance Core

Motivation: Avoiding Redundancy

Imagine writing a game with a Dog and a Cat. Both need a name, an age, and an eat() method. Without inheritance, you would copy and paste those variables and methods into both classes. When you inevitably find a bug in eat(), you have to fix it in every single animal class. Inheritance allows you to write the shared logic exactly once in a Base class (Animal), and let the Derived classes only specify what makes them unique.

Inheritance lets a derived class (child) acquire the properties and methods of a base class (parent). The derived class is a base class — it can be used anywhere a base class is expected. This is the Liskov Substitution Principle.

class Animal { // Base class protected: std::string name; int age; public: Animal(std::string n, int a) : name(n), age(a) {} virtual ~Animal() {} // Virtual destructor — ALWAYS in base class void eat() const { std::cout << name << " is eating\n"; } virtual void speak() const { // virtual = can be overridden by derived class std::cout << name << " makes a sound\n"; } std::string getName() const { return name; } }; class Dog : public Animal { // Dog IS-A Animal (public inheritance) private: std::string breed; public: Dog(std::string n, int a, std::string b) : Animal(n, a), // Call base class constructor FIRST via init list breed(b) {} void speak() const override { // override keyword (C++11) — compiler checks this std::cout << name << " says: Woof!\n"; } void fetch() const { // New method, only in Dog std::cout << name << " fetches the ball!\n"; } std::string getBreed() const { return breed; } }; class Cat : public Animal { public: Cat(std::string n, int a) : Animal(n, a) {} void speak() const override { std::cout << name << " says: Meow!\n"; } }; int main() { Dog d("Rex", 3, "Labrador"); d.eat(); // Inherited from Animal: "Rex is eating" d.speak(); // Overridden in Dog: "Rex says: Woof!" d.fetch(); // Dog-only method }
Inheritance Modes

class Dog : public Animal — public members of Animal remain public in Dog. Most common; models "IS-A" relationship.
class Dog : protected Animal — public/protected of Animal become protected in Dog.
class Dog : private Animal — all Animal members become private in Dog; models "IMPLEMENTED-IN-TERMS-OF". Very rare.

4.2   Constructor & Destructor Order in Inheritance Critical

class Base { public: Base() { std::cout << "Base constructor\n"; } ~Base() { std::cout << "Base destructor\n"; } }; class Derived : public Base { public: Derived() { std::cout << "Derived constructor\n"; } ~Derived() { std::cout << "Derived destructor\n"; } }; int main() { Derived d; // Output: // Base constructor ← base ALWAYS constructed first // Derived constructor // Derived destructor ← derived ALWAYS destroyed first (reverse order) // Base destructor }
Construction and Destruction Order

Construction: Base → Derived. The base must exist before the derived part is built on top of it.
Destruction: Derived → Base. Reverse of construction. The derived part is torn down first, then the base part.
This is symmetric and automatic. You can't change this order.

4.3   Multiple Inheritance & Diamond Problem Advanced

C++ supports inheriting from multiple base classes. This is powerful but can cause the diamond problem when two bases share a common ancestor.

class Flyable { public: virtual void fly() { std::cout << "Flying\n"; } virtual ~Flyable() {} }; class Swimmable { public: virtual void swim() { std::cout << "Swimming\n"; } virtual ~Swimmable() {} }; // Duck inherits from BOTH — perfectly valid class Duck : public Animal, public Flyable, public Swimmable { public: Duck(std::string n) : Animal(n, 1) {} void speak() const override { std::cout << name << ": Quack!\n"; } }; Duck d("Donald"); d.eat(); // From Animal d.fly(); // From Flyable d.swim(); // From Swimmable d.speak(); // Overridden in Duck

The Diamond Problem & Virtual Inheritance

Diamond Inheritance Problem Person / \ Student Worker \ / WorkStudy ← Has TWO copies of Person without virtual inheritance!
class Person { public: std::string name; }; // WITHOUT virtual: WorkStudy would have TWO copies of Person (ambiguous!) class Student : virtual public Person { ... }; // virtual inheritance class Worker : virtual public Person { ... }; // virtual inheritance class WorkStudy : public Student, public Worker { // Now only ONE copy of Person — shared between Student and Worker branches }; WorkStudy ws; ws.name = "Alice"; // No ambiguity — only one Person::name
Prefer Composition Over Multiple Inheritance

Multiple inheritance is complex and rarely necessary. Before reaching for it, ask: can I model this with composition (having a member of that type) or interfaces (pure abstract base classes)? Most real-world cases where you think you need multiple inheritance are better solved with composition + interfaces. Keep multiple inheritance for mixins and interface-only base classes.

4.4   Chapter Traps & Key Points

TrapWrongCorrect
Not calling base constructorDerived() {} — base default-initialized (may crash)Derived() : Base(args) {} — explicitly chain base constructor
SlicingAnimal a = Dog("Rex",3,"Lab"); — Dog part is sliced off!Animal* a = new Dog(...); or Animal& a = dog; — use pointer/reference
override not usedvoid speak() const — typo in signature = new function, not overrideAlways use override keyword — compiler catches signature mismatches
Calling virtual in constructorVirtual calls in constructors go to BASE version (polymorphism inactive)Never rely on virtual dispatch in constructors/destructors
Diamond without virtualTwo copies of base class, ambiguous member accessUse virtual inheritance when diamond structure is needed
↑ back to top
Chapter 05
Polymorphism & Virtual Functions — One Interface, Many Forms

5.1   Virtual Functions & the vtable Critical

Motivation: The Base Pointer Problem

Imagine you have an array of 50 different shapes, and you want to call area() on all of them. To put them in one array, they must all be stored as pointers to the base class: Shape*. But if area() is a normal function, calling shapeArray[0]->area() will ALWAYS execute the base Shape version of the function—even if the object is actually a Circle! The compiler only looks at the pointer type. virtual fixes this. It tells the compiler: "Wait until runtime, look at the ACTUAL object this pointer is aiming at, and call its specific version."

Polymorphism means "many forms" — the same function call can execute different code depending on the actual runtime type of the object. C++ achieves this through virtual functions and the virtual dispatch mechanism (vtable).

class Shape { public: virtual double area() const = 0; // Pure virtual — must override virtual double perimeter() const = 0; virtual void draw() const { // Virtual with default implementation std::cout << "Drawing a shape\n"; } virtual ~Shape() {} }; class Circle : public Shape { double radius; public: Circle(double r) : radius(r) {} double area() const override { return M_PI * radius * radius; } double perimeter() const override { return 2 * M_PI * radius; } void draw() const override { std::cout << "Drawing circle r=" << radius << "\n"; } }; class Rectangle : public Shape { double w, h; public: Rectangle(double w, double h) : w(w), h(h) {} double area() const override { return w * h; } double perimeter() const override { return 2*(w+h); } }; // POLYMORPHISM IN ACTION std::vector<Shape*> shapes; shapes.push_back(new Circle(5.0)); shapes.push_back(new Rectangle(4.0, 6.0)); shapes.push_back(new Circle(3.0)); for (Shape* s : shapes) { // Correct function called at RUNTIME based on actual type std::cout << "Area: " << s->area() << "\n"; s->draw(); // Circle::draw or Rectangle's inherited draw } // Clean up for (Shape* s : shapes) delete s;
How the vtable Works

When a class has virtual functions, the compiler creates a vtable (virtual function table) — an array of function pointers. Each object of that class gets a hidden vptr pointing to its class's vtable. When you call s->area() through a base pointer, C++ follows the vptr to find the right function. This is called dynamic dispatch and has a tiny overhead (~indirection through a pointer). The payoff: you write code against the base interface and it works correctly for all current and future derived types.

vtable layout Circle object Circle vtable ┌──────────────┐ ┌─────────────────────┐ │ vptr ─────── │ ──────────► │ Circle::area() │ │ radius: 5.0 │ │ Circle::perimeter() │ └──────────────┘ │ Circle::draw() │ └─────────────────────┘ Rectangle object Rectangle vtable ┌──────────────┐ ┌──────────────────────────┐ │ vptr ─────── │ ──────────► │ Rectangle::area() │ │ w: 4.0 │ │ Rectangle::perimeter() │ │ h: 6.0 │ │ Shape::draw() (inherited)│ └──────────────┘ └──────────────────────────┘

The Virtual Destructor Rule Critical Trap

Trap: Deleting via Base Pointer

If you delete a derived object through a pointer to its base class, and the base class has a non-virtual destructor, the result is undefined behavior. Usually, this means only the base class destructor runs, silently leaking the derived class's memory and resources.

class Base { public: ~Base() { std::cout << "Base destroyed\n"; } // BUG: non-virtual! }; class Derived : public Base { int* data; public: Derived() { data = new int[100]; } ~Derived() { delete[] data; std::cout << "Derived destroyed\n"; } }; Base* ptr = new Derived(); delete ptr; // Output: "Base destroyed" // The 100 ints in Derived are LEAKED because ~Derived() was never called! // FIX: add 'virtual' to ~Base() {}

5.2   override, final, and Hiding vs Overriding Important

// override — compiler verifies this actually overrides a base virtual class Derived : public Base { void foo() const override; // ERROR if Base has no virtual foo() const void foo() override; // ERROR if only Base::foo() const exists (const mismatch) }; // final — prevents further overriding or inheritance class Sealed final : public Base { }; // Nobody can inherit from Sealed class Base2 { virtual void foo() final; // foo() can no longer be overridden }; // HIDING vs OVERRIDING — critical distinction class B { public: virtual void f() { std::cout << "B::f\n"; } void g() { std::cout << "B::g\n"; } // NOT virtual }; class D : public B { public: void f() override { std::cout << "D::f\n"; } // OVERRIDES B::f (virtual) void g() { std::cout << "D::g\n"; } // HIDES B::g (not virtual) }; B* p = new D(); p->f(); // "D::f" — virtual dispatch → correct derived version p->g(); // "B::g" — NOT virtual → calls base version! (hiding, not overriding)
Hiding is NOT Polymorphism

If a base function is not virtual, the derived class can define a function with the same name — but this hides the base function, not overrides it. Through a base pointer, the base function is always called, regardless of the actual object type. This is one of the most insidious bugs in C++ OOP. Always use virtual for functions meant to be overridden. Always use override in derived classes.

5.3   Casting in Class Hierarchies Core

// static_cast — compile-time cast, no runtime check Derived* d = static_cast<Derived*>(basePtr); // You assert it IS a Derived // If it's NOT actually a Derived, UB! No runtime protection. // dynamic_cast — runtime type check (RTTI) — safest for polymorphic hierarchies Shape* s = new Circle(5.0); Circle* c = dynamic_cast<Circle*>(s); if (c != nullptr) { std::cout << "It's a Circle!\n"; } else { std::cout << "Not a Circle\n"; } // dynamic_cast returns nullptr for pointer casts if type doesn't match // dynamic_cast throws std::bad_cast for reference casts if type doesn't match // typeid — get runtime type information #include <typeinfo> std::cout << typeid(*s).name() << "\n"; // Implementation-defined name if (typeid(*s) == typeid(Circle)) { ... }
Avoid dynamic_cast in Hot Code Paths

dynamic_cast requires RTTI and has runtime overhead. Frequent use often signals a design smell — you shouldn't need to know the concrete type at runtime if the virtual interface is designed well. For performance-critical code, use virtual dispatch instead. dynamic_cast is appropriate for introspection (e.g., debugging, serialization, visitor pattern).

5.4   Chapter Traps & Key Points

TrapWrongCorrect
No virtual destructordelete basePtr — derived destructor skipped, resource leakvirtual ~Base() {} — always in polymorphic base classes
Hiding instead of overridingDerived::g() without virtual in base — base called via pointerMark base function virtual; mark derived override
Object slicingBase b = derived; — polymorphism gone, derived data lostAlways use pointer or reference for polymorphic behavior
Pure virtual class instantiationShape s; // error: Shape has pure virtual membersAbstract classes cannot be instantiated; use pointers to derived
Virtual call in constructorVirtual functions in constructor call base version (not derived)Polymorphism is only active AFTER the object is fully constructed
↑ back to top
Chapter 06
Operator Overloading — Making Your Types Behave Naturally

6.1   What is Operator Overloading? Core

C++ lets you define what operators like +, -, ==, << mean for your custom types. This enables your classes to feel as natural as built-in types — v1 + v2 for vectors, a == b for objects, cout << obj for printing.

Rules of Operator Overloading

1. You can only overload existing operators — you can't invent new ones.
2. At least one operand must be a user-defined type — you can't change int + int.
3. You cannot change arity (unary stays unary, binary stays binary).
4. You cannot overload: :: . .* ?: sizeof typeid.
5. Keep semantics intuitive — + should add, not subtract.

6.2   Complete Operator Overloading Examples Heavy Reference

class Complex { public: double real, imag; Complex(double r = 0, double i = 0) : real(r), imag(i) {} // ── ARITHMETIC OPERATORS ────────────────────────────────────── // As member function: left operand = *this, right = param Complex operator+(const Complex& rhs) const { return Complex(real + rhs.real, imag + rhs.imag); } Complex operator-(const Complex& rhs) const { return Complex(real - rhs.real, imag - rhs.imag); } Complex operator*(const Complex& rhs) const { return Complex(real*rhs.real - imag*rhs.imag, real*rhs.imag + imag*rhs.real); } // Unary minus: -a Complex operator-() const { return Complex(-real, -imag); } // ── COMPOUND ASSIGNMENT ─────────────────────────────────────── Complex& operator+=(const Complex& rhs) { real += rhs.real; imag += rhs.imag; return *this; // Return *this for chaining: a += b += c } // ── COMPARISON OPERATORS ───────────────────────────────────── bool operator==(const Complex& rhs) const { return real == rhs.real && imag == rhs.imag; } bool operator!=(const Complex& rhs) const { return !(*this == rhs); // Implement != in terms of == } // ── SUBSCRIPT OPERATOR ─────────────────────────────────────── double& operator[](int idx) { if (idx == 0) return real; if (idx == 1) return imag; throw std::out_of_range("Complex index out of range"); } const double& operator[](int idx) const { // const version if (idx == 0) return real; if (idx == 1) return imag; throw std::out_of_range("Complex index out of range"); } // ── PREFIX/POSTFIX INCREMENT ───────────────────────────────── Complex& operator++() { // Prefix: ++c (no param) real++; return *this; } Complex operator++(int) { // Postfix: c++ (dummy int param) Complex old = *this; ++(*this); return old; // Return old value (before increment) } // ── CONVERSION OPERATOR ────────────────────────────────────── explicit operator double() const { // Complex → double (magnitude) return std::sqrt(real*real + imag*imag); } explicit operator bool() const { // Used in: if(complex_val) ... return real != 0 || imag != 0; } // ── STREAM OPERATORS (as friend, not member) ───────────────── friend std::ostream& operator<<(std::ostream& os, const Complex& c); friend std::istream& operator>>(std::istream& is, Complex& c); }; // output operator: must be non-member (left operand is ostream, not Complex) std::ostream& operator<<(std::ostream& os, const Complex& c) { os << c.real; if (c.imag >= 0) os << "+" << c.imag << "i"; else os << c.imag << "i"; return os; // Always return os to allow chaining: cout << a << b } std::istream& operator>>(std::istream& is, Complex& c) { is >> c.real >> c.imag; return is; } // Usage Complex a(1, 2), b(3, -1); Complex c = a + b; // (4, 1) std::cout << c << "\n"; // 4+1i c += a; // compound assignment bool eq = (a == b); // false double mag = static_cast<double>(a); // explicit conversion to magnitude

6.3   Spaceship Operator <=> (C++20) Modern C++

C++20 introduced the three-way comparison operator <=>, nicknamed the "spaceship operator". Define it once and the compiler auto-generates all six comparison operators (< <= > >= == !=).

#include <compare> class Version { int major, minor, patch; public: Version(int ma, int mi, int p) : major(ma), minor(mi), patch(p) {} // Define spaceship — compiler generates all 6 comparison operators auto operator<=>(const Version& o) const = default; // 'default' uses member-by-member lexicographic comparison }; Version v1(1, 2, 3), v2(1, 3, 0); v1 < v2; // true — auto-generated v1 >= v2; // false — auto-generated v1 == v2; // false — auto-generated

6.4   Chapter Traps & Key Points

TrapWrongCorrect
Not returning *this from +=void operator+=(…) — breaks a += b += cT& operator+=(…) { …; return *this; }
Postfix vs prefix returnPostfix returning reference *this (returns modified, not old)Postfix: save old, increment, return old value (by value)
operator== missing constbool operator==(const T& o) — not const, can't call on const objectsbool operator==(const T& o) const
Not returning ostream& from <<void operator<<(ostream, T) — breaks chainingostream& operator<<(ostream& os, const T& t) { …; return os; }
Implicit conversion operatoroperator int() — silently converts where not expectedexplicit operator int() — require static_cast
↑ back to top
Chapter 07
Abstract Classes & Interfaces — Contracts, not Implementations

7.1   Pure Virtual Functions & Abstract Classes Core

A pure virtual function is declared with = 0. It has no body in the base class — it's a contract saying "every derived class MUST provide this." A class with at least one pure virtual function is an abstract class — it cannot be instantiated directly.

class ISerializable { // Pure interface — all pure virtual public: virtual std::string serialize() const = 0; virtual void deserialize(const std::string& data) = 0; virtual ~ISerializable() {} // Virtual destructor even in interface }; class IDrawable { public: virtual void draw() const = 0; virtual void resize(double factor) = 0; virtual ~IDrawable() {} }; // Concrete class implementing multiple interfaces class Sprite : public ISerializable, public IDrawable { double x, y, scale; std::string texture; public: Sprite(double x, double y, std::string tex) : x(x), y(y), scale(1.0), texture(tex) {} std::string serialize() const override { return texture + ":" + std::to_string(x) + "," + std::to_string(y); } void deserialize(const std::string& data) override { // Parse data and set members... } void draw() const override { std::cout << "Drawing " << texture << " at (" << x << "," << y << ")\n"; } void resize(double factor) override { scale *= factor; } }; // ISerializable* s = new ISerializable(); ERROR — abstract class! ISerializable* s = new Sprite(0, 0, "hero.png"); // OK — pointer to interface std::cout << s->serialize() << "\n"; delete s;
Pure Virtual with Default Implementation

A pure virtual function can still have a body — it's optional. Derived classes must still override it, but they can call the base implementation via Base::func(). This is useful when you want to provide a default behavior that subclasses can choose to reuse.

7.2   Programming to Interfaces — The Power of Abstraction Design Principle

The principle: program to an interface, not an implementation. Write code against abstract base classes — then swap concrete implementations without changing calling code.

Build a plugin system: swap logging implementations without changing the logger call sites.

class ILogger { public: virtual void log(const std::string& msg) = 0; virtual void error(const std::string& msg) = 0; virtual ~ILogger() {} }; class ConsoleLogger : public ILogger { public: void log(const std::string& msg) override { std::cout << "[LOG] " << msg << "\n"; } void error(const std::string& msg) override { std::cerr << "[ERROR] " << msg << "\n"; } }; class FileLogger : public ILogger { std::ofstream file; public: FileLogger(const std::string& path) : file(path, std::ios::app) {} void log(const std::string& msg) override { file << "[LOG] " << msg << "\n"; } void error(const std::string& msg) override { file << "[ERROR] " << msg << "\n"; } }; // Application class depends only on the INTERFACE — not any concrete logger class Application { ILogger* logger; // Pointer to interface — we don't know or care which impl public: Application(ILogger* l) : logger(l) {} void run() { logger->log("Application started"); // ... business logic ... logger->log("Application done"); } }; // Switch logger with ZERO changes to Application code: ConsoleLogger cl; Application app1(&cl); app1.run(); // Logs to console FileLogger fl("app.log"); Application app2(&fl); app2.run(); // Logs to file — same code!

7.3   Chapter Traps & Key Points

TrapWrongCorrect
Instantiating abstract classIShape s; // compile errorIShape* s = new Circle(...); or use references
Forgetting to implement all pure virtualsDerived class missing one pure virtual override — still abstract!All pure virtuals must be overridden for class to be concrete
No virtual destructor in interfaceDeleting through interface pointer leaks derived resourcesAlways: virtual ~Interface() {} even in pure abstract classes
Storing objects in interface containersvector<ILogger> loggers — slicing!vector<ILogger*> or vector<unique_ptr<ILogger>>
↑ back to top
Chapter 08
Rule of Five & Resource Management — Owning Memory Correctly

8.1   Rule of Zero, Three, and Five Critical

Rule of Zero

If your class doesn't directly manage any resources (no raw pointers to owned memory, no file handles, no locks), don't define any of the five special functions. Let the compiler generate them — they'll be correct. Use RAII wrappers (std::vector, std::string, std::unique_ptr) to manage resources.

Rule of Three (pre-C++11)

If you need to define any of: destructor, copy constructor, or copy assignment operator — you almost certainly need to define all three. They're all about resource management and they all need to be consistent.

Rule of Five (C++11+)

With move semantics, add two more: move constructor and move assignment operator. If you define any of the five, consider whether you need all five. Defining one suppresses compiler generation of others.

First Principles Walkthrough

1. The Disaster (Shallow Copy): If your object has an int* data_ pointing to heap memory, the compiler's default copy just copies the memory address. Now two objects point to the same memory. When they both get destroyed, they both try to delete[] data_. The second one crashes your program (Double Free).
2. The Fix (Copy Constructor): To fix this, you write a Copy Constructor. Instead of copying the address, it asks the OS for new memory, then copies the actual numbers over (Deep Copy). Safe, but slow for huge data.
3. The Optimization (Move Constructor): Suppose you create a temporary 10GB Buffer, pass it to a function, and immediately destroy the temporary. Creating a Deep Copy of 10GB is a massive waste of time because the original is about to be thrown away anyway! A Move Constructor fixes this by simply stealing the pointer from the temporary object and setting the temporary's pointer to null. It's an instant, safe transfer of ownership.

class Buffer { // Owns heap memory — needs all five private: size_t size_; int* data_; public: // 1. Constructor Buffer(size_t n) : size_(n), data_(new int[n]()) { std::cout << "Construct " << n << "\n"; } // 2. Destructor ~Buffer() { delete[] data_; std::cout << "Destruct\n"; } // 3. Copy Constructor — deep copy Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) { std::copy(other.data_, other.data_ + size_, data_); std::cout << "Copy construct\n"; } // 4. Copy Assignment — deep copy Buffer& operator=(const Buffer& other) { if (this == &other) return *this; // Self-assignment check delete[] data_; // Free existing size_ = other.size_; data_ = new int[size_]; std::copy(other.data_, other.data_ + size_, data_); std::cout << "Copy assign\n"; return *this; } // 5. Move Constructor — steal the resource (no copy) Buffer(Buffer&& other) noexcept // noexcept: won't throw : size_(other.size_), data_(other.data_) { // Take other's pointer other.data_ = nullptr; // Leave other in valid empty state other.size_ = 0; std::cout << "Move construct\n"; } // 6. Move Assignment — steal the resource Buffer& operator=(Buffer&& other) noexcept { if (this == &other) return *this; delete[] data_; // Free current resource data_ = other.data_; // Steal other's resource size_ = other.size_; other.data_ = nullptr; // Leave other in valid state other.size_ = 0; std::cout << "Move assign\n"; return *this; } int& operator[](size_t i) { return data_[i]; } size_t size() const { return size_; } }; int main() { Buffer a(10); // Construct 10 Buffer b = a; // Copy construct Buffer c(20); // Construct 20 c = a; // Copy assign Buffer d = std::move(a); // Move construct — a is now empty (data_ = nullptr) c = std::move(b); // Move assign } // All destructors called — no leaks

8.2   Move Semantics — std::move and Rvalue References Modern C++

Move semantics enable transferring ownership of a resource instead of copying it. This is critical for performance with large objects like vectors, strings, and buffers.

// T&& — rvalue reference (binds to temporaries and std::move'd values) int&& rref = 5; // Rvalue reference to literal (contrived example) Buffer createBuffer() { Buffer temp(1000); return temp; // NRVO or move — no copy of 1000 ints! } Buffer b1 = createBuffer(); // Move constructor (from temporary return) Buffer b2 = std::move(b1); // Explicit move — b1 is now in valid empty state // b1.data_ is nullptr now — don't use b1! // std::move doesn't "move" anything by itself // It's a CAST to rvalue reference — tells the compiler the object can be moved from // The actual move happens in the move constructor/operator if one exists // Perfect forwarding with std::forward (advanced — see File 03) template<typename T> void wrapper(T&& arg) { process(std::forward<T>(arg)); // Forwards as lvalue or rvalue as appropriate }
noexcept on Move Operations

Always mark move constructor and move assignment noexcept. The STL (e.g., std::vector::push_back) will only use your move operations if they're noexcept — otherwise it falls back to copying for exception safety. A move that doesn't throw should always be declared noexcept.

8.3   Chapter Traps & Key Points

TrapWrongCorrect
Using moved-from objectBuffer b2 = move(b1); b1[0]; — b1.data_ is nullptr!After move, treat moved-from object as valid but unspecified state — don't use it
Missing noexcept on moveBuffer(Buffer&&) — vector resizes use copy, not move!Buffer(Buffer&&) noexcept — enables STL optimizations
Defining destructor suppresses moveDefine ~Foo() {} → compiler suppresses move operations → copies!Explicitly = default move operations: Foo(Foo&&) noexcept = default;
std::move on return valuereturn std::move(local); — prevents NRVO (actually slower!)return local; — compiler applies NRVO or implicit move
Rule of Zero violationClass has a raw pointer member, no copy/move defined → shallow copy bugsWrap in unique_ptr and follow Rule of Zero, or implement all five
↑ back to top
Chapter 09
OOP Design Principles — SOLID, Composition & Patterns

9.1   SOLID Principles Essential for Interviews

S — Single Responsibility Principle

A class should have exactly one reason to change. If a class handles business logic, database saving, and UI rendering, a change in the UI will force you to recompile and retest the database logic. By separating concerns, your code becomes modular, testable, and robust against cascading changes.

// BAD: User class does everything class User { std::string name; public: void updateName(std::string n) { name = n; } // Logic void saveToDB() { /* SQL code... */ } // DB logic }; // GOOD: Split responsibilities class User { std::string name; public: void updateName(std::string n) { name = n; } }; class UserRepository { public: void save(const User& u) { /* SQL code... */ } };

O — Open/Closed Principle

Software entities should be open for extension, but closed for modification. When requirements change, you should be able to add new functionality by writing new code, not by modifying existing, already-tested code. Modifying existing code risks breaking things that already work.

// BAD: Modifying existing logic for every new shape void drawShape(int type) { if (type == 1) drawCircle(); else if (type == 2) drawSquare(); // Adding a Triangle means modifying this function! } // GOOD: Open for extension (new classes), closed for modification (no if-else) class Shape { public: virtual void draw() = 0; }; class Circle : public Shape { public: void draw() override { /*...*/ } }; void drawShape(Shape& s) { s.draw(); } // Never needs to change!

L — Liskov Substitution Principle

If a program works with objects of a base class, it should also work with objects of any derived class without altering the correctness of the program. If a subclass overrides a method to do something completely unexpected, or throws an exception where the base class wouldn't, it violates LSP and creates bugs for callers relying on the base interface.

// BAD: Square breaks Rectangle's mathematical invariants class Rectangle { public: virtual void setWidth(int w) { width = w; } virtual void setHeight(int h) { height = h; } }; class Square : public Rectangle { public: // Forces both to change, violating what a user of Rectangle expects! void setWidth(int w) override { width = w; height = w; } }; // A function expecting a Rectangle might change width and expect the // height to stay the same. Square breaks this assumption! // GOOD: Don't force an IS-A relationship if invariants conflict. // A Square is mathematically a Rectangle, but behaviorally it is NOT. // Keep them separate or make them immutable.

I — Interface Segregation Principle

A client should never be forced to depend on methods it doesn't use. Fat, monolithic interfaces force implementing classes to create dummy or empty methods, cluttering the code and coupling unrelated systems. Break interfaces down into small, highly cohesive roles.

// BAD: A fat interface class IMachine { public: virtual void print() = 0; virtual void scan() = 0; virtual void fax() = 0; }; // A BasicPrinter has to implement scan() and fax() just to throw errors! // GOOD: Segregated interfaces class IPrinter { public: virtual void print() = 0; }; class IScanner { public: virtual void scan() = 0; }; class BasicPrinter : public IPrinter { /* only implements print */ }; class AdvancedCopier : public IPrinter, public IScanner { /* both */ };

D — Dependency Inversion Principle

High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions. This means you should inject dependencies via interfaces (like a database connection), rather than hardcoding concrete classes inside your business logic.

// BAD: Business logic is tightly coupled to a specific database tool class MySQLDatabase { public: void insert() { } }; class App { MySQLDatabase db; // Hard dependency! Can't test independent of MySQL. public: void save() { db.insert(); } }; // GOOD: App depends on an abstraction. class IDatabase { public: virtual void insert() = 0; }; class App { IDatabase* db; // Depends on abstraction. Can inject a MockDatabase! public: App(IDatabase* injected_db) : db(injected_db) {} void save() { db->insert(); } };

9.2   Composition over Inheritance Design Rule

Inheritance models IS-A. Composition models HAS-A. Prefer composition — it's more flexible, avoids tight coupling, and doesn't require deep inheritance hierarchies.

// BAD: Inheritance when IS-A is questionable class Engine { public: void start(); void stop(); }; class Car : public Engine { ... }; // Car IS-A Engine? Wrong! Car HAS-A Engine. // GOOD: Composition class Engine { public: void start() { std::cout << "Engine started\n"; } void stop() { std::cout << "Engine stopped\n"; } int getRPM() const; }; class GPS { public: std::pair<double,double> getLocation() const; void navigate(std::string destination); }; class Car { private: Engine engine; // Composition: Car HAS-A Engine GPS gps; // Car HAS-A GPS std::string model; public: void startCar() { engine.start(); // Delegate to Engine gps.navigate("Home"); // Delegate to GPS } int getEngineRPM() const { return engine.getRPM(); } }; // Can swap Engine implementation without touching Car! // Can have Car without GPS (optional member) easily. // Inheritance would have locked you in.

9.3   Essential Design Patterns in C++ Senior Level

Singleton — One Instance

class Config { private: Config() {} // Private constructor Config(const Config&) = delete; // No copy Config& operator=(const Config&) = delete; // No copy assign public: static Config& getInstance() { static Config instance; // Thread-safe in C++11 (magic static) return instance; } std::string get(const std::string& key) { /* ... */ } }; Config& cfg = Config::getInstance(); // Config cfg2 = Config::getInstance(); OK — it's a reference, not a copy

Factory Method — Create Without Knowing the Concrete Type

class Button { public: virtual void render() = 0; virtual ~Button() {} // Factory method — returns the right Button for the platform static std::unique_ptr<Button> create(std::string platform); }; class WindowsButton : public Button { public: void render() override { std::cout << "Windows button\n"; } }; class MacButton : public Button { public: void render() override { std::cout << "Mac button\n"; } }; std::unique_ptr<Button> Button::create(std::string platform) { if (platform == "windows") return std::make_unique<WindowsButton>(); if (platform == "mac") return std::make_unique<MacButton>(); throw std::invalid_argument("Unknown platform: " + platform); } auto btn = Button::create("windows"); btn->render(); // "Windows button" — caller doesn't know it's a WindowsButton

Observer — Decouple Event Emitters from Listeners

class IObserver { public: virtual void update(const std::string& event) = 0; virtual ~IObserver() {} }; class EventEmitter { std::vector<IObserver*> observers; public: void subscribe(IObserver* o) { observers.push_back(o); } void unsubscribe(IObserver* o) { observers.erase(std::remove(observers.begin(), observers.end(), o), observers.end()); } void emit(const std::string& event) { for (auto* o : observers) o->update(event); } };

9.4   Chapter Traps & Key Points

Anti-PatternProblemSolution
God ClassOne class does everything — 5000 linesSplit into focused classes by responsibility (SRP)
Deep inheritance hierarchy6+ levels deep — fragile, hard to understandPrefer composition; keep hierarchies max 2-3 levels
Shotgun surgeryOne change requires editing 20 filesEncapsulate change behind interfaces
Singleton overuseSingletons everywhere = hidden global state, untestableUse dependency injection instead; singletons for truly global resources only
Concrete dependencyClass A holds a FileLogger — can't unit test without filesClass A holds ILogger* — inject MockLogger in tests
↑ back to top
Chapter 10
Traps & Master Reference — The Complete OOP Cheatsheet

10.1   Compiler-Generated Special Functions Reference Cheatsheet

You DefineCompiler Generates Default:Compiler Suppresses:
NothingAll 6 (default ctor, copy ctor, copy=, move ctor, move=, dtor)
Any constructorDefault constructor
DestructorCopy ctor, copy=Move ctor, move= (deprecated)
Copy constructorCopy assignmentMove ctor, move=
Copy assignmentCopy constructorMove ctor, move=
Move constructorCopy ctor, copy= (deleted!)
Move assignmentCopy ctor, copy= (deleted!)
Use = default and = delete Explicitly

Rather than relying on implicit generation rules (which are complex), be explicit: Foo(const Foo&) = default; to explicitly request compiler generation, and Foo(const Foo&) = delete; to explicitly prohibit copying. This is clearer and immune to the suppress rules above.

10.2   Top OOP Bugs Reference Must Know

#BugCatch PhraseFix
1Object SlicingBase b = derived; — derived data lostAlways use Base* or Base& for polymorphism
2Missing virtual destructordelete base_ptr; → derived dtor never calledvirtual ~Base() {} in every polymorphic base
3Shallow copyTwo objects share same pointer → double freeDeep copy constructor, or use unique_ptr (Rule of Zero)
4Self-assignment not handleda = a; deletes data before copying from selfif (this == &other) return *this; at top of op=
5Virtual call in constructorDispatches to base, not derived (vtable not set up yet)Never rely on virtual dispatch in ctor/dtor
6Hiding, not overridingNon-virtual base function "overridden" — wrong version calledvirtual in base + override in derived
7Not calling base constructorBase member default-initialized or not initializedDerived() : Base(args) {} always chain explicitly
8No self-assignment in movemove(a, a) — frees memory before stealing itif (this == &other) return *this; in move op= too
9Using moved-from objectDereferences nullptr after moveTreat moved-from as unspecified; don't use it
10Diamond ambiguityTwo copies of base class, ambiguous member accessVirtual inheritance for shared base in diamond

10.3   OOP Quick-Reference Cards Reference

Virtual Keywords

virtual — enables dynamic dispatch
virtual f() = 0 — pure virtual
override — verify overrides virtual
final — prevents further override/inherit
virtual ~Base() — always in base

Access Quick Ref

public — anyone
protected — class + subclasses
private — class only
friend — named class/function
struct default: public
class default: private

Special Functions

Ctor, copy ctor, copy=
move ctor, move=, dtor
= default — request generation
= delete — prohibit
Prefer Rule of Zero

Cast Operators

static_cast<T>(x) — compile-time
dynamic_cast<T*>(p) — RTTI, safe
const_cast<T>(x) — add/remove const
reinterpret_cast<T>(x) — raw bits
Prefer static_cast; avoid C-cast (T)x

Key Idioms

RAII — resource in ctor, free in dtor
Pimpl — hide implementation in private impl
CRTP — Curiously Recurring Template Pattern
NVI — Non-Virtual Interface
Dependency Injection — pass dependencies in

SOLID in One Line Each

S: One reason to change
O: Extend without modifying
L: Substitutable subtype
I: Small focused interfaces
D: Depend on abstractions

↑ back to top