01-classes-objects-constructors.md

Module 01: Classes, Objects & Constructors

Goal: Understand the building blocks of OOP — classes, objects, constructors, destructors, and memory. Time: 2 days of focused study Prerequisites: Basic C++ syntax (variables, functions, pointers)


Table of Contents

  1. What is OOP? Why OOP?
  2. Classes and Objects
  3. Access Modifiers (Preview)
  4. Constructors
  5. Destructors
  6. The this Pointer
  7. Static Members
  8. const Correctness
  9. Rule of Three / Five / Zero
  10. Memory Layout of Objects
  11. Common Mistakes
  12. Practice Problems
  13. Interview Questions

1. What is OOP? Why OOP?

Procedural vs OOP

PROCEDURAL (C-style): Data and functions are SEPARATE. Functions operate on data passed to them. As programs grow, data flows become unmanageable. struct BankAccount { string owner; double balance; }; void deposit(BankAccount& acc, double amount) { acc.balance += amount; } void withdraw(BankAccount& acc, double amount) { acc.balance -= amount; } // Anyone can do: acc.balance = -99999; ← NO PROTECTION! OOP: Data and functions are BUNDLED TOGETHER inside objects. Objects protect their own data and expose controlled interfaces. class BankAccount { private: string owner; double balance; // Can't be accessed from outside! public: void deposit(double amount) { balance += amount; } bool withdraw(double amount) { if (amount > balance) return false; // Protected! balance -= amount; return true; } };

Real-World Analogy

A CAR is an object: - Data (attributes): color, speed, fuel, engineType - Behavior (methods): start(), accelerate(), brake(), refuel() You don't need to know HOW the engine works to drive. You just use the steering wheel, pedals, and gear (the INTERFACE). The engine internals are HIDDEN (encapsulation). All cars share the same BLUEPRINT (class). Each physical car is an INSTANCE (object) of that blueprint.

The Four Pillars of OOP

1. ENCAPSULATION — Bundle data + methods, hide internals 2. ABSTRACTION — Show only what's necessary, hide complexity 3. INHERITANCE — Create new classes from existing ones 4. POLYMORPHISM — One interface, multiple behaviors

2. Classes and Objects

Class = Blueprint, Object = Instance

#include <iostream> #include <string> using namespace std; // CLASS — the blueprint class Dog { public: // Data members (attributes) string name; string breed; int age; // Member functions (methods) void bark() { cout << name << " says: Woof!" << endl; } void info() { cout << name << " | " << breed << " | " << age << " years" << endl; } }; int main() { // OBJECTS — instances of the class Dog dog1; // Create object (calls default constructor) dog1.name = "Buddy"; dog1.breed = "Golden Retriever"; dog1.age = 3; Dog dog2; dog2.name = "Max"; dog2.breed = "German Shepherd"; dog2.age = 5; dog1.bark(); // Buddy says: Woof! dog2.bark(); // Max says: Woof! dog1.info(); // Buddy | Golden Retriever | 3 years // Each object has its OWN copy of data members // But they share the same function definitions (code is not duplicated) return 0; }

Class vs Struct in C++

// In C++, class and struct are ALMOST IDENTICAL. // The ONLY difference is the default access modifier: struct Point { // Members are PUBLIC by default int x, y; // ← public }; class Point2 { // Members are PRIVATE by default int x, y; // ← private (can't access from outside!) public: void setX(int val) { x = val; } }; // Convention: // Use struct for simple data holders (POD — Plain Old Data) // Use class for objects with behavior and encapsulation

Java equivalent: Java has no struct. Everything is a class. Default access is package-private (not public or private).

Object Creation — Stack vs Heap

#include <iostream> #include <memory> using namespace std; class Player { public: string name; int health; Player(string n, int h) : name(n), health(h) { cout << name << " created" << endl; } ~Player() { cout << name << " destroyed" << endl; } }; int main() { // ---- STACK ALLOCATION (automatic lifetime) ---- { Player p1("Alice", 100); // Created on stack // p1 is automatically destroyed when this scope ends } // ← p1's destructor called HERE // ---- HEAP ALLOCATION (manual lifetime) ---- Player* p2 = new Player("Bob", 100); // Created on heap // p2 exists until you explicitly delete it delete p2; // ← Destructor called HERE. Forgetting this = MEMORY LEAK! // ---- SMART POINTERS (modern C++ — preferred!) ---- { auto p3 = make_unique<Player>("Charlie", 100); // Heap, auto-deleted // No need for delete — unique_ptr handles it! } // ← p3's destructor called HERE automatically return 0; } /* Output: Alice created Alice destroyed Bob created Bob destroyed Charlie created Charlie destroyed */
STACK vs HEAP: Stack: ✅ Fast allocation/deallocation ✅ Automatic cleanup (RAII) ❌ Limited size (usually 1-8 MB) ❌ Object dies when scope ends Heap: Large (limited by system memory) ✅ Object lives as long as you want ❌ Slower allocation ❌ Must manually free (or use smart pointers) Memory leaks if you forget delete

3. Access Modifiers (Preview)

class Employee { public: // Accessible from ANYWHERE string getName() { return name; } protected: // Accessible from THIS class and DERIVED classes int employeeId; private: // Accessible ONLY from THIS class string name; double salary; }; int main() { Employee e; e.getName(); // ✅ public — works e.employeeId = 1; // ❌ protected — compile error e.salary = 50000; // ❌ private — compile error }
Access Modifier │ Same Class │ Derived Class │ Outside ────────────────┼────────────┼───────────────┼──────── public │ ✅ │ ✅ │ ✅ protected │ ✅ │ ✅ │ ❌ private │ ✅ │ ❌ │ ❌

Java equivalent: Java has public, protected, private, and also package-private (no keyword — accessible within the same package).


4. Constructors

A constructor is a special member function that initializes an object when it's created.

Rules

1. Same name as the class 2. No return type (not even void!) 3. Called automatically when object is created 4. Can be overloaded (multiple constructors) 5. If you write NO constructor, compiler generates a default one

Default Constructor

class Rectangle { public: int width; int height; // Default constructor — no parameters Rectangle() { width = 0; height = 0; cout << "Default constructor called" << endl; } int area() { return width * height; } }; int main() { Rectangle r; // Default constructor called cout << r.area() << endl; // 0 }

Parameterized Constructor

class Rectangle { public: int width; int height; // Parameterized constructor Rectangle(int w, int h) { width = w; height = h; } // You can have BOTH default and parameterized (overloading) Rectangle() : width(0), height(0) {} int area() { return width * height; } }; int main() { Rectangle r1(10, 5); // Parameterized — width=10, height=5 Rectangle r2; // Default — width=0, height=0 Rectangle r3(7, 3); cout << r1.area() << endl; // 50 cout << r2.area() << endl; // 0 }

⚠️ The Compiler-Generated Default Constructor

class Foo { public: int x; // NO constructor defined → compiler generates default constructor }; class Bar { public: int x; Bar(int val) : x(val) {} // Only parameterized constructor // Compiler does NOT generate default constructor anymore! }; int main() { Foo f; // ✅ Works — compiler-generated default constructor Bar b(10); // ✅ Works // Bar b2; // ❌ ERROR! No default constructor exists! // Fix: explicitly define a default constructor // Or use: Bar() = default; }

Member Initializer List (Preferred Way)

class Player { private: string name; int health; const int maxHealth; // const members MUST be initialized in initializer list! int& ref; // References MUST be initialized in initializer list! public: // ❌ Assignment in body (works but less efficient for complex types) // Player(string n, int h) { // name = n; // First default-constructs, then assigns // health = h; // } // ✅ Member initializer list (directly constructs with the given value) Player(string n, int h, int& r) : name(n), health(h), maxHealth(100), ref(r) // ← Initializer list { // Constructor body (for additional logic) cout << "Player " << name << " created!" << endl; } }; /* WHY INITIALIZER LIST IS BETTER: Assignment in body: 1. Default-construct the member 2. Then assign a new value ← wasteful for strings, vectors, etc. Initializer list: 1. Directly construct with the value ← one step, more efficient REQUIRED for: - const members - reference members - members without default constructors - base class initialization */

Copy Constructor

class Student { public: string name; int* grades; // Dynamic array (pointer!) int count; // Regular constructor Student(string n, int c) : name(n), count(c) { grades = new int[count]; for (int i = 0; i < count; i++) grades[i] = 0; } // ---- SHALLOW COPY (DEFAULT — DANGEROUS with pointers!) ---- // If you don't write a copy constructor, the compiler generates one // that does a MEMBER-WISE COPY (shallow copy). // Both objects would point to the SAME grades array! // Deleting one corrupts the other! // ---- DEEP COPY (you must write this!) ---- Student(const Student& other) : name(other.name), count(other.count) { grades = new int[count]; // Allocate NEW memory for (int i = 0; i < count; i++) { grades[i] = other.grades[i]; // Copy values } cout << "Deep copy of " << name << endl; } ~Student() { delete[] grades; // Free memory } }; int main() { Student s1("Alice", 5); Student s2 = s1; // Copy constructor called (deep copy) Student s3(s1); // Same thing — copy constructor // s1 and s2 have SEPARATE grade arrays (deep copy) // Modifying s1.grades does NOT affect s2.grades }
SHALLOW COPY vs DEEP COPY: Shallow Copy (default): s1.grades ──► [90, 85, 78] ◄── s2.grades Both point to SAME memory! Delete s1 → s2.grades becomes a dangling pointer → CRASH! Deep Copy (you write this): s1.grades ──► [90, 85, 78] s2.grades ──► [90, 85, 78] ← separate copy! Deleting s1 doesn't affect s2.

Move Constructor (C++11)

#include <iostream> #include <string> #include <utility> // for std::move using namespace std; class Buffer { int* data; size_t size; public: // Regular constructor Buffer(size_t s) : size(s), data(new int[s]) { cout << "Constructed (size=" << s << ")" << endl; } // Copy constructor (expensive — copies all data) Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) { copy(other.data, other.data + size, data); cout << "Copied (size=" << size << ")" << endl; } // Move constructor (cheap — steals resources) Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) { other.data = nullptr; // Leave source in valid but empty state other.size = 0; cout << "Moved (size=" << size << ")" << endl; } ~Buffer() { delete[] data; cout << "Destroyed" << endl; } }; Buffer createBuffer() { Buffer temp(1000); return temp; // Move constructor (or copy elision) } int main() { Buffer b1(100); // Constructed Buffer b2 = b1; // Copied (expensive) Buffer b3 = std::move(b1); // Moved (cheap — b1 is now empty!) // b1 is now in a "moved-from" state — don't use it (except destroy)! Buffer b4 = createBuffer(); // Move or copy elision } /* Move Semantics — WHEN and WHY: Copy: Allocate new memory → Copy every element → Slow for large objects Move: Steal the pointer → Set source to null → O(1) regardless of size! std::move() doesn't actually move anything! It just CASTS an lvalue to an rvalue reference (&&), enabling the move constructor/assignment to be called. Analogy: Copy = Photocopy a 1000-page book (slow, uses paper) Move = Hand the book to someone else (fast, original no longer has it) */

Delegating Constructors (C++11)

class Connection { string host; int port; bool ssl; public: // Primary constructor Connection(string h, int p, bool s) : host(h), port(p), ssl(s) { cout << "Connecting to " << host << ":" << port << endl; } // Delegates to the primary constructor Connection(string h, int p) : Connection(h, p, false) {} // Delegates further Connection(string h) : Connection(h, 80) {} // Default Connection() : Connection("localhost") {} }; // Java equivalent: this("localhost", 80, false);

explicit Keyword — Prevent Implicit Conversions

class Fraction { int num, den; public: // Without explicit: Fraction(int n, int d = 1) : num(n), den(d) {} // Fraction f = 5; ← This works! Implicitly calls Fraction(5, 1) // This is often UNINTENDED and can cause subtle bugs. // With explicit: explicit Fraction(int n, int d = 1) : num(n), den(d) {} // Fraction f = 5; ← ERROR! No implicit conversion // Fraction f(5); ← OK — explicit construction // Fraction f = Fraction(5); ← OK }; // RULE: Use `explicit` for single-argument constructors // unless you intentionally want implicit conversion.

5. Destructors

A destructor cleans up when an object is destroyed.

class FileHandler { FILE* file; string filename; public: FileHandler(const string& name) : filename(name) { file = fopen(name.c_str(), "r"); if (file) cout << "Opened: " << filename << endl; } // Destructor — called when object goes out of scope or is deleted ~FileHandler() { if (file) { fclose(file); cout << "Closed: " << filename << endl; } } // Rules: // 1. Same name as class, prefixed with ~ // 2. No parameters, no return type // 3. Only ONE destructor per class (can't overload) // 4. Called automatically — you rarely call it manually }; int main() { { FileHandler f("data.txt"); // Constructor: Opened data.txt // Use file... } // Destructor: Closed data.txt ← AUTOMATIC cleanup! // This pattern is called RAII (Resource Acquisition Is Initialization) // The resource (file) is acquired in the constructor // and released in the destructor. No leaks possible! }

When Are Destructors Called?

1. Stack objects: when they go out of scope 2. Heap objects: when you call delete 3. Static objects: when the program ends 4. Temporary objects: at the end of the full expression 5. Array elements: when array is deleted (in reverse order!) 6. In derived classes: derived destructor runs first, then base

Virtual Destructors (Critical!)

class Base { public: Base() { cout << "Base constructed" << endl; } // ❌ WITHOUT virtual destructor: // ~Base() { cout << "Base destroyed" << endl; } // ✅ WITH virtual destructor: virtual ~Base() { cout << "Base destroyed" << endl; } }; class Derived : public Base { int* data; public: Derived() : data(new int[100]) { cout << "Derived constructed" << endl; } ~Derived() { delete[] data; // Free allocated memory cout << "Derived destroyed" << endl; } }; int main() { Base* ptr = new Derived(); // Polymorphic usage delete ptr; // Without virtual destructor: // Output: Base destroyed ← Derived destructor NEVER CALLED! // MEMORY LEAK! data is never freed! // With virtual destructor: // Output: Derived destroyed // Base destroyed ← Both called correctly! } // RULE: If a class has ANY virtual functions, it MUST have a virtual destructor. // If a class is intended to be a base class, give it a virtual destructor.

6. The this Pointer

this is a hidden pointer to the current object. Every non-static member function receives it implicitly.

class Rectangle { int width, height; public: // 'this' resolves name conflicts Rectangle(int width, int height) { this->width = width; // this->width = member, width = parameter this->height = height; } // Better: use initializer list to avoid the issue entirely // Rectangle(int width, int height) : width(width), height(height) {} // 'this' enables method chaining (fluent interface) Rectangle& setWidth(int w) { width = w; return *this; // Return reference to current object } Rectangle& setHeight(int h) { height = h; return *this; } void print() { cout << width << " x " << height << endl; } }; int main() { Rectangle r(0, 0); // Method chaining! r.setWidth(10).setHeight(5).print(); // 10 x 5 // This works because each method returns *this (the same object) // r.setWidth(10) returns r → r.setHeight(5) returns r → r.print() }
Under the hood, the compiler transforms: r.setWidth(10); into: Rectangle::setWidth(&r, 10); // 'this' = &r (address of r) 'this' is always a CONST POINTER to the object: Rectangle* const this; // Can't change what 'this' points to In a const method: const Rectangle* const this; // Can't modify the object either

7. Static Members

Static members belong to the class, not to any individual object.

Static Data Members

class Player { string name; public: static int playerCount; // Shared across ALL Player objects Player(string n) : name(n) { playerCount++; cout << name << " joined. Total: " << playerCount << endl; } ~Player() { playerCount--; cout << name << " left. Total: " << playerCount << endl; } }; // MUST be defined outside the class (allocates storage) int Player::playerCount = 0; int main() { Player p1("Alice"); // Total: 1 Player p2("Bob"); // Total: 2 { Player p3("Charlie"); // Total: 3 } // Charlie destroyed — Total: 2 // Access via class name (no object needed) cout << "Players online: " << Player::playerCount << endl; // 2 }

Static Member Functions

class MathUtils { public: // Static functions can be called WITHOUT creating an object static int add(int a, int b) { return a + b; } static int max(int a, int b) { return a > b ? a : b; } // Static functions CANNOT access non-static members // (because there's no 'this' pointer — no specific object!) // int instanceVar; // static void foo() { instanceVar = 5; } ← ❌ ERROR! // Static functions CAN access other static members static int totalCalls; static void track() { totalCalls++; } // ✅ OK }; int MathUtils::totalCalls = 0; int main() { // No need to create an object int result = MathUtils::add(3, 4); // 7 int m = MathUtils::max(10, 20); // 20 }
STATIC MEMBERS — Key Points: Static Data: ✅ Shared by ALL objects of the class ✅ Exists even if no objects exist ✅ Must be defined outside the class ✅ Initialized to 0 by default Static Functions: ✅ Can be called without an object: ClassName::function() ❌ Cannot access non-static members (no 'this' pointer) ❌ Cannot be virtual ✅ Can access static members only

Java equivalent: static works similarly. Java also has static blocks for initialization. In Java, static methods are inherited but not overridden (they're hidden).


8. const Correctness

Const Objects and Const Methods

class Account { string name; double balance; public: Account(string n, double b) : name(n), balance(b) {} // const method — promises NOT to modify the object double getBalance() const { // balance = 0; ← ❌ ERROR! Can't modify in const method return balance; } string getName() const { return name; } // Non-const method — CAN modify the object void deposit(double amount) { balance += amount; } }; int main() { const Account acc("Alice", 1000); acc.getBalance(); // ✅ const method on const object — OK acc.getName(); // ✅ const method — OK // acc.deposit(50); // ❌ ERROR! Non-const method on const object! // RULE: const objects can ONLY call const methods }

Const with Pointers (The Confusing Part)

int x = 10, y = 20; // READ RIGHT-TO-LEFT: const int* p1 = &x; // Pointer TO a const int → can't modify *p1 int* const p2 = &x; // Const pointer TO an int → can't modify p2 const int* const p3 = &x; // Const pointer to const int → can't modify either *p1 = 5; // ❌ Can't modify the value pointed to p1 = &y; // ✅ Can change what it points to *p2 = 5; // ✅ Can modify the value p2 = &y; // ❌ Can't change what it points to *p3 = 5; // ❌ Can't modify value p3 = &y; // ❌ Can't change pointer // Mnemonic: "const applies to what's on its LEFT (or right if nothing is on left)"

mutable — Exception to const

class Cache { mutable int accessCount = 0; // CAN be modified in const methods string data; public: Cache(string d) : data(d) {} string getData() const { accessCount++; // ✅ OK because accessCount is mutable return data; } int getAccessCount() const { return accessCount; } };

9. Rule of Three / Five / Zero

Rule of Three (Pre-C++11)

If you define ANY of these, you should define ALL THREE:

  1. Destructor
  2. Copy Constructor
  3. Copy Assignment Operator
class DynamicArray { int* data; size_t size; public: // Constructor DynamicArray(size_t s) : size(s), data(new int[s]()) {} // 1. Destructor ~DynamicArray() { delete[] data; } // 2. Copy Constructor (deep copy) DynamicArray(const DynamicArray& other) : size(other.size) { data = new int[size]; copy(other.data, other.data + size, data); } // 3. Copy Assignment Operator DynamicArray& operator=(const DynamicArray& other) { if (this != &other) { // Self-assignment check! delete[] data; // Free old data size = other.size; data = new int[size]; copy(other.data, other.data + size, data); } return *this; } };

Rule of Five (C++11+)

Add these two to the Rule of Three: 4. Move Constructor 5. Move Assignment Operator

class DynamicArray { int* data; size_t size; public: DynamicArray(size_t s) : size(s), data(new int[s]()) {} // 1. Destructor ~DynamicArray() { delete[] data; } // 2. Copy Constructor DynamicArray(const DynamicArray& other) : size(other.size), data(new int[other.size]) { copy(other.data, other.data + size, data); } // 3. Copy Assignment DynamicArray& operator=(const DynamicArray& other) { if (this != &other) { delete[] data; size = other.size; data = new int[size]; copy(other.data, other.data + size, data); } return *this; } // 4. Move Constructor DynamicArray(DynamicArray&& other) noexcept : data(other.data), size(other.size) { other.data = nullptr; other.size = 0; } // 5. Move Assignment DynamicArray& operator=(DynamicArray&& other) noexcept { if (this != &other) { delete[] data; data = other.data; size = other.size; other.data = nullptr; other.size = 0; } return *this; } };

Rule of Zero (Modern C++ — PREFERRED!)

If your class doesn't manage resources directly, don't define ANY of the five. Use RAII wrappers (smart pointers, std::vector, std::string) instead.

// ✅ Rule of Zero — let the standard library handle resources class Student { string name; // std::string manages its own memory vector<int> grades; // std::vector manages its own memory unique_ptr<Address> address; // unique_ptr manages its own memory public: Student(string n) : name(move(n)) {} // NO destructor needed! // NO copy constructor needed! // NO assignment operator needed! // The compiler-generated defaults work correctly because // all members know how to copy/move/destroy themselves. };
WHICH RULE TO FOLLOW: Rule of Zero (PREFERRED): Use RAII types (string, vector, unique_ptr, shared_ptr) Let compiler generate everything Rule of Five: When you MUST manage raw resources (rare in modern C++) Define all 5 special members Rule of Three: Legacy C++ (pre-C++11) Still important to understand for interviews

10. Memory Layout of Objects

class Simple { int x; // 4 bytes char c; // 1 byte double d; // 8 bytes }; // What is sizeof(Simple)? // NOT 13 (4 + 1 + 8)! // It's 16 or 24 due to PADDING/ALIGNMENT! /* Memory layout (64-bit system, typical): Offset 0: [x x x x] ← int x (4 bytes) Offset 4: [c . . .] ← char c (1 byte) + 3 bytes PADDING Offset 8: [d d d d d d d d] ← double d (8 bytes) Total: 16 bytes WHY PADDING? CPUs read memory in aligned chunks. A double at offset 5 would require two memory reads. Padding ensures each member is at its natural alignment. REORDERING MEMBERS CAN REDUCE SIZE: class Smaller { double d; // 8 bytes, offset 0 int x; // 4 bytes, offset 8 char c; // 1 byte, offset 12 + 3 padding }; // Still 16 bytes, but if you had more fields, order matters! */ class WithVirtual { virtual void foo() {} int x; }; // sizeof(WithVirtual) = 16 on 64-bit (8 for vptr + 4 for int + 4 padding) // The vptr (virtual table pointer) adds 8 bytes on 64-bit systems!

11. Common Mistakes

// ❌ MISTAKE 1: Forgetting to initialize members class Bad { int x; // UNINITIALIZED — contains garbage! public: int getX() { return x; } // Returns garbage }; // ✅ Fix: Always initialize in constructor or at declaration // ❌ MISTAKE 2: Not making destructor virtual in base class class Base { ~Base() {} // NOT virtual! }; class Derived : public Base { int* ptr = new int[100]; ~Derived() { delete[] ptr; } }; // Base* b = new Derived(); delete b; ← MEMORY LEAK! // ✅ Fix: virtual ~Base() {} // ❌ MISTAKE 3: Missing self-assignment check in operator= MyClass& operator=(const MyClass& other) { delete[] data; // Oops, if this == &other, you just deleted your own data! data = new int[other.size]; // ... } // ✅ Fix: if (this != &other) { ... } // ❌ MISTAKE 4: Returning reference/pointer to local variable int& bad() { int x = 42; return x; // x is destroyed when function returns — dangling reference! } // ❌ MISTAKE 5: Using raw new/delete in modern C++ int* arr = new int[100]; // ... exception thrown here? delete[] arr; // Never reached — MEMORY LEAK! // ✅ Fix: vector<int> arr(100); or unique_ptr<int[]> arr(new int[100]);

12. Practice Problems

Problem 1: Implement a String Class

// Implement a simplified MyString class that: // - Stores a char array on the heap // - Has all 5 special members (Rule of Five) // - Supports length(), c_str(), and print() class MyString { char* data; size_t len; public: // Constructor from C-string MyString(const char* str = "") { len = strlen(str); data = new char[len + 1]; strcpy(data, str); } // Destructor ~MyString() { delete[] data; } // Copy constructor MyString(const MyString& other) : len(other.len) { data = new char[len + 1]; strcpy(data, other.data); } // Copy assignment MyString& operator=(const MyString& other) { if (this != &other) { delete[] data; len = other.len; data = new char[len + 1]; strcpy(data, other.data); } return *this; } // Move constructor MyString(MyString&& other) noexcept : data(other.data), len(other.len) { other.data = nullptr; other.len = 0; } // Move assignment MyString& operator=(MyString&& other) noexcept { if (this != &other) { delete[] data; data = other.data; len = other.len; other.data = nullptr; other.len = 0; } return *this; } size_t length() const { return len; } const char* c_str() const { return data; } void print() const { cout << (data ? data : "") << endl; } };

Problem 2: Implement a Counter with Static Members

// Create a class where each object gets a unique ID // Track total objects created and currently alive class TrackedObject { static int totalCreated; static int currentlyAlive; int id; public: TrackedObject() : id(++totalCreated) { currentlyAlive++; cout << "Object #" << id << " created. Alive: " << currentlyAlive << endl; } TrackedObject(const TrackedObject& other) : id(++totalCreated) { currentlyAlive++; cout << "Object #" << id << " copied from #" << other.id << endl; } ~TrackedObject() { currentlyAlive--; cout << "Object #" << id << " destroyed. Alive: " << currentlyAlive << endl; } int getId() const { return id; } static int getTotalCreated() { return totalCreated; } static int getCurrentlyAlive() { return currentlyAlive; } }; int TrackedObject::totalCreated = 0; int TrackedObject::currentlyAlive = 0;

Problem 3: Method Chaining Builder

// Create a QueryBuilder class with method chaining class QueryBuilder { string table; string conditions; string ordering; int limitVal = -1; public: QueryBuilder& from(const string& t) { table = t; return *this; } QueryBuilder& where(const string& condition) { if (!conditions.empty()) conditions += " AND "; conditions += condition; return *this; } QueryBuilder& orderBy(const string& field) { ordering = field; return *this; } QueryBuilder& limit(int n) { limitVal = n; return *this; } string build() const { string query = "SELECT * FROM " + table; if (!conditions.empty()) query += " WHERE " + conditions; if (!ordering.empty()) query += " ORDER BY " + ordering; if (limitVal > 0) query += " LIMIT " + to_string(limitVal); return query + ";"; } }; // Usage: // string q = QueryBuilder() // .from("users") // .where("age > 18") // .where("active = true") // .orderBy("name") // .limit(10) // .build(); // → "SELECT * FROM users WHERE age > 18 AND active = true ORDER BY name LIMIT 10;"

13. Interview Questions

Q1: What is a class? What is an object?

Answer: A class is a user-defined blueprint or template that defines data members (attributes) and member functions (behavior). An object is a specific instance of a class, occupying actual memory. Think of a class as the blueprint for a house, and objects as the actual houses built from that blueprint. Each object has its own copy of non-static data members but shares the same function code.

Q2: What are the different types of constructors in C++?

Answer: (1) Default constructor — no parameters, initializes with default values. (2) Parameterized constructor — takes arguments to initialize with specific values. (3) Copy constructor — takes a const reference to another object of the same class, creates a duplicate. (4) Move constructor (C++11) — takes an rvalue reference, "steals" resources from a temporary. (5) Delegating constructor (C++11) — calls another constructor of the same class. (6) Converting constructor — single-argument constructor that allows implicit type conversion (prevent with explicit).

Q3: What is the difference between deep copy and shallow copy?

Answer: Shallow copy copies member values as-is. If a member is a pointer, only the pointer value is copied, not the pointed-to data — both objects point to the same memory. Deep copy allocates new memory and copies the actual data. Shallow copy is the compiler's default. Deep copy requires a user-defined copy constructor and assignment operator. Use deep copy when your class manages heap-allocated resources.

Q4: What is the Rule of Three/Five/Zero?

Answer: Rule of Three: If you define any of destructor, copy constructor, or copy assignment operator, define all three (because if one needs custom logic, the others likely do too). Rule of Five (C++11): Add move constructor and move assignment operator. Rule of Zero: Prefer using RAII wrappers (smart pointers, vector, string) so the compiler-generated defaults work correctly — no need to define any special members.

Q5: What is RAII?

Answer: Resource Acquisition Is Initialization. A C++ idiom where resource lifetime is tied to object lifetime. Resources (memory, files, locks, sockets) are acquired in the constructor and released in the destructor. Since destructors are called automatically when objects go out of scope, resources are always properly released, even in the presence of exceptions. Examples: unique_ptr, lock_guard, fstream.

Q6: What is the this pointer?

Answer: this is an implicit pointer available in all non-static member functions, pointing to the object on which the function was called. It's of type ClassName* const (const pointer to the object). Used for: resolving name conflicts between parameters and members, enabling method chaining (return *this), and passing the current object to other functions.

Q7: What is the difference between struct and class in C++?

Answer: The ONLY difference is the default access modifier: struct members are public by default, class members are private by default. The same applies to inheritance — struct inherits publicly by default, class inherits privately. Functionally they're identical. Convention: use struct for simple data aggregates, class for objects with behavior and invariants.

Q8: What is explicit and why is it used?

Answer: explicit prevents implicit type conversions via single-argument constructors. Without it, Foo f = 5; would implicitly call Foo(5), which can lead to unexpected conversions and bugs. With explicit, only direct initialization works: Foo f(5) or Foo f = Foo(5). Best practice: always use explicit for single-parameter constructors unless implicit conversion is intentionally desired.

Q9: When is a destructor called?

Answer: (1) Stack objects — when they go out of scope, (2) heap objects — when delete is called, (3) global/static objects — at program termination, (4) temporaries — at the end of the full expression, (5) array elements — in reverse order when the array is destroyed. For derived classes, the derived destructor runs first, then the base destructor.

Q10: Why should destructors be virtual in base classes?

Answer: When deleting a derived object through a base pointer (Base* p = new Derived(); delete p;), without a virtual destructor, only the base destructor is called — the derived destructor is skipped. This can cause resource leaks. A virtual destructor ensures the correct destructor chain is called via dynamic dispatch. Rule: if a class has ANY virtual function, it must have a virtual destructor.

Q11: What are static members?

Answer: Static data members are shared across all instances of a class — there's only one copy regardless of how many objects exist. Static member functions can be called without an object (ClassName::func()) but cannot access non-static members because they have no this pointer. Static data members must be defined outside the class. Use cases: object counters, shared configuration, utility functions.

Q12: What is const correctness?

Answer: Marking functions as const when they don't modify the object, ensuring that const objects can only call const methods. This is a compile-time contract: int getX() const; promises not to modify any data member. Const correctness prevents accidental mutations, enables the compiler to catch bugs, and allows passing objects by const reference. mutable is an exception for members that need to change even in const context (e.g., caches, mutexes).


Next Module: 02 - Encapsulation & Abstraction — Data hiding, access control, and showing only what's necessary.