// Object Oriented Programming — Complete Guide

Master
Object
Oriented
Programming

Every concept from fundamentals to advanced topics — with real code, edge cases, and clear explanations. Built to make OOP actually click.

Fundamentals Classes & Objects Constructors Inheritance Polymorphism Advanced Topics
GS
Golap Saroar
CSE Undergraduate · RUET
SCROLL

Table of Contents

Six chapters covering every OOP concept from fundamentals to advanced topics. Click any card to jump directly to that chapter.

Fundamentals of OOP

The foundation that every other concept builds upon. Understanding these ideas first makes inheritance, polymorphism, and everything else feel natural.

🌍

Introduction to Object Oriented Programming

WHY IT EXISTS

Before OOP, programs were written procedurally — a long sequence of instructions, top to bottom. This worked for small programs but became a nightmare for large systems. You'd have functions and data scattered everywhere, no clear structure, and modifying one thing would break ten others.

OOP was invented to solve this by modeling programs the way the real world works — as a collection of objects that have properties and can do things. A Car object has a color, speed, and engine. It can accelerate() or brake(). You don't need to know the internal mechanics — you just interact with the interface.

📦

Object

Real instance with data & behavior

🏗️

Class

Blueprint/template for objects

🧬

Inheritance

Child reuses parent's code

🎭

Polymorphism

One interface, many behaviors

⚖️

Principles of Object-Oriented Design

THE FOUR PILLARS

OOP is built on four core principles that guide how you design and structure your code. Master these and everything else falls into place.

🔒

Encapsulation

Hide internal data. Expose only what's needed via public methods.

🧬

Inheritance

Build new classes on top of existing ones. Avoid code duplication.

🎭

Polymorphism

Same method name, different behavior depending on object type.

🎭

Abstraction

Show only what's relevant. Hide complex implementation details.

🚗 Real world analogy: A car dashboard is abstraction — you press the gas without knowing the combustion engine internals. The engine's internals being hidden is encapsulation. A sports car and a truck both being "drivable" is polymorphism. Both inheriting from "Vehicle" is inheritance.
⚠️ Common Mistake: Students often confuse class and object. A class is the cookie-cutter mold. An object is the actual cookie. You can make a thousand cookies (objects) from one mold (class). Memory is allocated only for the object, not the class definition.
📝 Exam Tips: Be ready to name and define all 4 pillars. Know the difference between class and object. Examiners frequently ask: "What is the difference between procedural and object-oriented programming?" — Procedural focuses on functions and their sequence; OOP focuses on objects and their interactions.
🔒

Encapsulation & Information Hiding

WHY: PROTECTS DATA INTEGRITY

Encapsulation means bundling data and methods together inside a class, and restricting direct access to the data. You protect your object's state by making data private and providing controlled public methods (getters/setters) to interact with it.

Information hiding is the principle behind this — external code should not need to know how something works internally, only what it can do.

C++
class Student {
    int age;        // private — cannot be accessed directly
    string name;

public:
    void setAge(int a) {
        if (a > 0 && a < 120) age = a;   // validated setter
        else cout << "Invalid age!\n";
    }
    int getAge() { return age; }  // controlled getter
};

int main() {
    Student s;
    // s.age = -5;   ❌ compile error — private!
    s.setAge(-5);   // safely rejected by our validation
    s.setAge(20);   // accepted ✓
}
⚡ Edge Case

Even though private members cannot be accessed by child classes, they still exist in the child's memory. They're inherited in terms of storage, just not accessible directly. Use protected if you want child classes to access them.

💊 Real world analogy: Encapsulation is like a medicine capsule. The drug (data) is sealed inside; you can't directly touch it. You interact via the pill — you swallow it (call the method) and it does its job. You don't mix the chemicals yourself.
⚠️ Common Mistake: Making all data public and skipping getters/setters "to save time." This destroys encapsulation — anyone can set age = -999 directly and corrupt your object's state. Always validate inside setters.
📝 Exam Tip: Differentiate encapsulation from abstraction: Encapsulation = HOW you hide (using access modifiers + methods). Abstraction = WHAT you hide (showing only essential features). Encapsulation implements abstraction.
🔗

Data Binding

HOW: LINKING CALLS TO CODE

Data binding (also called name binding) is the process of associating a function call to its actual implementation. When you write obj.method(), the program needs to figure out which specific function to execute — this is binding.

There are two types: Early (static) binding which happens at compile time, and Late (dynamic) binding which happens at runtime via virtual functions.

EARLY BINDING (Compile Time)

  • Resolved by compiler
  • Faster execution
  • No runtime overhead
  • Used for normal functions
  • Less flexible

LATE BINDING (Run Time)

  • Resolved at runtime
  • Slightly slower (vtable lookup)
  • Small overhead per call
  • Used for virtual functions
  • Enables polymorphism
📞 Real world analogy: Think of calling a restaurant. Early binding is like a speed dial to a fixed number — you know exactly who you'll reach before calling. Late binding is like calling directory assistance (104) — you say the name, and at call-time it resolves to the actual number.
⚠️ Common Mistake: Thinking that late binding is always "better." Early binding is faster with zero overhead. Use late binding (virtual) only when you genuinely need runtime polymorphism. Adding virtual to everything wastes memory (vtable) and slows execution.
📝 Exam Tip: Exams often ask which type of binding is used for: (a) function overloading → early, (b) function overriding with virtual → late, (c) templates → early. Memorize this table. Also: data binding and name binding mean the same thing.

Static and Dynamic Binding

THE CORE DIFFERENCE

Static binding: The compiler decides at compile time which function to call. This happens for regular (non-virtual) functions and overloaded functions.

Dynamic binding: The decision is made at runtime based on the actual object type, not the pointer type. This is how polymorphism works.

C++ — Static vs Dynamic
class Animal {
public:
    void sound()            { cout << "Animal\n"; }  // static binding
    virtual void speak()   { cout << "Animal\n"; }  // dynamic binding
};

class Dog : public Animal {
public:
    void sound()            { cout << "Dog\n"; }
    void speak() override  { cout << "Woof!\n"; }
};

int main() {
    Animal* ptr = new Dog();
    ptr->sound();   // prints "Animal" — static, decided at compile time
    ptr->speak();   // prints "Woof!"  — dynamic, decided at runtime ✓
}
⚠️ Critical insight: Without virtual, even if your pointer holds a Dog, calling sound() will call Animal's version. This silent bug is one of the most common mistakes in OOP. Always mark overridable methods as virtual.
🎬 Real world analogy: Static binding is like a pre-recorded phone tree ("Press 1 for sales"). The path is fixed at setup time. Dynamic binding is like a live switchboard operator who looks at your ID when you call and routes you to the right department based on who you actually are.
📝 Exam Tip: A classic exam trap: Animal* ptr = new Dog(); ptr->sound(); — if sound() is NOT virtual, it prints "Animal" (static binding). If it IS virtual, it prints "Dog" (dynamic binding). Draw this out to remember it. This single question appears on almost every OOP exam.
👉

The this Pointer

EVERY OBJECT KNOWS ITSELF

Inside every non-static member function, there is a hidden pointer called this that points to the object the function was called on. The compiler automatically passes it behind the scenes. You never declare it — it's always there.

Why it matters: Used to resolve name conflicts between member variables and parameters, to return the current object for method chaining, and to pass the current object to another function.

C++ — this Pointer
class Counter {
    int count;
public:
    Counter(int count) {
        this->count = count;  // 'this->count' = member, 'count' = parameter
    }

    // Return *this enables method chaining
    Counter& increment() { count++; return *this; }
    Counter& add(int n)   { count += n; return *this; }
    void show() { cout << count << endl; }
};

int main() {
    Counter c(0);
    c.increment().add(5).increment().show();  // 7 — method chaining!
}
🪞 Real world analogy: this is like the word "I" or "myself" in a sentence. When a person says "I will introduce myself", they're referring to themselves without needing to say their own name. Similarly, this lets an object refer to itself without knowing its own variable name from outside.
⚠️ Common Mistake: Static member functions have NO this pointer — they belong to the class, not any object. Trying to use this inside a static function is a compile error. Also: forgetting to return *this (dereferenced) when you want method chaining — returning this returns a pointer, not a reference.
📝 Exam Tip: Three key uses of this: (1) Resolve name conflict between member and parameter with same name, (2) Return current object by reference (return *this) for chaining, (3) Pass current object to another function. Exams often ask: "What is this?" — answer: an implicit pointer to the calling object, available in all non-static member functions.
🔐

The const Keyword in Classes

IMMUTABILITY & SAFETY

const in classes appears in three important places: const member functions (cannot modify the object), const objects (can only call const methods), and const data members (must be initialized in constructor initializer list). Together they enforce read-only guarantees at compile time.

C++ — const in Classes
class BankAccount {
    double balance;
    const int accountID;  // const data member — never changes
public:
    // const data member MUST be in initializer list
    BankAccount(int id, double b) : accountID(id), balance(b) {}

    // const member function — cannot modify any member
    double getBalance() const { return balance; }
    int    getID()      const { return accountID; }

    // non-const — modifies object
    void deposit(double amt) { balance += amt; }
};

int main() {
    const BankAccount acc(1001, 5000.0);  // const object
    cout << acc.getBalance();  // ✓ — getBalance() is const
    // acc.deposit(100);        ❌ — cannot call non-const on const object!
}

✓ CONST MEMBER FN RULES

  • Cannot modify data members
  • Can be called on const objects
  • Can be called on non-const objects
  • Use for all getters/read methods
  • Makes intent clear to compiler

✗ NON-CONST FN RULES

  • Can modify data members
  • Cannot be called on const objects
  • Can be called on non-const objects
  • Use for setters/mutating methods
  • Not safe for read-only access
📖 Real world analogy: A const member function is like a read-only mode on a document. You can open and read every page, but the system won't let you type anything. The document (object) is protected from changes. A const object is like a laminated certificate — it exists, you can look at it, but nobody can write on it.
⚠️ Common Mistake: Forgetting to mark getter functions as const. If you have a const object and your getX() is not marked const, the call will fail at compile time even though getters don't modify anything. Always add const after the closing parenthesis of any function that doesn't modify the object.
📝 Exam Tip: The const goes AFTER the parameter list: int getAge() const. A classic 5-mark question: "Why can't a const object call a non-const method?" — Answer: because a non-const method might modify the object, violating the const guarantee. Also: const data members must be initialized via the constructor initializer list — assignment inside the constructor body is NOT allowed.

Classes & Objects

The building blocks of every OOP program. A class is the blueprint — an object is the real thing built from it.

🏗️

Structure of a Class

ANATOMY OF A CLASS

A class is a user-defined data type that groups together data members (variables) and member functions (methods). It acts as a template — no memory is allocated until you create an object from it.

C++ — Full Class Anatomy
class Car {
    // ── Data Members (attributes) ──────────────
    int    speed;        // private by default
    string brand;

public:
    // ── Constructor ───────────────────────────
    Car(string b, int s) : brand(b), speed(s) {}

    // ── Member Functions (methods) ────────────
    void accelerate(int amt) { speed += amt; }
    void brake(int amt)       { speed -= amt; }

    // ── Accessor (getter) ─────────────────────
    int getSpeed() const { return speed; }

    // ── Destructor ────────────────────────────
    ~Car() { cout << brand << " scrapped.\n"; }
};

int main() {
    Car c1("Toyota", 0);    // object created on stack
    Car* c2 = new Car("BMW", 0); // object created on heap
    c1.accelerate(60);
    delete c2;               // must manually free heap objects
}
🏠 Real world analogy: A class is like an architectural blueprint for a house. The blueprint itself doesn't exist physically — it just describes walls, rooms, doors. Each house you build from it (each object) is real, takes up physical space, and can have different paint colors (different data values).
⚠️ Common Mistake: Forgetting that class members are private by default in C++ (unlike struct, where they are public by default). Also forgetting the semicolon ; after the closing brace of a class — this is a compile error unique to C++ class definitions.
📝 Exam Tip: Know the difference between class and struct in C++: the only technical difference is default access (class = private, struct = public). Conceptually, use struct for plain data, class for objects with behavior. Exams test this.
🚦

Access Modifiers

CONTROLS WHO SEES WHAT

Access modifiers define the visibility of class members. They are the primary tool for implementing encapsulation.

ModifierSame ClassChild ClassOutside ClassUse Case
privateInternal data, balance, password
protectedData child classes need to access
publicInterface methods, getters/setters
Rule of thumb: Start with everything private. Only make things public when external code genuinely needs access. Use protected specifically when child classes need it.
🏦 Real world analogy: Think of a bank. private = the bank vault (only bank staff can access). protected = the back office (head office branches can access). public = the ATM machine (anyone can use the interface).
📝 Exam Tip: The most tested question: "What is the default access modifier in a class?" → private. In a structpublic. Also memorize: protected members ARE accessible in derived classes (unlike private), but NOT outside the class hierarchy. This distinction is crucial.
🪆

Nested Classes

CLASS INSIDE A CLASS

A nested class is a class defined inside another class. It's used when a class is only relevant in the context of its outer class. The nested class is a member of the outer class but has its own scope.

C++
class University {
public:
    string uniName;

    class Department {   // nested class
    public:
        string deptName;
        void show() {
            cout << "Dept: " << deptName << endl;
        }
    };
};

int main() {
    University::Department d;   // access nested class
    d.deptName = "CSE";
    d.show();
}
⚡ Edge Case

A nested class does NOT automatically have access to the outer class's private members. It's just scoped inside, but it follows normal access rules.

🇷🇺 Real world analogy: A nested class is like a department within a university. The "Department" class makes no sense outside of "University". Just like the HR Department doesn't exist independently — it's always part of some company. This is why you scope it inside.
📝 Exam Tip: To create an object of a nested class outside its enclosing class, you must use the scope resolution operator: OuterClass::InnerClass obj;. Forgetting the outer class name is a common exam mistake. Also: nested classes don't automatically get access to private members of the outer class.
👻

Abstract Classes

FORCE CHILD CLASSES TO IMPLEMENT

An abstract class is a class that cannot be instantiated — you can never create an object directly from it. It's designed purely to be inherited. It contains at least one pure virtual function (marked with = 0), which child classes must implement.

Why? It defines a contract. Any class that inherits from Shape must have a draw() method — you enforce this without dictating how it works.

C++
class Shape {               // abstract class
public:
    virtual void draw() = 0;   // pure virtual — MUST override
    virtual double area() = 0;
    virtual ~Shape() {}
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    void draw() override   { cout << "Drawing circle\n"; }
    double area() override { return 3.14 * radius * radius; }
};

// Shape s;  ❌ Cannot instantiate abstract class!
Circle c(5);  // ✓ Circle is concrete — all pure virtuals implemented
📋 Real world analogy: An abstract class is like a job description. "Employee" as an abstract concept says: everyone must have a work() method. But you can't hire just an "Employee" — you hire a "Software Engineer" or "Designer" (concrete classes) who fulfills that job description.
⚠️ Common Mistake: A class is only abstract if it has at least one pure virtual function (= 0). A class with only regular virtual functions is NOT abstract. Also: if a child class doesn't implement ALL pure virtual functions of its parent, it remains abstract too and cannot be instantiated.
📝 Exam Tip: (1) "Can you create an object of an abstract class?" → NO. (2) "What makes a class abstract?" → At least one pure virtual function with = 0. Always include a virtual destructor (virtual ~Shape(){}) in abstract classes — forgetting it causes undefined behavior when deleting derived objects.
📚

Arrays of Objects

MANAGING MULTIPLE OBJECTS

You can create arrays where each element is an object. This is how you manage collections — like 30 students, 15 bank accounts, or 100 game enemies.

C++
class Student {
public:
    string name;
    int marks;
    void show() { cout << name << ": " << marks << endl; }
};

int main() {
    Student batch[3];          // array of 3 Student objects
    batch[0].name = "Rahim";  batch[0].marks = 85;
    batch[1].name = "Karim";  batch[1].marks = 90;

    for (int i = 0; i < 2; i++)
        batch[i].show();

    // Dynamic array on heap
    Student* heap = new Student[50];
    delete[] heap;  // use delete[] for arrays!
}
Critical: When deleting a heap-allocated array of objects, always use delete[] not delete. Using delete on an array only destroys the first element's destructor — the rest leak memory.
🏫 Real world analogy: An array of objects is like a classroom register. Each row is a Student object with their name and marks. You interact with individual students using index numbers (batch[0], batch[1]). A dynamic array is like a Google Sheets row that can grow as students enroll.
📝 Exam Tip: The most critical rule: delete[] for arrays, delete for single objects. Using the wrong one is undefined behavior. Also: stack-allocated arrays (e.g., Student batch[3]) are automatically destroyed when they go out of scope — you DON'T call delete on them.
🎯

Pointer to Objects

ENABLES POLYMORPHISM

Pointers to objects are what make polymorphism work. A base class pointer can point to any derived class object, and virtual function calls through it resolve to the correct type at runtime.

C++ — . vs ->
Car c1("Toyota", 0);
Car* ptr = &c1;

c1.accelerate(50);   // dot notation — direct object
ptr->accelerate(50); // arrow notation — pointer to object
(*ptr).accelerate(50); // same as above, dereferenced

// Heap object — MUST use delete
Car* heap = new Car("BMW", 0);
heap->accelerate(100);
delete heap;   // always!
🚀 Real world analogy: A pointer to an object is like a TV remote. The remote (pointer) is not the TV (object) — it's just a small device that points to and controls the TV. You can have multiple remotes pointing to the same TV, or swap which TV a remote controls without moving the TVs themselves.
📝 Exam Tip: Two operators to access members: dot (.) for direct objects, arrow (->) for pointers. ptr->method() is exactly equivalent to (*ptr).method(). For polymorphism to work, you MUST use a pointer or reference — direct objects cause static binding regardless of virtual keyword.
🤝

Friend Function

CONTROLLED PRIVACY BREACH

A friend function is a function that is not a member of a class but has access to its private and protected members. You declare it inside the class with the friend keyword. This is useful when two classes need to work closely together.

C++
class Box {
    double volume;
public:
    Box(double v) : volume(v) {}
    friend void printVolume(Box b); // declaration inside class
};

// defined outside — NOT a member function
void printVolume(Box b) {
    cout << b.volume << endl;  // can access private! ✓
}
⚠️ Use sparingly. Friend functions break encapsulation. Only use when the alternative (getters) would make the design worse — such as overloading the << operator for printing.
⚡ Edge Case

Friendship is NOT inherited. If class B is a friend of A, B's child class is NOT automatically a friend of A. Friendship is also not mutual — if A declares B as friend, B cannot access A's privates unless B also declares A as friend.

🔑 Real world analogy: A friend function is like a trusted locksmith who has a spare key to your house. They're not family (not a member), but you've explicitly given them access to your private space. You wouldn't give keys to everyone — only people with a specific, justified need.
📝 Exam Tip: Three critical properties of friendship in C++: (1) Not inherited, (2) Not transitive, (3) Not mutual. The declaration goes INSIDE the class (with friend keyword), but the definition is OUTSIDE. A friend function is NOT a member — calling it does NOT use the -> or . syntax on an object.
📌

Static Variable and Function

SHARED ACROSS ALL OBJECTS

A static member belongs to the class itself, not to any individual object. All objects share the same static variable. Use it for things like counting how many objects have been created.

C++
class Account {
public:
    static int totalAccounts;   // shared by ALL objects
    Account() { totalAccounts++; }
    ~Account() { totalAccounts--; }
    static int getCount() { return totalAccounts; }  // static function
};

int Account::totalAccounts = 0;  // must define outside class

int main() {
    Account a1, a2, a3;
    cout << Account::getCount();  // 3 — called on class, not object
}
Static function rule: A static member function can only access other static members. It has no this pointer since it doesn't belong to any specific object.
🏛️ Real world analogy: A static variable is like the number of seats in a theater (shared for all performances), while each ticket's row/seat number is an instance variable (unique to each customer). The seat count belongs to the theater itself, not to any individual ticket.
⚠️ Common Mistake: Forgetting to define the static member outside the class: int Account::totalAccounts = 0;. Declaring it inside is only a declaration — the definition must happen outside. Without it, you'll get a linker error, not a compile error.
📝 Exam Tip: Static members can be accessed two ways: ClassName::member (preferred) or obj.member. Static functions have NO this pointer — they cannot access non-static members directly. This is a frequent exam question.

Constructors & Destructors

Control exactly what happens when objects come to life and when they're destroyed. These are among the most nuanced parts of C++ OOP.

🔨

Default Constructor

ZERO-ARGUMENT INITIALIZATION

A default constructor takes no parameters. If you define no constructor at all, the compiler generates one automatically. But as soon as you define any constructor, the auto-generated default disappears.

C++
class Player {
    string name;
    int    health;
public:
    Player() {              // default constructor
        name   = "Unknown";
        health = 100;
    }
    Player(string n, int h) : name(n), health(h) {}
};

int main() {
    Player p1;              // calls default constructor
    Player p2[10];          // default constructor called 10 times
    Player p3("Rahim", 200); // parameterized constructor
}
⚡ Edge Case

If you define ONLY a parameterized constructor, you lose the default. Then Player p1; will fail to compile. You must explicitly write the default constructor back if you need both.

🏭 Real world analogy: A default constructor is like a car that rolls off the factory line with default settings — black color, manual transmission, standard engine. You can then customize it. If no customer specs are given, the factory defaults apply.
📝 Exam Tip: When is the default constructor called? (1) Player p1;, (2) Player arr[10]; — called 10 times, (3) When a derived class constructor doesn't explicitly call a parent constructor. If you define a parameterized constructor and forget to also define a default one, array declarations and default initialization will fail to compile.
⚙️

Parameterized Constructor

INITIALIZE WITH CUSTOM VALUES

A parameterized constructor accepts arguments to initialize an object with specific values at creation. It's the most commonly used constructor type. The initializer list (using : before the body) is the preferred and more efficient way to initialize members — it directly constructs them instead of default-constructing then assigning.

C++ — Parameterized + Initializer List
class Rectangle {
    double width, height;
public:
    // Method 1: assignment inside body (OK but less efficient)
    Rectangle(double w, double h) {
        width = w; height = h;
    }

    // Method 2: Initializer List (PREFERRED — more efficient)
    Rectangle(double w, double h) : width(w), height(h) {}

    double area()     const { return width * height; }
    double perimeter() const { return 2 * (width + height); }
};

int main() {
    Rectangle r1(4.0, 6.0);    // calls parameterized constructor
    Rectangle r2(10.0, 3.5);
    cout << r1.area();           // 24.0
    cout << r2.perimeter();     // 27.0
}
Types of Constructors — Complete Summary:
1. Default — no parameters, auto-generated if none defined
2. Parameterized — takes arguments, custom initialization
3. Copy — creates from existing object (const T&)
4. Dynamic — allocates heap memory inside constructor
5. Delegating (C++11) — one constructor calls another
🏠 Real world analogy: A default constructor is like ordering a pizza with default toppings. A parameterized constructor is like customizing your order — you specify size, crust, toppings at the time of ordering. The pizza (object) is made exactly to your specifications the moment you order (create) it.
⚠️ Common Mistake: Confusing the initializer list with assignment. : width(w) calls the constructor of width directly. width = w inside the body first default-constructs width, then assigns — two steps instead of one. For const members and reference members, only the initializer list works — body assignment will NOT compile.
📝 Exam Tip: Exams ask "List the types of constructors in C++." Know all 4 (default, parameterized, copy, dynamic). Initializer list order matches the ORDER members are declared in the class — not the order you write in the list. Initializing out-of-declaration order can cause subtle bugs. This is a tricky but testable point.
📋

Copy Constructor

CREATES FROM AN EXISTING OBJECT

A copy constructor creates a new object as a copy of an existing one. It takes a const reference to an object of the same class. The compiler generates one by default, but you must write your own when your class manages dynamic memory (otherwise you get a shallow copy — a dangerous bug).

C++
class Player {
    string name;
    int    health;
public:
    Player(string n, int h) : name(n), health(h) {}

    // Copy constructor
    Player(const Player& other) {
        name   = other.name;
        health = other.health;
        cout << "Copied: " << name << endl;
    }
};

int main() {
    Player p1("Rahim", 100);
    Player p2 = p1;    // copy constructor called
    Player p3(p1);     // copy constructor called (same thing)
    someFunction(p1);  // copy constructor called (pass by value)
}
📷 Real world analogy: A copy constructor is like a photocopier. You put in the original document (existing object) and get out an identical but separate copy. Changes to the copy don't affect the original — they're independent documents.
⚠️ Common Mistake: The copy constructor must take its argument by reference (const Player&), NOT by value. If it took by value, copying the argument would call the copy constructor again, leading to infinite recursion and a stack overflow crash.
📝 Exam Tip: 3 situations where copy constructor is automatically called: (1) Player p2 = p1;, (2) Player p2(p1);, (3) Passing object by value to a function. Assignment after creation (p2 = p1;) calls the assignment operator, NOT the copy constructor.
🔄

Copy Assignment Operator

ASSIGNING ONE OBJECT TO ANOTHER

The copy assignment operator (operator=) is called when you assign one existing object to another existing object. This is the third member of the Rule of Three. Without a custom one, C++ does a shallow copy — dangerous when your class manages dynamic memory.

Critical difference from copy constructor: the copy constructor creates a new object. The assignment operator works on an object that already exists — so it must first free the old memory before copying new data.

C++ — Copy Assignment Operator
class Buffer {
    int* data;
    int  size;
public:
    Buffer(int n) : size(n) { data = new int[n]{}; }

    // Copy Assignment Operator
    Buffer& operator=(const Buffer& other) {
        if (this == &other) return *this;  // Step 1: self-assignment check!
        delete[] data;                    // Step 2: free old memory
        size = other.size;
        data = new int[size];            // Step 3: allocate new memory
        for(int i=0;i<size;i++) data[i]=other.data[i]; // Step 4: copy data
        return *this;                     // Step 5: return self for chaining
    }

    ~Buffer() { delete[] data; }
};

int main() {
    Buffer a(5), b(10);
    a = b;           // calls operator= (NOT copy constructor — both exist already)
    Buffer c = b;    // calls COPY CONSTRUCTOR (c is being created)
}

COPY CONSTRUCTOR

  • Called when creating new object
  • Buffer c = b; or Buffer c(b);
  • No existing memory to free
  • No self-assignment check needed
  • Signature: T(const T&)

ASSIGNMENT OPERATOR

  • Called on existing objects
  • a = b; (both already exist)
  • Must free old memory first
  • Self-assignment check required
  • Signature: T& operator=(const T&)
⚠️ Self-assignment trap: Without the if (this == &other) check, doing a = a would delete a's memory first, then try to copy from it — reading freed memory and crashing. Always check for self-assignment first.
📋 Real world analogy: Copy constructor = printing a blank form and filling it in fresh. Copy assignment operator = taking a form that already has writing on it, erasing it completely, and then writing new content copied from another form. The erasing step (delete old memory) is unique to assignment.
📝 Exam Tip — Rule of Three: If your class needs a custom destructor (because it manages heap memory), it also needs a custom copy constructor AND custom copy assignment operator. Define all three or define none. C++11 extends this to the Rule of Five (adding move constructor and move assignment) but Rule of Three is what university exams focus on.

Dynamic Constructor

ALLOCATES MEMORY AT RUNTIME

A dynamic constructor allocates memory on the heap using new inside the constructor. This is used when the size of data isn't known at compile time — like a string of variable length or a dynamic array.

C++
class Buffer {
    int* data;
    int  size;
public:
    // Dynamic constructor — size decided at runtime
    Buffer(int n) {
        size = n;
        data = new int[n];   // heap allocation in constructor
        cout << "Allocated " << n << " ints\n";
    }

    ~Buffer() {
        delete[] data;          // ALWAYS free in destructor
        cout << "Freed buffer\n";
    }
};

int main() {
    int n;
    cout << "Size? "; cin >> n;
    Buffer buf(n);   // size decided while program runs
}
🏗️ Real world analogy: A dynamic constructor is like ordering a custom-sized container. You don't know the size when you design the factory (compile time) — you only know it when the customer places the order (runtime). The container is then built to exact specifications at that moment.
⚠️ Critical Rule: Every new in a constructor MUST have a corresponding delete in the destructor. If you allocate dynamically in the constructor but forget to free it in the destructor, you have a memory leak. For arrays: new[] must pair with delete[].
🔍

Deep Copy vs Shallow Copy

MOST CRITICAL MEMORY CONCEPT

This is one of the most important and dangerous concepts in C++. When your class has a pointer member, the default copy constructor does a shallow copy — it copies the pointer address, not the actual data. Both objects then point to the same memory. When one is destroyed, the other has a dangling pointer. This causes crashes.

A deep copy allocates new memory and copies the actual data, so both objects are fully independent.

❌ SHALLOW COPY (default)

  • Copies pointer address
  • Both objects share memory
  • First destructor frees memory
  • Second destructor crashes!
  • Double free error

✓ DEEP COPY (manual)

  • Allocates new memory
  • Copies actual data values
  • Objects are fully independent
  • Each destructor frees its own
  • Safe ✓
C++ — Deep Copy Constructor
class Buffer {
    int* data;
    int  size;
public:
    Buffer(int n) : size(n) { data = new int[n]{}; }

    // DEEP copy constructor
    Buffer(const Buffer& other) {
        size = other.size;
        data = new int[size];  // NEW allocation
        for (int i = 0; i < size; i++)
            data[i] = other.data[i];  // copy VALUES not address
    }
    ~Buffer() { delete[] data; }
};
Rule of Three: If you define any one of (1) destructor, (2) copy constructor, (3) copy assignment operator, you almost certainly need to define all three. This is a C++ best practice to avoid memory bugs.
📦 Real world analogy: Shallow copy is like sharing a Google Doc link — both people see the same document, and if one edits it, the other sees the change too. Deep copy is like downloading the doc as a PDF — now you have your own separate copy, changes to one don't affect the other.
📝 Exam Tip: The shallow copy problem only occurs when your class contains a pointer member. If your class only has primitive types (int, double) or objects with proper copy constructors (string, vector), the default copy is safe. The exam often shows a class with int* and asks why it crashes — the answer is shallow copy / double free.
🔗

Constructor for Derived Class & Order of Execution

PARENT ALWAYS FIRST

When a derived class object is created, both constructors run — the parent's first, then the child's. When destroyed, the order reverses: child destructor first, then parent's. You pass arguments to the parent constructor via the initializer list.

C++ — Constructor Chain
class A {
public:
    A()  { cout << "A constructed\n"; }
    ~A() { cout << "A destroyed\n"; }
};
class B : public A {
public:
    B()  { cout << "B constructed\n"; }
    ~B() { cout << "B destroyed\n"; }
};
class C : public B {
public:
    C()  { cout << "C constructed\n"; }
    ~C() { cout << "C destroyed\n"; }
};

int main() {
    C obj;
}
// Output:
// A constructed  ← parent first
// B constructed
// C constructed  ← child last
// C destroyed    ← reversed on destruction
// B destroyed
// A destroyed    ← parent last
🏢 Real world analogy: When you join a company (derived class created), you first get company-wide onboarding (parent constructor), then your department training (child constructor). When you leave (destructor), your department clears your desk first (child destructor), then company-wide revokes your ID (parent destructor).
📝 Exam Tip: Constructor order = parent first, child last. Destructor order = child first, parent last (LIFO). For a 3-level chain A→B→C: prints A constructed → B constructed → C constructed → C destroyed → B destroyed → A destroyed. This exact question appears on most OOP exams.
🧹

Destructor

CLEANUP WHEN OBJECT DIES

A destructor runs automatically when an object goes out of scope or is explicitly deleted. Its job is to release any resources the object holds — mainly heap memory, open files, or network connections. Syntax: tilde followed by the class name, no parameters, no return type.

Always make base class destructors virtual. If a base class destructor is not virtual, deleting a derived object through a base pointer only calls the base destructor — the derived destructor is silently skipped, causing memory leaks.
C++ — Virtual Destructor
class Base {
public:
    virtual ~Base() { cout << "Base destroyed\n"; }
};
class Derived : public Base {
    int* data = new int[100];
public:
    ~Derived() { delete[] data; cout << "Derived destroyed\n"; }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // virtual ~Base() ensures Derived's destructor also runs ✓
}
🏨 Real world analogy: A destructor is like hotel checkout. When you leave (object destroyed), you hand back the room key (free memory), return borrowed towels (close file handles), and settle the bill (release connections). If you forget any of these, the hotel (system) leaks resources.
📝 Exam Tip: If your class has any virtual functions, it MUST have a virtual destructor. Without it, Base* ptr = new Derived(); delete ptr; only calls Base's destructor, silently leaking Derived's memory. This undefined behavior is a classic exam trap worth memorizing.

Inheritance

One of OOP's superpowers — build new classes on top of existing ones without rewriting code. Model real-world IS-A relationships.

🧬

Single vs Multiple Inheritance

HOW MANY PARENTS?

Single inheritance: A class inherits from exactly one parent. Simple and clean — the most common form.

Multiple inheritance: A class inherits from two or more parents. Powerful but can cause the Diamond Problem (see below). Java avoids it entirely; C++ supports it.

C++
// Single Inheritance
class Animal { public: void eat() {} };
class Dog : public Animal {};   // one parent

// Multiple Inheritance
class Flyable  { public: void fly() {} };
class Swimmable{ public: void swim(){} };
class Duck : public Flyable, public Swimmable {};  // two parents

int main() {
    Duck d;
    d.fly();   // from Flyable
    d.swim();  // from Swimmable
}
⚡ Diamond Problem

If both parent classes themselves inherit from the same grandparent, the derived class gets two copies of the grandparent's data. This ambiguity causes errors. Solution: virtual inheritance.

🦆 Real world analogy: Single inheritance: a Duck IS-AN Animal. Multiple inheritance: a FlyingFish IS-A Fish AND IS-A Swimmer. Both are valid relationships. The Diamond problem is like a Teaching Assistant who IS-A Student AND IS-AN Employee, and both Student/Employee inherit from Person — how many names do they have?
📝 Exam Tip: Java disallows multiple inheritance with classes to avoid the Diamond Problem — instead using interfaces. C++ allows it but requires virtual inheritance to resolve the ambiguity. Know which languages support multiple inheritance: C++ yes, Java no (but multiple interface implementation).
🏛️

Multilevel & Hierarchical Inheritance

CHAINS AND TREES OF CLASSES

Multilevel inheritance: A chain where B inherits from A, and C inherits from B. C gets the combined members of both A and B. This models a grandparent → parent → child relationship.

Hierarchical inheritance: Multiple classes inherit from a single parent. All share the parent's interface but implement their own behavior. This is the most common real-world pattern — one base class, many specialized derivatives.

C++ — Multilevel vs Hierarchical
// ── MULTILEVEL: A → B → C chain ───────────────
class Animal  { public: void breathe() { cout << "breathing
"; } };
class Mammal  : public Animal  { public: void nurse() {} };
class Dog     : public Mammal  { public: void bark()  {} };
// Dog has: breathe() from Animal + nurse() from Mammal + bark()

// ── HIERARCHICAL: one parent, many children ───
class Shape   { public: virtual double area() = 0; };
class Circle  : public Shape { double r; public: double area() override { return 3.14*r*r; } };
class Square  : public Shape { double s; public: double area() override { return s*s; } };
class Triangle: public Shape { double b,h; public: double area() override { return 0.5*b*h; } };
// Shape → Circle, Shape → Square, Shape → Triangle (fan out)
⛓️

Multilevel

A → B → C chain. Each level adds specialization. Dog IS-A Mammal IS-AN Animal.

🌳

Hierarchical

One parent, many children. All shapes share area() interface but implement differently.

🔀

Hybrid

Combination of multiple types. Can mix multilevel and multiple inheritance.

🌍 Real world analogy: Multilevel is like the taxonomy of life: Living Thing → Animal → Vertebrate → Mammal → Dog → Golden Retriever. Each level inherits all traits above and adds its own. Hierarchical is like a company org chart: one CEO, many department heads — all reporting to the same parent but each department is independent.
📝 Exam Tip: Five types of inheritance you must know: (1) Single, (2) Multiple, (3) Multilevel, (4) Hierarchical, (5) Hybrid. Exams frequently ask to "draw a diagram" or "write code" for each type. Multilevel constructor order still follows parent-first rule: Animal → Mammal → Dog when Dog is created. All three constructors run in order.
🎛️

Modes of Inheritance

CONTROLS VISIBILITY AFTER INHERITING

The mode keyword (public, protected, private) after the colon controls how the parent's members are seen in the child.

Parent Memberpublic inheritanceprotected inheritanceprivate inheritance
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privateinaccessibleinaccessibleinaccessible
In practice: You almost always use public inheritance. It preserves the IS-A relationship. Private/protected inheritance represents "implemented-in-terms-of" and is rarely used.
🚦 Real world analogy: Think of inheritance modes as security clearance at a building. Public inheritance: child gets the same badge as the parent — guests can visit either. Protected inheritance: the child gets internal access, but guests (outside code) cannot reach the parent's sections through the child. Private inheritance: the child completely hides that they even use the parent's facilities.
📝 Exam Tip: The most critical table in inheritance — when mode is public: parent public stays public, protected stays protected, private stays inaccessible. When mode is private: BOTH public and protected become private in the child. The table with all 9 combinations is almost guaranteed to appear on your exam.
💎

Virtual Inheritance

SOLVES THE DIAMOND PROBLEM

When two parent classes both inherit from the same grandparent, and a class inherits from both parents, the grandparent's data gets duplicated. Virtual inheritance ensures only one shared copy of the grandparent exists.

C++ — Diamond Problem & Fix
class Person  { public: string name; };

// virtual keyword here prevents duplication
class Student : virtual public Person {};
class Employee: virtual public Person {};

class TA : public Student, public Employee {};
// Now TA has ONE copy of Person::name, not two ✓
💎 Real world analogy: A Teaching Assistant is both a Student AND an Employee — both inherit from Person. Without virtual inheritance, the TA has TWO Person sub-objects (two names, two IDs — absurd!). With virtual inheritance, only ONE shared Person exists, just like a real person has only one identity no matter how many roles they hold.
📝 Exam Tip: The virtual keyword goes on the INTERMEDIATE classes: class Student : virtual public Person and class Employee : virtual public Person. NOT on the final class (TA). This is a very common mistake — putting virtual on the wrong class is the #1 error students make with diamond problems.
📜

Interface

PURE CONTRACT — NO IMPLEMENTATION

An interface is a class with only pure virtual functions — no data, no implementation. It defines a contract: "any class that implements me MUST have these methods." In C++ it's simulated with abstract classes. In Java it's a formal keyword.

C++ — Interface pattern
class IPrintable {            // interface — all pure virtual
public:
    virtual void print() = 0;
    virtual void preview() = 0;
    virtual ~IPrintable() {}
};

class Document : public IPrintable {
public:
    void print()   override { cout << "Printing doc\n"; }
    void preview() override { cout << "Previewing doc\n"; }
};
🔀

Upcasting & Downcasting

NAVIGATING THE CLASS HIERARCHY

Upcasting: Converting a derived class pointer/reference to a base class pointer/reference. Always safe and done implicitly. This is what enables polymorphism — storing a Dog* in an Animal*.

Downcasting: Converting a base class pointer back to a derived class pointer. Potentially unsafe — the base pointer might not actually point to that derived type. Use dynamic_cast for safe downcasting with runtime type checking.

C++ — Upcasting & Downcasting
class Animal {
public:
    virtual void speak() { cout << "Animal
"; }
    virtual ~Animal() {}
};
class Dog : public Animal {
public:
    void speak()   override { cout << "Woof!
"; }
    void fetch()            { cout << "Fetching!
"; }
};

int main() {
    Dog* dog = new Dog();

    // UPCASTING — implicit, always safe
    Animal* aPtr = dog;         // Dog* → Animal* ✓
    aPtr->speak();              // "Woof!" — virtual dispatch works
    // aPtr->fetch();           ❌ Animal* can't see fetch()

    // DOWNCASTING — requires explicit cast
    Dog* dPtr = dynamic_cast<Dog*>(aPtr);  // safe cast with check
    if (dPtr) {
        dPtr->fetch();           // ✓ safely recovered Dog* access
    }

    // UNSAFE downcast — don't do this
    Dog* risky = static_cast<Dog*>(aPtr); // no runtime check — dangerous!
    delete dog;
}

✓ UPCASTING (SAFE)

  • Derived → Base pointer
  • Implicit (no cast needed)
  • Always succeeds
  • Loses derived-specific methods
  • Foundation of polymorphism

⚠️ DOWNCASTING (RISKY)

  • Base → Derived pointer
  • Explicit cast required
  • May fail at runtime
  • Use dynamic_cast for safety
  • Returns nullptr if type mismatch
⚡ dynamic_cast vs static_cast

dynamic_cast checks the actual runtime type and returns nullptr if the cast is invalid — safe but has a small runtime cost. static_cast blindly trusts you — no runtime check, no safety, undefined behavior if wrong. For downcasting, always prefer dynamic_cast and check the result.

🎭 Real world analogy: Upcasting is like calling a Golden Retriever just a "dog" — you lose the breed-specific details but the core identity holds. Downcasting is like looking at a dog and claiming "that's specifically a Golden Retriever" — you might be right, but if it's actually a Labrador, you'd be wrong and could act incorrectly. dynamic_cast is like checking the ID tag first.
📝 Exam Tip: dynamic_cast only works on classes with at least one virtual function (polymorphic classes). Using it on a non-polymorphic class causes a compile error. For pointers: returns nullptr on failure. For references: throws std::bad_cast on failure. Exams test: "When does dynamic_cast return nullptr?" — when the actual type of the object is not the target type.

Polymorphism

One name, many forms. The ability to treat different object types uniformly while they each behave differently. This is where OOP becomes truly powerful.

🔧

Operator and Function Overloading

COMPILE-TIME POLYMORPHISM

Function overloading: Multiple functions with the same name but different parameter lists. The compiler picks the right one based on the arguments — decided at compile time.

Operator overloading: Redefine what operators like +, ==, << do for your custom class.

C++ — Overloading
// Function overloading
int    add(int a, int b)       { return a + b; }
double add(double a, double b) { return a + b; }
string add(string a, string b) { return a + b; }

// Operator overloading
class Vector2D {
public:
    double x, y;
    Vector2D(double x, double y) : x(x), y(y) {}

    // Overload + operator
    Vector2D operator+(const Vector2D& other) const {
        return Vector2D(x + other.x, y + other.y);
    }

    // Overload << for printing
    friend ostream& operator<<(ostream& os, const Vector2D& v) {
        return os << "(" << v.x << ", " << v.y << ")";
    }
};

int main() {
    Vector2D v1(1,2), v2(3,4);
    Vector2D v3 = v1 + v2;   // calls our overloaded +
    cout << v3;              // calls our overloaded << → (4, 6)
}
🔧 Real world analogy: Function overloading is like a smartphone with one volume button — press it in the music app to adjust music volume, in a call to adjust call volume, in an alarm to adjust alarm volume. Same button name, different behavior based on context. Operator overloading is like redefining what + means for complex numbers vs strings.
⚠️ Common Mistake: You CANNOT overload operators by just changing the return type — the parameter list must differ. Also, some operators cannot be overloaded: :: (scope), . (member access), ?: (ternary), sizeof. Trying to overload these causes a compile error.
📝 Exam Tip: Overloading = compile-time (static) polymorphism. It's NOT the same as overriding (which is runtime polymorphism). Key difference: overloading changes the function signature in the SAME class; overriding provides a new implementation in a DERIVED class with the SAME signature.
🔁

Function Overriding

REDEFINING PARENT BEHAVIOR IN CHILD

Function overriding occurs when a derived class provides its own implementation of a function that is already defined in the base class, with the exact same signature (name, parameters, return type). Unlike overloading, overriding works across classes and is the mechanism behind runtime polymorphism when combined with virtual.

The override keyword (C++11) is optional but highly recommended — it asks the compiler to verify you're actually overriding something. Without it, a typo in the function name creates a new function silently instead of overriding.

C++ — Overriding vs Overloading
class Animal {
public:
    virtual void speak() { cout << "Generic animal sound
"; }
};

class Dog : public Animal {
public:
    void speak() override { cout << "Woof!
"; }  // OVERRIDING
};

class Cat : public Animal {
public:
    void speak() override { cout << "Meow!
"; }  // OVERRIDING
    void speak(int times)  { /* ... */  }          // OVERLOADING (same class)
};

int main() {
    Animal* animals[] = { new Dog(), new Cat(), new Animal() };
    for (auto a : animals)
        a->speak();   // Woof! / Meow! / Generic — each calls its own version
}

OVERLOADING

  • Same class, same name
  • Different parameter list
  • Compile-time resolution
  • Static polymorphism
  • No virtual needed
  • Return type can differ too

OVERRIDING

  • Parent + child class
  • Exact same signature
  • Runtime resolution
  • Dynamic polymorphism
  • Requires virtual in base
  • Use override keyword
final keyword: Mark a virtual function with final to prevent any further overriding in subclasses: void speak() override final { }. You can also mark an entire class final to prevent it from being inherited at all.
🎼 Real world analogy: Overloading is like a musician who can play multiple instruments — same person, different performances based on which instrument (parameter) you hand them. Overriding is like a cover band performing the same song title as the original artist — same song name, but each band (class) plays it their own way.
⚠️ Common Mistake: Not using the override keyword. If you misspell the function name in the derived class (e.g., Speek() instead of speak()), without override the compiler silently creates a new function — no error, no overriding. With override, the compiler immediately tells you: "this doesn't override anything."
📝 Exam Tip — The Big Comparison: "Differentiate overloading and overriding" is one of the most common 5–10 mark questions in OOP exams. Memorize at least 5 differences. Key ones: same vs different class, compile-time vs runtime, different vs same signature, no virtual needed vs virtual required, static vs dynamic polymorphism.
⏱️

Runtime vs Compile-time Polymorphism

WHEN IS THE DECISION MADE?

COMPILE-TIME (Early)

  • Function overloading
  • Operator overloading
  • Templates
  • Resolved by compiler
  • Faster execution
  • No vtable overhead

RUNTIME (Late)

  • Virtual function override
  • Requires base class pointer
  • Resolved via vtable
  • Slightly slower
  • True polymorphic behavior
  • Enables plugin-like design
⏰ Real world analogy: Compile-time polymorphism is like a vending machine with buttons labeled A1, B2, etc. — pressing the button always gives the same item, decided when the machine was stocked. Runtime polymorphism is like asking a waiter for "the special" — the actual dish depends on what the chef prepared today (the runtime object type).
📝 Exam Tip: Runtime polymorphism requires THREE things working together: (1) inheritance, (2) a virtual function in the base class, (3) a base class pointer or reference. Without all three, you get compile-time binding. This trinity is critical to understand for any OOP exam.
🎭

Virtual Functions & vtable

THE ENGINE OF RUNTIME POLYMORPHISM

When you mark a function virtual, the compiler creates a vtable (virtual table) — a lookup table of function pointers for that class. Each object gets a hidden pointer to its class's vtable. When you call a virtual function through a pointer, the program looks up the vtable at runtime to find the actual function to call.

C++ — Runtime Polymorphism
class Shape {
public:
    virtual double area() = 0;   // pure virtual
    virtual ~Shape() {}
};
class Circle : public Shape {
    double r;
public:
    Circle(double r):r(r){}
    double area() override { return 3.14*r*r; }
};
class Rect : public Shape {
    double w,h;
public:
    Rect(double w,double h):w(w),h(h){}
    double area() override { return w*h; }
};

int main() {
    Shape* shapes[] = { new Circle(5), new Rect(4,6) };
    for (auto s : shapes)
        cout << s->area() << endl;  // 78.5, then 24 — correct! ✓
}
🎭 Real world analogy: The vtable is like a theater program book. Each performance (class) has a program listing which actor plays which role (function). When an audience member (code) calls for "the hero to speak", they check the program book at runtime to find who is ACTUALLY playing the hero tonight — not who was cast at audition time.
⚠️ Common Mistake: Forgetting that virtual dispatch requires a POINTER or REFERENCE. If you call through a direct object (not a pointer), even virtual functions use static binding: Animal a = Dog(); a.speak(); calls Animal::speak (object slicing also occurs). Always use Animal* ptr = new Dog(); for polymorphism.
📝 Exam Tip: vtable is created per class, vptr is created per object. Every object with virtual functions carries a hidden vptr (usually 8 bytes on 64-bit systems). This is the "small overhead" of virtual functions. Know: pure virtual = must override, virtual = may override, non-virtual = cannot override polymorphically.

Unary Operator Overloading

PREFIX, POSTFIX, NEGATION

Unary operators work on a single operand. The most commonly overloaded unary operators are ++ (increment), -- (decrement), - (negation), and ! (logical NOT). The trick to remember: prefix returns a reference (T&) and postfix takes a dummy int parameter and returns by value.

C++ — Unary Operator Overloading
class Counter {
    int value;
public:
    Counter(int v = 0) : value(v) {}

    // PREFIX ++obj  — increment, then return updated object
    Counter& operator++() {
        ++value;
        return *this;
    }

    // POSTFIX obj++  — save old value, increment, return OLD value
    Counter operator++(int) {   // dummy int param distinguishes postfix
        Counter temp = *this;   // save current
        ++value;                 // increment
        return temp;             // return OLD value
    }

    // NEGATION -obj
    Counter operator-() const { return Counter(-value); }

    void show() const { cout << value << endl; }
};

int main() {
    Counter c(5);
    ++c;       // calls operator++()   — c is now 6
    c++;       // calls operator++(int) — c is now 7
    (-c).show();  // -7
}

PREFIX ++obj

  • No parameters
  • Increments first
  • Returns reference to self
  • T& operator++()
  • More efficient (no copy)

POSTFIX obj++

  • Dummy int parameter
  • Saves old value first
  • Returns old value by copy
  • T operator++(int)
  • Less efficient (extra copy)
🏃 Real world analogy: Prefix increment is like "ready, set, go" — you prepare (increment) THEN act (return). Postfix is like a photo taken at the finish line — you capture the position (save old value) BEFORE crossing (incrementing). The photo shows where you WERE, not where you ended up.
⚠️ Common Mistake: Returning by value in prefix (slow) and by reference in postfix (dangerous — the local temp gets destroyed). Prefix should return T&, postfix should return T by value. Also: the dummy int in postfix is never given a name — it's literally just there as a signature differentiator.
📝 Exam Tip: The dummy int parameter is the signature that distinguishes postfix from prefix — exams love asking this. Know both signatures cold: prefix = T& operator++(), postfix = T operator++(int). Also know which operators CANNOT be overloaded: ::, ., .*, ?:, sizeof.
🛡️

Errors and Exception Handling

GRACEFUL ERROR MANAGEMENT

Exception handling separates error-handling code from normal code. Instead of returning error codes and checking them everywhere, you throw an exception when something goes wrong and catch it where you can handle it.

C++
class BankException : public exception {
    string msg;
public:
    BankException(string m) : msg(m) {}
    const char* what() const noexcept override { return msg.c_str(); }
};

class Account {
    double balance;
public:
    Account(double b): balance(b){}
    void withdraw(double amt) {
        if (amt > balance) throw BankException("Insufficient funds!");
        balance -= amt;
    }
};

int main() {
    try {
        Account acc(100);
        acc.withdraw(200);      // throws
    } catch (const BankException& e) {
        cout << "Error: " << e.what() << endl;
    } catch (...) {
        cout << "Unknown error!\n";  // catch-all
    }
}
Catch by const reference (const BankException&), not by value. Catching by value creates an unnecessary copy and can cause object slicing if catching a base exception type.
🏦 Real world analogy: Exception handling is like a bank teller protocol. The teller (function) tries a transaction (try block). If the card is declined (exception thrown), they don't panic — they follow a specific error protocol (catch block). The bank manager watching might handle systemic issues (outer catch block). Normal customers behind you aren't affected.
⚠️ Common Mistake: Catching exceptions by value instead of by reference. Catch by value causes unnecessary copying and, more critically, suffers from object slicing — if you catch a base exception type by value, the derived exception's extra data is lost. Always use catch(const MyException& e).
📝 Exam Tip: The catch-all block catch(...) catches EVERYTHING but gives you no information about the exception. Order of catch blocks matters — put most-specific (derived) exceptions before most-general (base). If you put catch(exception&) first, it will swallow all derived exceptions too.

Advanced Topics

Going beyond core OOP — the concepts that make large-scale software design possible.

💾

Persistent Objects & Portable Data

OBJECTS THAT SURVIVE PROGRAM SHUTDOWN

Normally, all objects cease to exist when a program ends. Persistent objects are objects whose state is saved and can be restored later — via files, databases, or serialization.

Serialization converts an object to a storable format (JSON, XML, binary). Deserialization reconstructs the object from that format. This is how game saves, user settings, and databases work.

C++ — Saving object to file
#include <fstream>

struct Player { string name; int score; };

void save(Player& p) {
    ofstream f("save.dat", ios::binary);
    f.write((char*)&p, sizeof(p));
}

void load(Player& p) {
    ifstream f("save.dat", ios::binary);
    f.read((char*)&p, sizeof(p));
}
📐

UML Basics

VISUALIZE BEFORE YOU CODE

UML (Unified Modeling Language) is a standardized way to visually represent class structure and relationships before writing code. A class diagram shows classes as boxes with three sections: class name, attributes, and methods. Relationships are shown with arrows.

Arrow/NotationMeaningExample
—▷ (hollow arrow)Inheritance (IS-A)Dog inherits Animal
- - ▷ (dashed)Interface implementationCircle implements IShape
◆— (filled diamond)Composition (owns)Car has Engine (dies with car)
◇— (hollow diamond)Aggregation (uses)School has Students (exist independently)
——AssociationStudent uses Library
Draw before you code. Sketching a UML diagram for 10 minutes before writing prevents hours of refactoring later. Even a rough diagram on paper counts.
🧩

Templates

WRITE ONCE, WORK WITH ANY TYPE

Templates allow you to write a function or class that works with any data type. Instead of writing separate swap() functions for int, double, string — write one template and the compiler generates the appropriate version for each type used.

C++
// Function template
template<typename T>
T maxOf(T a, T b) { return (a > b) ? a : b; }

// Class template
template<typename T>
class Stack {
    T arr[100];
    int top = -1;
public:
    void push(T val) { arr[++top] = val; }
    T    pop()       { return arr[top--]; }
};

int main() {
    cout << maxOf(3, 7);           // 7 (int)
    cout << maxOf(3.14, 2.71);    // 3.14 (double)

    Stack<int> intStack;
    Stack<string> strStack;
}
📦

Namespace

AVOIDS NAME COLLISIONS

A namespace groups related code under a named scope to prevent naming conflicts. When two libraries both have a class called Vector, namespaces let them coexist without collision. std:: is the namespace for the C++ standard library.

C++
namespace Physics {
    double velocity(double d, double t) { return d/t; }
}

namespace Math {
    double velocity(double x) { return x*x; }  // same name, no conflict
}

int main() {
    Physics::velocity(100, 5);  // 20.0
    Math::velocity(4);         // 16.0
}
Avoid using namespace std; in headers. It pollutes the global namespace for anyone who includes your header. It's fine in .cpp files for convenience, but never in headers.
🧵

Multithreading

DO MULTIPLE THINGS SIMULTANEOUSLY

A thread is the smallest unit of execution. Multithreading allows a program to do multiple tasks at the same time — like loading data in the background while the UI stays responsive. In OOP, each thread can operate on its own objects. The challenge is ensuring multiple threads don't corrupt shared data — this requires synchronization with mutexes.

C++ — Basic Thread (C++11)
#include <thread>
#include <mutex>

int counter = 0;
mutex mtx;

void increment() {
    for (int i = 0; i < 1000; i++) {
        lock_guard<mutex> lock(mtx);  // prevent race condition
        counter++;
    }
}

int main() {
    thread t1(increment), t2(increment);
    t1.join();  // wait for t1 to finish
    t2.join();  // wait for t2 to finish
    cout << counter;  // 2000
}
⚡ Edge Case — Race Condition

Without the mutex, two threads can read counter = 5, both increment to 6, and both write back 6 — losing one increment. This is a race condition and produces non-deterministic bugs that are extremely hard to find. Always protect shared mutable data.

🏛️

Concept of MVC Framework

SEPARATE CONCERNS IN LARGE APPS

MVC (Model-View-Controller) is an architectural pattern that separates a program into three interconnected parts. It's the foundation of most web frameworks (Django, Rails, Laravel, Spring MVC) and many desktop app frameworks.

🗃️

Model

Data & business logic. No UI knowledge. e.g., Student class, database queries.

🖥️

View

Presentation layer only. Displays data. No business logic. e.g., HTML, UI components.

🎮

Controller

Handles user input. Tells Model to update. Tells View to refresh.

Why it matters for OOP: MVC is OOP principles applied at the architectural level. Encapsulation between layers, single responsibility for each component, and clean interfaces between them.
0%