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.
Table of Contents
Six chapters covering every OOP concept from fundamentals to advanced topics. Click any card to jump directly to that chapter.
- Introduction to OOP
- OOP Design Principles
- Encapsulation & Info Hiding
- Data Binding
- Static vs Dynamic Binding
- this Pointer
- const Keyword in Classes
- Structure of a Class
- Access Modifiers
- Nested & Abstract Classes
- Arrays & Pointers to Objects
- Friend Function, Static Members
- Default, Parameterized & Copy
- Copy Assignment Operator
- Dynamic Constructor
- Deep vs Shallow Copy
- Derived Class Constructors
- Destructor & Virtual Destructor
- Rule of Three
- Single vs Multiple Inheritance
- Multilevel & Hierarchical
- Modes of Inheritance
- Virtual Inheritance
- Interface
- Upcasting & Downcasting
- Operator & Function Overloading
- Function Overriding (vs Overloading)
- Runtime vs Compile-time
- Virtual Functions & vtable
- Unary Operator Overloading
- Exception Handling
- Persistent Objects & UML
- Templates & Namespace
- Multithreading
- MVC Framework
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
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
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.
Encapsulation & Information Hiding
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.
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 ✓ }
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.
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.Data Binding
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
virtual to everything wastes memory (vtable) and slows execution.Static and Dynamic Binding
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.
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 ✓ }
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.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
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.
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! }
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.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.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
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.
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
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.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.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
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.
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 }
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.Access Modifiers
Access modifiers define the visibility of class members. They are the primary tool for implementing encapsulation.
| Modifier | Same Class | Child Class | Outside Class | Use Case |
|---|---|---|---|---|
private | ✓ | ✗ | ✗ | Internal data, balance, password |
protected | ✓ | ✓ | ✗ | Data child classes need to access |
public | ✓ | ✓ | ✓ | Interface methods, getters/setters |
private. Only make things public when external code genuinely needs access. Use protected specifically when child classes need it.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).private. In a struct → public. Also memorize: protected members ARE accessible in derived classes (unlike private), but NOT outside the class hierarchy. This distinction is crucial.Nested Classes
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.
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(); }
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.
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
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.
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
work() method. But you can't hire just an "Employee" — you hire a "Software Engineer" or "Designer" (concrete classes) who fulfills that job description.= 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.= 0. Always include a virtual destructor (virtual ~Shape(){}) in abstract classes — forgetting it causes undefined behavior when deleting derived objects.Arrays of 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.
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! }
delete[] not delete. Using delete on an array only destroys the first element's destructor — the rest leak memory.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
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.
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!
.) 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
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.
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! ✓ }
<< operator for printing.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.
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
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.
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 }
this pointer since it doesn't belong to any specific object.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.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
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.
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 }
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.
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
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.
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 }
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
: 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.Copy Constructor
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).
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) }
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.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
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.
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&)
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.Dynamic Constructor
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.
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 }
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
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 ✓
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; } };
int* and asks why it crashes — the answer is shallow copy / double free.Constructor for Derived Class & Order of Execution
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.
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
Destructor
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.
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 ✓ }
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
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.
// 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 }
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.
Multilevel & Hierarchical Inheritance
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.
// ── 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.
Modes of Inheritance
The mode keyword (public, protected, private) after the colon controls how the parent's members are seen in the child.
| Parent Member | public inheritance | protected inheritance | private inheritance |
|---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | inaccessible | inaccessible | inaccessible |
public inheritance. It preserves the IS-A relationship. Private/protected inheritance represents "implemented-in-terms-of" and is rarely used.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
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.
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 ✓
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
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.
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
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.
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 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.
dynamic_cast is like checking the ID tag first.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
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.
// 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) }
:: (scope), . (member access), ?: (ternary), sizeof. Trying to overload these causes a compile error.Function Overriding
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.
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 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.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."Runtime vs Compile-time Polymorphism
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
Virtual Functions & vtable
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.
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! ✓ }
Animal a = Dog(); a.speak(); calls Animal::speak (object slicing also occurs). Always use Animal* ptr = new Dog(); for polymorphism.Unary Operator Overloading
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.
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)
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.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
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.
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 } }
const BankException&), not by value. Catching by value creates an unnecessary copy and can cause object slicing if catching a base exception type.exception type by value, the derived exception's extra data is lost. Always use catch(const MyException& e).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
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.
#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
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/Notation | Meaning | Example |
|---|---|---|
—▷ (hollow arrow) | Inheritance (IS-A) | Dog inherits Animal |
- - ▷ (dashed) | Interface implementation | Circle implements IShape |
◆— (filled diamond) | Composition (owns) | Car has Engine (dies with car) |
◇— (hollow diamond) | Aggregation (uses) | School has Students (exist independently) |
—— | Association | Student uses Library |
Templates
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.
// 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
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.
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 }
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
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.
#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 }
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
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.