Introduction to Object-Oriented Programming

Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around data, or objects, rather than functions and logic. An object can be defined as a data field that has unique attributes and behavior.

Why OOP?

OOP provides several advantages over procedural programming:

  • Modularity: Code can be organized into independent modules (classes)
  • Reusability: Classes can be reused across programs through inheritance
  • Maintainability: Easier to maintain and modify existing code
  • Data Hiding: Implementation details can be hidden from users
  • Flexibility: Polymorphism allows the same function to work with different objects

Core Principles of OOP

Object-Oriented Programming is built around four main principles:

1. Encapsulation

Encapsulation is the mechanism of bundling the data (variables) and the methods (functions) that work on the data into a single unit called a class. It also restricts direct access to some of an object's components.

2. Inheritance

Inheritance allows a class to inherit attributes and methods from another class. This promotes code reusability and establishes a relationship between classes.

3. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common super class. The most common use of polymorphism is when a parent class reference is used to refer to a child class object.

4. Abstraction

Abstraction means hiding the complex implementation details and showing only the necessary features of the object. It helps to reduce programming complexity and effort.

Note: This tutorial assumes you have basic knowledge of C++ syntax and programming concepts. We'll start with the fundamentals and progress to advanced topics.

Basic OOP Terminology

  • Class: A blueprint for creating objects
  • Object: An instance of a class
  • Method: A function defined inside a class
  • Attribute: A variable defined inside a class
  • Constructor: A special method called when an object is created
  • Destructor: A special method called when an object is destroyed

Select a topic from the left sidebar to dive deeper into specific OOP concepts in C++.

Classes and Objects

Class: A class is a user-defined data type that holds its own data members and member functions. It serves as a blueprint for creating objects.
Object: An object is an instance of a class. When a class is defined, no memory is allocated until objects of that class are created.

Understanding the Blueprint Analogy

Think of a class as an architectural blueprint for a house:

  • Class (Blueprint): Defines the structure - number of rooms, dimensions, materials
  • Object (Actual House): The physical house built from the blueprint
  • Multiple Objects: You can build multiple houses from the same blueprint

Basic Class Structure

Here's the complete syntax for defining a class in C++:

class_structure.cpp
class ClassName {
  // Access specifiers
  public:
    // Public members (accessible from anywhere)
    returnType memberFunction1(parameters);
    dataType memberVariable1;

  private:
    // Private members (accessible only within the class)
    returnType memberFunction2(parameters);
    dataType memberVariable2;

  protected:
    // Protected members (accessible within class and derived classes)
    returnType memberFunction3(parameters);
    dataType memberVariable3;
};

Complete Example: Student Class

Let's create a comprehensive Student class to understand all concepts:

student_class_complete.cpp
#include
#include
using namespace std;

class Student {
  private:
    // Private data members (attributes)
    string name;
    int age;
    double gpa;
    string studentId;

  public:
    // Public member functions (methods)
    
    // Setter methods
    void setName(string n) {
      name = n;
    }

    void setAge(int a) {
      if(a >= 0 && a <= 120) { // Validation
        age = a;
      } else {
        cout << "Invalid age!" << endl;
      }
    }

    void setGPA(double g) {
      if(g >= 0.0 && g <= 4.0) { // Validation
        gpa = g;
      } else {
        cout << "Invalid GPA!" << endl;
      }
    }

    void setStudentId(string id) {
      studentId = id;
    }

    // Getter methods
    string getName() {
      return name;
    }

    int getAge() {
      return age;
    }

    double getGPA() {
      return gpa;
    }

    string getStudentId() {
      return studentId;
    }

    // Other member functions
    void displayInfo() {
      cout << "Student ID: " << studentId << endl;
      cout << "Name: " << name << endl;
      cout << "Age: " << age << endl;
      cout << "GPA: " << gpa << endl;
      cout << "-------------------" << endl;
    }

    bool isHonorsStudent() {
      return gpa >= 3.5;
    }
};

int main() {
  // Create student objects
  Student student1;
  Student student2;

  // Set student1 information using setter methods
  student1.setStudentId("S001");
  student1.setName("Alice Johnson");
  student1.setAge(20);
  student1.setGPA(3.8);

  // Set student2 information
  student2.setStudentId("S002");
  student2.setName("Bob Smith");
  student2.setAge(22);
  student2.setGPA(3.2);

  // Display student information
  cout << "Student Information:" << endl;
  student1.displayInfo();
  student2.displayInfo();

  // Check honors status using getter methods
  cout << "Honors Status:" << endl;
  cout << student1.getName() << ": " << (student1.isHonorsStudent() ? "Yes" : "No") << endl;
  cout << student2.getName() << ": " << (student2.isHonorsStudent() ? "Yes" : "No") << endl;

  return 0;
}
Student Information:
Student ID: S001
Name: Alice Johnson
Age: 20
GPA: 3.8
-------------------
Student ID: S002
Name: Bob Smith
Age: 22
GPA: 3.2
-------------------
Honors Status:
Alice Johnson: Yes
Bob Smith: No

Key Concepts Explained

1. Data Members (Attributes)

Data members are variables that store the state of an object:

  • Private: Hidden from outside the class (name, age, gpa, studentId)
  • Public: Accessible from anywhere (rarely used for data members)
  • Protected: Accessible within class and derived classes

2. Member Functions (Methods)

Functions that define the behavior of objects:

  • Setters: Modify private data members with validation
  • Getters: Provide read-only access to private data
  • Other Methods: Perform operations using object data

3. Access Specifiers

Specifier Accessibility When to Use
private Only within the class Data members, internal helper methods
public From anywhere Interface methods, getters/setters
protected Within class and derived classes For inheritance (covered later)

Creating Multiple Objects

You can create as many objects as needed from a single class:

multiple_objects.cpp
#include
using namespace std;

class Car {
  private:
    string brand;
    string model;
    int year;
    double price;

  public:
    void setDetails(string b, string m, int y, double p) {
      brand = b;
      model = m;
      year = y;
      price = p;
    }

    void display() {
      cout << brand << " " << model << " (" << year << ") - $" << price << endl;
    }
};

int main() {
  // Create multiple car objects
  Car car1, car2, car3;

  // Set different details for each car
  car1.setDetails("Toyota", "Camry", 2022, 25000);
  car2.setDetails("Honda", "Civic", 2023, 22000);
  car3.setDetails("Ford", "Mustang", 2021, 35000);

  // Display all cars
  cout << "Car Inventory:" << endl;
  car1.display();
  car2.display();
  car3.display();

  return 0;
}
Car Inventory:
Toyota Camry (2022) - $25000
Honda Civic (2023) - $22000
Ford Mustang (2021) - $35000

Array of Objects

You can create arrays of objects for managing multiple instances efficiently:

object_array.cpp
#include
using namespace std;

class Book {
  public:
    string title;
    string author;
    int pages;

    void display() {
      cout << "'" << title << "' by " << author << " (" << pages << " pages)" << endl;
    }
};

int main() {
  // Create an array of Book objects
  Book library[3];

  // Initialize the books
  library[0].title = "The C++ Programming Language";
  library[0].author = "Bjarne Stroustrup";
  library[0].pages = 1376;

  library[1].title = "Effective Modern C++";
  library[1].author = "Scott Meyers";
  library[1].pages = 334;

  library[2].title = "C++ Primer";
  library[2].author = "Stanley Lippman";
  library[2].pages = 976;

  // Display all books using a loop
  cout << "Library Contents:" << endl;
  for(int i = 0; i < 3; i++) {
    cout << i+1 << ". ";
    library[i].display();
  }

  return 0;
}
Library Contents:
1. 'The C++ Programming Language' by Bjarne Stroustrup (1376 pages)
2. 'Effective Modern C++' by Scott Meyers (334 pages)
3. 'C++ Primer' by Stanley Lippman (976 pages)
Best Practice: Always make data members private and provide public getter and setter methods. This practice, known as encapsulation, protects your data and allows you to add validation logic.
Note: In the next section, we'll learn about constructors and destructors, which are special member functions that handle object initialization and cleanup automatically.

Common Mistakes to Avoid

Warning:
1. Forgetting to initialize objects: Always set initial values for your objects
2. Direct access to private members: Use getters/setters instead
3. Not validating data: Always validate input in setter methods
4. Memory leaks: Clean up dynamically allocated memory (covered later)

Constructors and Destructors

Constructor: A special member function that is automatically called when an object is created. It has the same name as the class and no return type.
Destructor: A special member function that is automatically called when an object is destroyed. It has the same name as the class preceded by a tilde (~) and no return type or parameters.

Why We Need Constructors and Destructors

Think of constructors and destructors as the setup and cleanup crew for your objects:

  • Constructor: Like moving into a new house - you set up furniture, connect utilities
  • Destructor: Like moving out - you clean up, disconnect utilities, return keys
  • Automatic: Both are called automatically - you don't need to call them manually

Types of Constructors

C++ provides several types of constructors for different situations:

Type Purpose When to Use
Default Constructor Creates object with default values When no initial values are provided
Parameterized Constructor Creates object with specific values When you know initial values
Copy Constructor Creates object as copy of another When duplicating existing objects
Move Constructor (C++11) Transfers resources efficiently Advanced optimization

1. Default Constructor

Called when you create an object without any parameters:

default_constructor.cpp
#include
using namespace std;

class Student {
  private:
    string name;
    int age;
    double gpa;

  public:
    // Default Constructor
    Student() {
      name = "Unknown";
      age = 0;
      gpa = 0.0;
      cout << "Default constructor called! Student created with default values." << endl;
    }

    void display() {
      cout << "Name: " << name << ", Age: " << age << ", GPA: " << gpa << endl;
    }
};

int main() {
  cout << "Creating student1 (default constructor):" << endl;
  Student student1; // Default constructor called
  student1.display();

  cout << "Creating student2 (default constructor):" << endl;
  Student student2; // Default constructor called again
  student2.display();

  return 0;
}
Creating student1 (default constructor):
Default constructor called! Student created with default values.
Name: Unknown, Age: 0, GPA: 0
Creating student2 (default constructor):
Default constructor called! Student created with default values.
Name: Unknown, Age: 0, GPA: 0

2. Parameterized Constructor

Called when you create an object with specific values:

parameterized_constructor.cpp
#include
using namespace std;

class Student {
  private:
    string name;
    int age;
    double gpa;

  public:
    // Parameterized Constructor
    Student(string studentName, int studentAge, double studentGPA) {
      name = studentName;
      age = studentAge;
      gpa = studentGPA;
      cout << "Parameterized constructor called for " << name << endl;
    }

    void display() {
      cout << "Name: " << name << ", Age: " << age << ", GPA: " << gpa << endl;
    }
};

int main() {
  cout << "Creating students with parameterized constructor:" << endl;
  
  // Different ways to call parameterized constructor
  Student student1("Alice", 20, 3.8); // Direct initialization
  Student student2 = Student("Bob", 22, 3.5); // Explicit call
  
  student1.display();
  student2.display();

  return 0;
}
Creating students with parameterized constructor:
Parameterized constructor called for Alice
Parameterized constructor called for Bob
Name: Alice, Age: 20, GPA: 3.8
Name: Bob, Age: 22, GPA: 3.5

3. Copy Constructor

Creates a new object as a copy of an existing object:

copy_constructor.cpp
#include
using namespace std;

class Student {
  private:
    string name;
    int age;
    double gpa;

  public:
    // Parameterized Constructor
    Student(string studentName, int studentAge, double studentGPA) {
      name = studentName;
      age = studentAge;
      gpa = studentGPA;
      cout << "Parameterized constructor called for " << name << endl;
    }

    // Copy Constructor
    Student(const Student &original) {
      name = original.name + " (copy)";
      age = original.age;
      gpa = original.gpa;
      cout << "Copy constructor called! Created copy of " << original.name << endl;
    }

    void display() {
      cout << "Name: " << name << ", Age: " << age << ", GPA: " << gpa << endl;
    }

    void setName(string newName) {
      name = newName;
    }
};

int main() {
  cout << "=== Copy Constructor Demo ===" << endl;
  
  // Create original student
  Student original("John", 21, 3.7);
  cout << "Original: ";
  original.display();

  // Different ways to use copy constructor
  Student copy1 = original; // Copy constructor called
  Student copy2(original); // Copy constructor called
  
  cout << "After copying:" << endl;
  cout << "Copy 1: ";
  copy1.display();
  cout << "Copy 2: ";
  copy2.display();

  // Changing copy doesn't affect original
  copy1.setName("Copy1 Modified");
  cout << "After modifying copy1:" << endl;
  cout << "Original: ";
  original.display();
  cout << "Copy 1: ";
  copy1.display();

  return 0;
}
=== Copy Constructor Demo ===
Parameterized constructor called for John
Original: Name: John, Age: 21, GPA: 3.7
Copy constructor called! Created copy of John
Copy constructor called! Created copy of John
After copying:
Copy 1: Name: John (copy), Age: 21, GPA: 3.7
Copy 2: Name: John (copy), Age: 21, GPA: 3.7
After modifying copy1:
Original: Name: John, Age: 21, GPA: 3.7
Copy 1: Name: Copy1 Modified, Age: 21, GPA: 3.7

4. Destructor

Automatically called when an object is destroyed (goes out of scope):

destructor_demo.cpp
#include
using namespace std;

class Student {
  private:
    string name;
    int age;

  public:
    // Constructor
    Student(string n, int a) : name(n), age(a) {
      cout << "Constructor called for " << name << endl;
    }

    // Destructor
    ~Student() {
      cout << "Destructor called for " << name << " - Object destroyed!" << endl;
    }

    void display() {
      cout << "Name: " << name << ", Age: " << age << endl;
    }
};

void demoFunction() {
  cout << "--- Inside demoFunction ---" << endl;
  Student tempStudent("Temporary", 19);
  tempStudent.display();
  cout << "--- Leaving demoFunction ---" << endl;
  // tempStudent's destructor called automatically here
}

int main() {
  cout << "=== Destructor Demo ===" << endl;
  
  { // Inner scope
    cout << "--- Entering inner scope ---" << endl;
    Student student1("Alice", 20);
    student1.display();
    cout << "--- Leaving inner scope ---" << endl;
    // student1's destructor called automatically here
  }

  cout << "--- Back in main scope ---" << endl;
  
  demoFunction();
  
  cout << "--- End of main - all objects will be destroyed ---" << endl;
  return 0;
  // All remaining objects' destructors called here
}
=== Destructor Demo ===
--- Entering inner scope ---
Constructor called for Alice
Name: Alice, Age: 20
--- Leaving inner scope ---
Destructor called for Alice - Object destroyed!
--- Back in main scope ---
--- Inside demoFunction ---
Constructor called for Temporary
Name: Temporary, Age: 19
--- Leaving demoFunction ---
Destructor called for Temporary - Object destroyed!
--- End of main - all objects will be destroyed ---

Constructor Initialization Lists

A more efficient way to initialize class members:

initialization_list.cpp
#include
using namespace std;

class Point {
  private:
    int x, y;
    const int id; // const member
    int &ref; // reference member
  public:
    // Constructor with initialization list
    Point(int xVal, int yVal, int identifier, int &reference)
      : x(xVal), y(yVal), id(identifier), ref(reference) {
      cout << "Point constructor called with id: " << id << endl;
    }

    void display() {
      cout << "Point(" << x << ", " << y << "), id: " << id << ", ref: " << ref << endl;
    }
};

int main() {
  int externalVar = 100;
  Point p1(5, 10, 1, externalVar);
  p1.display();

  externalVar = 200;
  cout << "After changing externalVar: " << endl;
  p1.display();

  return 0;
}
Point constructor called with id: 1
Point(5, 10), id: 1, ref: 100
After changing externalVar:
Point(5, 10), id: 1, ref: 200
Why Use Initialization Lists?
1. Required for const members and references
2. More efficient - avoids default construction + assignment
3. Cleaner code - initialization separated from constructor body

Complete Example: All Constructor Types

all_constructors.cpp
#include
using namespace std;

class Book {
  private:
    string title;
    string author;
    int pages;

  public:
    // Default Constructor
    Book() : title("Unknown"), author("Unknown"), pages(0) {
      cout << "Default constructor called" << endl;
    }

    // Parameterized Constructor
    Book(string t, string a, int p) : title(t), author(a), pages(p) {
      cout << "Parameterized constructor called for '" << title << "'" << endl;
    }

    // Copy Constructor
    Book(const Book &other) : title(other.title + " (copy)"), author(other.author), pages(other.pages) {
      cout << "Copy constructor called" << endl;
    }

    // Destructor
    ~Book() {
      cout << "Destructor called for '" << title << "'" << endl;
    }

    void display() {
      cout << "'" << title << "' by " << author << " (" << pages << " pages)" << endl;
    }
};

int main() {
  cout << "=== All Constructor Types Demo ===" << endl;
  
  Book book1; // Default constructor
  book1.display();

  Book book2("The Great Gatsby", "F. Scott Fitzgerald", 218); // Parameterized
  book2.display();

  Book book3 = book2; // Copy constructor
  book3.display();

  cout << "=== End of main - destructors will be called ===" << endl;
  return 0;
}
=== All Constructor Types Demo ===
Default constructor called
'Unknown' by Unknown (0 pages)
Parameterized constructor called for 'The Great Gatsby'
'The Great Gatsby' by F. Scott Fitzgerald (218 pages)
Copy constructor called
'The Great Gatsby (copy)' by F. Scott Fitzgerald (218 pages)
=== End of main - destructors will be called ===
Destructor called for 'The Great Gatsby (copy)'
Destructor called for 'The Great Gatsby'
Destructor called for 'Unknown'
Key Points to Remember:
1. Constructors are called automatically when objects are created
2. Destructors are called automatically when objects are destroyed
3. Use initialization lists for efficient member initialization
4. Copy constructor creates a separate copy (not a reference)
5. Objects are destroyed in reverse order of creation
Common Mistakes:
1. Forgetting to initialize all members in constructors
2. Not using initialization lists for const/reference members
3. Confusing copy constructor with assignment
4. Trying to call destructors manually (don't do this!)
5. Not handling dynamic memory properly in destructors

Access Modifiers

Access Modifiers: Keywords in C++ that set the accessibility of classes, methods, and data members. They determine which parts of your code can access specific class members.

Why We Need Access Modifiers

Think of access modifiers as security levels for your class members:

  • Public: Like a public park - anyone can access it
  • Private: Like your bedroom - only you can access it
  • Protected: Like a family room - only family members can access it

The Three Access Modifiers

Access Modifier Accessibility Best For
public Accessible from anywhere in the program Interface methods, getters/setters
private Accessible only within the class itself Data members, internal helper methods
protected Accessible within the class and derived classes Members needed for inheritance

1. Public Access Modifier

Public members can be accessed from anywhere in your program:

public_access.cpp
#include
using namespace std;

class Student {
  public:
    // Public data members
    string name;
    int age;

    // Public member function
    void displayInfo() {
      cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
  Student student1;

  // Direct access to public members - allowed!
  student1.name = "Alice";
  student1.age = 20;

  // Access public method
  student1.displayInfo();

  // Can even modify directly (not recommended!)
  student1.age = -5; // No validation - bad!
  cout << "After direct modification: ";
  student1.displayInfo();

  return 0;
}
Name: Alice, Age: 20
After direct modification: Name: Alice, Age: -5
Problem with Public Data Members: Anyone can modify them directly, which can lead to invalid data (like negative age!). This is why we usually keep data members private.

2. Private Access Modifier

Private members can only be accessed within the class itself:

private_access.cpp
#include
using namespace std;

class BankAccount {
  private:
    // Private data members - hidden from outside
    double balance;
    string accountNumber;
    string password;

    // Private helper method - internal use only
    bool isValidAmount(double amount) {
      return amount > 0;
    }

  public:
    string accountHolder;

    // Public methods to access private data
    BankAccount(string holder, string accNum, double initialBalance) {
      accountHolder = holder;
      accountNumber = accNum;
      balance = initialBalance;
      password = "default123";
    }

    void deposit(double amount) {
      if (isValidAmount(amount)) {
        balance += amount;
        cout << "Deposited: $" << amount << endl;
      } else {
        cout << "Invalid deposit amount!" << endl;
      }
    }

    bool withdraw(double amount) {
      if (isValidAmount(amount) && amount <= balance) {
        balance -= amount;
        cout << "Withdrawn: $" << amount << endl;
        return true;
      } else {
        cout << "Withdrawal failed! Insufficient funds or invalid amount." << endl;
        return false;
      }
    }

    double getBalance() {
      return balance;
    }

    string getAccountNumber() {
      return accountNumber;
    }
};

int main() {
  BankAccount myAccount("John Doe", "ACC123456", 1000.0);

  // Public member - accessible
  cout << "Account Holder: " << myAccount.accountHolder << endl;

  // Access private data through public methods - allowed
  cout << "Account Number: " << myAccount.getAccountNumber() << endl;
  cout << "Initial Balance: $" << myAccount.getBalance() << endl;

  // Use public methods to modify private data
  myAccount.deposit(500.0);
  myAccount.withdraw(200.0);
  myAccount.withdraw(2000.0); // Should fail

  cout << "Final Balance: $" << myAccount.getBalance() << endl;

  // These would cause COMPILATION ERRORS (uncomment to test):
  // myAccount.balance = 1000000; // Error: 'balance' is private
  // myAccount.password = "hacked"; // Error: 'password' is private
  // myAccount.isValidAmount(100); // Error: 'isValidAmount' is private

  return 0;
}
Account Holder: John Doe
Account Number: ACC123456
Initial Balance: $1000
Deposited: $500
Withdrawn: $200
Withdrawal failed! Insufficient funds or invalid amount.
Final Balance: $1300
Best Practice: Always make data members private and provide public getter and setter methods. This is called encapsulation and it protects your data from invalid modifications.

3. Protected Access Modifier

Protected members are accessible within the class and by derived classes (inheritance):

protected_access.cpp
#include
using namespace std;

class Vehicle {
  protected:
    // Protected members - accessible in derived classes
    string brand;
    int year;
    double price;

  private:
    // Private member - only accessible in this class
    string vin; // Vehicle Identification Number

  public:
    Vehicle(string b, int y, double p, string v) {
      brand = b;
      year = y;
      price = p;
      vin = v;
    }

    void displayBasicInfo() {
      cout << brand << " (" << year << ") - $" << price << endl;
    }

    string getVIN() {
      return vin;
    }
};

// Derived class (inheritance)
class Car : public Vehicle {
  private:
    int doors;

  public:
    Car(string b, int y, double p, string v, int d) : Vehicle(b, y, p, v) {
      doors = d;
    }

    void displayFullInfo() {
      // Can access protected members from base class
      cout << "Car: " << brand << " (" << year << ")" << endl;
      cout << "Price: $" << price << endl;
      cout << "Doors: " << doors << endl;
      
      // Can access private member through public method
      cout << "VIN: " << getVIN() << endl;
      
      // This would cause COMPILATION ERROR:
      // cout << vin << endl; // Error: 'vin' is private in Vehicle
    }
};

int main() {
  Car myCar("Toyota", 2023, 25000, "1HGCM82633A123456", 4);

  cout << "Basic Info: ";
  myCar.displayBasicInfo();

  cout << "\nFull Info:" << endl;
  myCar.displayFullInfo();

  // These would cause COMPILATION ERRORS:
  // myCar.brand = "Honda"; // Error: 'brand' is protected
  // myCar.price = 30000; // Error: 'price' is protected

  return 0;
}
Basic Info: Toyota (2023) - $25000

Full Info:
Car: Toyota (2023)
Price: $25000
Doors: 4
VIN: 1HGCM82633A123456

Default Access Specifiers

C++ has different default access depending on whether you use class or struct:

default_access.cpp
#include
using namespace std;

class MyClass {
  // Default: PRIVATE (for classes)
  int data1;
  void method1();

  public:
    int data2;
    void method2();
};

struct MyStruct {
  // Default: PUBLIC (for structs)
  int data1;
  void method1() { cout << "Struct method" << endl; }

  private:
    int data2;
    void method2() {}
};

int main() {
  MyClass obj1;
  MyStruct obj2;

  // For class: default is private
  // obj1.data1 = 10; // Error: private
  obj1.data2 = 20; // OK: public

  // For struct: default is public
  obj2.data1 = 30; // OK: public
  obj2.method1(); // OK: public
  // obj2.data2 = 40; // Error: private

  cout << "Class public data: " << obj1.data2 << endl;
  cout << "Struct public data: " << obj2.data1 << endl;

  return 0;
}
Struct method
Class public data: 20
Struct public data: 30

Access Modifiers and Inheritance

Access modifiers interact with inheritance in important ways:

Base Class Access Inheritance Type Access in Derived Class
private Any inheritance Not accessible
protected public inheritance protected
protected protected inheritance protected
protected private inheritance private
public public inheritance public
public protected inheritance protected
public private inheritance private

Complete Example: Employee Management System

employee_system.cpp
#include
using namespace std;

class Employee {
  private:
    // Private data - most secure
    string ssn; // Social Security Number
    double salary;
    string password;

    // Private helper method
    bool isValidSalary(double s) {
      return s >= 0;
    }

  protected:
    // Protected data - accessible to derived classes
    int employeeId;
    string department;

  public:
    // Public data - accessible to everyone
    string name;
    string position;

    Employee(string n, string pos, int id, string dept) {
      name = n;
      position = pos;
      employeeId = id;
      department = dept;
      salary = 0.0;
      ssn = "Unknown";
      password = "default";
    }

    // Public methods to access private data
    void setSalary(double s) {
      if (isValidSalary(s)) {
        salary = s;
        cout << "Salary set to: $" << salary << endl;
      } else {
        cout << "Invalid salary amount!" << endl;
      }
    }

    double getSalary() {
      return salary;
    }

    void setSSN(string s) {
      ssn = s;
      cout << "SSN updated securely" << endl;
    }

    void displayPublicInfo() {
      cout << "Name: " << name << endl;
      cout << "Position: " << position << endl;
      cout << "Department: " << department << endl;
    }

    void displayProtectedInfo() {
      cout << "Employee ID: " << employeeId << endl;
      cout << "Department: " << department << endl;
    }
};

int main() {
  Employee emp("Alice Johnson", "Software Engineer", 1001, "IT");

  cout << "=== Public Information ===" << endl;
  emp.displayPublicInfo();

  cout << "\n=== Protected Information ===" << endl;
  emp.displayProtectedInfo();

  cout << "\n=== Salary Management ===" << endl;
  emp.setSalary(75000.0);
  emp.setSalary(-5000.0); // Invalid - will be rejected
  cout << "Current Salary: $" << emp.getSalary() << endl;

  cout << "\n=== Security Demo ===" << endl;
  emp.setSSN("123-45-6789");

  // These would cause COMPILATION ERRORS:
  // emp.salary = 100000; // Error: private
  // emp.ssn = "hacked"; // Error: private
  // emp.employeeId = 9999; // Error: protected
  // emp.isValidSalary(50000); // Error: private method

  return 0;
}
=== Public Information ===
Name: Alice Johnson
Position: Software Engineer
Department: IT

=== Protected Information ===
Employee ID: 1001
Department: IT

=== Salary Management ===
Salary set to: $75000
Invalid salary amount!
Current Salary: $75000

=== Security Demo ===
SSN updated securely
Summary of Access Modifiers:
1. Public: Use for the interface that users need
2. Private: Use for data and internal implementation details
3. Protected: Use for members that derived classes need to access
4. Default: private for classes, public for structs
Best Practices:
1. Make all data members private by default
2. Use public getter and setter methods for controlled access
3. Use protected only when you specifically need inheritance
4. Always validate data in setter methods
5. Keep the public interface simple and minimal

Encapsulation

Encapsulation: The bundling of data (attributes) and methods (functions) that operate on that data into a single unit called a class, while restricting direct access to some of the object's components.

What is Encapsulation?

Encapsulation is one of the four fundamental principles of Object-Oriented Programming. It's often referred to as data hiding because it protects an object's internal state from unauthorized access and modification.

Think of encapsulation like a capsule medicine - the actual medicine (data) is protected inside the capsule (class), and you can only access it through specific methods (getters/setters).

Why Encapsulation Matters

  • Data Protection: Prevents accidental modification of sensitive data
  • Controlled Access: Allows validation before data modification
  • Flexibility: You can change internal implementation without affecting external code
  • Maintainability: Easier to debug and maintain code
  • Reusability: Encapsulated classes are self-contained and reusable

Implementing Encapsulation in C++

Encapsulation is achieved using access modifiers:

Access Modifier Accessibility Purpose in Encapsulation
private Only within the class Hide implementation details and protect data
public From anywhere Provide controlled interface to interact with object
protected Within class and derived classes Allow inheritance while maintaining some protection

Example: Bank Account with Encapsulation

Let's create a BankAccount class that demonstrates proper encapsulation:

encapsulation_bank_account.cpp
#include
#include
using namespace std;

class BankAccount {
  private:
    // Private data members - hidden from outside world
    string accountNumber;
    string accountHolder;
    double balance;
    double interestRate;

  public:
    // Public constructor - controlled object creation
    BankAccount(string accNum, string holder, double initialBalance) {
      accountNumber = accNum;
      accountHolder = holder;
      // Validation in constructor
      if (initialBalance >= 0) {
        balance = initialBalance;
      } else {
        balance = 0;
        cout << "Warning: Negative balance not allowed. Set to 0." << endl;
      }
      interestRate = 0.02; // Default interest rate
    }

    // Public getter methods - controlled read access
    string getAccountNumber() {
      return accountNumber;
    }

    string getAccountHolder() {
      return accountHolder;
    }

    double getBalance() {
      return balance;
    }

    // Public setter methods - controlled write access with validation
    void setInterestRate(double newRate) {
      if (newRate >= 0 && newRate <= 0.1) { // Validation
        interestRate = newRate;
      } else {
        cout << "Error: Interest rate must be between 0 and 0.1 (10%)" << endl;
      }
    }

    double getInterestRate() {
      return interestRate;
    }

    // Public business methods - controlled operations
    void deposit(double amount) {
      if (amount > 0) {
        balance += amount;
        cout << "Deposited: $" << amount << ". New balance: $" << balance << endl;
      } else {
        cout << "Error: Deposit amount must be positive" << endl;
      }
    }

    bool withdraw(double amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        cout << "Withdrawn: $" << amount << ". New balance: $" << balance << endl;
        return true;
      } else {
        cout << "Error: Insufficient funds or invalid amount" << endl;
        return false;
      }
    }

    void applyInterest() {
      double interest = balance * interestRate;
      balance += interest;
      cout << "Interest applied: $" << interest << ". New balance: $" << balance << endl;
    }

    void displayAccountInfo() {
      cout << "Account Number: " << accountNumber << endl;
      cout << "Account Holder: " << accountHolder << endl;
      cout << "Balance: $" << balance << endl;
      cout << "Interest Rate: " << (interestRate * 100) << "%" << endl;
    }
};

int main() {
  // Create a bank account
  BankAccount myAccount("123456789", "John Doe", 1000.0);

  // Display initial account info
  myAccount.displayAccountInfo();
  cout << endl;

  // Perform transactions (controlled access)
  myAccount.deposit(500.0);
  myAccount.withdraw(200.0);
  myAccount.setInterestRate(0.03);
  myAccount.applyInterest();

  cout << endl;
  cout << "Final Account Status:" << endl;
  myAccount.displayAccountInfo();

  // Try to access private members directly (this would cause compilation error)
  // myAccount.balance = 1000000; // ERROR: 'double BankAccount::balance' is private
  // cout << myAccount.balance; // ERROR: Cannot access private member

  return 0;
}
Account Number: 123456789
Account Holder: John Doe
Balance: $1000
Interest Rate: 2%

Deposited: $500. New balance: $1500
Withdrawn: $200. New balance: $1300
Interest applied: $39. New balance: $1339

Final Account Status:
Account Number: 123456789
Account Holder: John Doe
Balance: $1339
Interest Rate: 3%
Key Points: Notice how all the data members are private, and we only interact with the object through public methods. This prevents direct manipulation of sensitive data like balance.

Benefits Demonstrated in the Example

1. Data Validation

The setter methods validate input before modifying internal state:

  • deposit() checks if amount is positive
  • withdraw() checks for sufficient funds
  • setInterestRate() ensures rate is within reasonable bounds

2. Controlled Access

External code can only:

  • Read account information through getters
  • Modify data through controlled methods with validation
  • Perform business operations through specific methods

3. Implementation Flexibility

We can change the internal implementation without affecting external code. For example, we could:

  • Change how interest is calculated
  • Add transaction logging
  • Modify validation rules

...without changing how other code interacts with BankAccount objects.

Getter and Setter Methods

These are the primary tools for implementing encapsulation:

getters_setters.cpp
class Employee {
  private:
    string name;
    int age;
    double salary;

  public:
    // Getter methods (accessors)
    string getName() { return name; }
    int getAge() { return age; }
    double getSalary() { return salary; }

    // Setter methods (mutators) with validation
    void setName(string n) {
      if (!n.empty()) name = n;
    }
    void setAge(int a) {
      if (a >= 18 && a <= 65) age = a;
    }
    void setSalary(double s) {
      if (s >= 0) salary = s;
    }
};
Best Practice: Always make data members private and provide public getter/setter methods when external access is needed. This gives you complete control over how your class's data is accessed and modified.
Common Mistake: Don't make all data members public for "convenience." This breaks encapsulation and eliminates all the benefits of data protection and controlled access.

Real-World Analogy

Think of a car's engine:

  • Private: The actual engine components (pistons, valves, etc.)
  • Public: The steering wheel, pedals, and gear shift
  • Encapsulation: You don't need to know how the engine works internally to drive the car

This separation allows car manufacturers to improve engine technology without changing how people drive cars.

Inheritance

Inheritance: A fundamental OOP concept that allows a new class (derived class) to inherit properties and behaviors from an existing class (base class), promoting code reusability and establishing hierarchical relationships.

What is Inheritance?

Inheritance enables you to create a new class that is based on an existing class. The new class inherits all the features of the existing class and can add its own unique features. This creates a "is-a" relationship between classes.

Think of inheritance like a family tree: a child inherits characteristics from parents but can also have unique traits of their own.

Why Inheritance Matters

  • Code Reusability: Reuse existing code without rewriting it
  • Hierarchical Classification: Organize classes in a logical hierarchy
  • Extensibility: Extend existing functionality without modifying original code
  • Polymorphism Foundation: Enables runtime polymorphism through function overriding
  • Maintainability: Changes in base class automatically propagate to derived classes

Inheritance Syntax in C++

The basic syntax for inheritance is:

inheritance_syntax.cpp
class BaseClass {
  // base class members
};

class DerivedClass : access-specifier BaseClass {
  // derived class members
};

Types of Inheritance

C++ supports several types of inheritance:

Type Description Syntax
Single Inheritance Derived class inherits from one base class class Derived : public Base
Multiple Inheritance Derived class inherits from multiple base classes class Derived : public Base1, public Base2
Multilevel Inheritance Derived class inherits from another derived class class A → class B → class C
Hierarchical Inheritance Multiple derived classes inherit from one base class class A → class B, class C
Hybrid Inheritance Combination of multiple inheritance types Mix of above types

Access Specifiers in Inheritance

The access specifier determines how base class members are accessible in the derived class:

Base Class Member Public Inheritance Protected Inheritance Private Inheritance
private Not accessible Not accessible Not accessible
protected protected protected private
public public protected private
Note: Public inheritance is the most commonly used and represents a true "is-a" relationship.

Example: Single Inheritance - Vehicle Hierarchy

Let's create a vehicle hierarchy to demonstrate single inheritance:

single_inheritance.cpp
#include
#include
using namespace std;

// Base class
class Vehicle {
  protected:
    string brand;
    string model;
    int year;
    double price;

  public:
    // Constructor
    Vehicle(string b, string m, int y, double p)
      : brand(b), model(m), year(y), price(p) {}

    // Member functions
    void displayInfo() {
      cout << "Brand: " << brand << ", Model: " << model
            << ", Year: " << year << ", Price: $" << price << endl;
    }

    void start() {
      cout << "Vehicle started" << endl;
    }

    void stop() {
      cout << "Vehicle stopped" << endl;
    }
};

// Derived class - Car (inherits from Vehicle)
class Car : public Vehicle {
  private:
    int doors;
    string fuelType;
    bool sunroof;

  public:
    // Constructor - calls base class constructor
    Car(string b, string m, int y, double p,
         int d, string f, bool s)
      : Vehicle(b, m, y, p), doors(d), fuelType(f), sunroof(s) {}

    // Additional member functions specific to Car
    void displayCarInfo() {
      displayInfo(); // Call base class method
      cout << "Doors: " << doors << ", Fuel Type: " << fuelType
            << ", Sunroof: " << (sunroof ? "Yes" : "No") << endl;
    }

    // Override base class method
    void start() {
      cout << "Car engine started with key" << endl;
    }

    // Car-specific method
    void openSunroof() {
      if (sunroof) {
        cout << "Sunroof opened" << endl;
      } else {
        cout << "This car doesn't have a sunroof" << endl;
      }
    }
};

// Another derived class - Motorcycle
class Motorcycle : public Vehicle {
  private:
    string type; // sport, cruiser, etc.
    bool hasSidecar;

  public:
    Motorcycle(string b, string m, int y, double p,
         string t, bool sidecar)
      : Vehicle(b, m, y, p), type(t), hasSidecar(sidecar) {}

    // Override base class method
    void start() {
      cout << "Motorcycle started with kick-start" << endl;
    }

    void displayMotorcycleInfo() {
      displayInfo();
      cout << "Type: " << type << ", Sidecar: "
            << (hasSidecar ? "Yes" : "No") << endl;
    }

    // Motorcycle-specific method
    void wheelie() {
      cout << "Performing a wheelie!" << endl;
    }
};

int main() {
  cout << "=== Vehicle Inheritance Demo ===" << endl << endl;

  // Create a Car object
  Car myCar("Toyota", "Camry", 2023, 25000.0, 4, "Gasoline", true);
  cout << "Car Information:" << endl;
  myCar.displayCarInfo();
  myCar.start();
  myCar.openSunroof();
  myCar.stop();

  cout << endl << "-------------------" << endl << endl;

  // Create a Motorcycle object
  Motorcycle myBike("Harley-Davidson", "Sportster", 2022, 15000.0, "Cruiser", false);
  cout << "Motorcycle Information:" << endl;
  myBike.displayMotorcycleInfo();
  myBike.start();
  myBike.wheelie();
  myBike.stop();

  return 0;
}
=== Vehicle Inheritance Demo ===

Car Information:
Brand: Toyota, Model: Camry, Year: 2023, Price: $25000
Doors: 4, Fuel Type: Gasoline, Sunroof: Yes
Car engine started with key
Sunroof opened
Vehicle stopped

-------------------

Motorcycle Information:
Brand: Harley-Davidson, Model: Sportster, Year: 2022, Price: $15000
Type: Cruiser, Sidecar: No
Motorcycle started with kick-start
Performing a wheelie!
Vehicle stopped

Multiple Inheritance Example

C++ supports inheriting from multiple base classes:

multiple_inheritance.cpp
#include
#include
using namespace std;

class Printable {
  public:
    virtual void print() = 0; // Pure virtual function
};

class Storable {
  public:
    virtual void save() = 0; // Pure virtual function
};

class Document : public Printable, public Storable {
  private:
    string title;
    string content;

  public:
    Document(string t, string c) : title(t), content(c) {}

    // Implement Printable interface
    void print() override {
      cout << "Printing Document: " << title << endl;
      cout << "Content: " << content << endl;
    }

    // Implement Storable interface
    void save() override {
      cout << "Saving Document: " << title << " to database" << endl;
    }

    // Document-specific method
    void edit(string newContent) {
      content = newContent;
      cout << "Document edited" << endl;
    }
};

int main() {
  Document doc("C++ Inheritance Guide", "Inheritance is a powerful OOP feature...");

  // Use methods from both base classes
  doc.print();
  doc.save();
  doc.edit("Updated content about inheritance...");
  doc.print();

  return 0;
}
Printing Document: C++ Inheritance Guide
Content: Inheritance is a powerful OOP feature...
Saving Document: C++ Inheritance Guide to database
Document edited
Printing Document: C++ Inheritance Guide
Content: Updated content about inheritance...

Constructor and Destructor Order in Inheritance

Understanding the order of constructor and destructor calls is crucial:

constructor_order.cpp
#include
using namespace std;

class Base {
  public:
    Base() { cout << "Base constructor called" << endl; }
    ~Base() { cout << "Base destructor called" << endl; }
};

class Derived : public Base {
  public:
    Derived() { cout << "Derived constructor called" << endl; }
    ~Derived() { cout << "Derived destructor called" << endl; }
};

int main() {
  cout << "Creating Derived object:" << endl;
  Derived d;
  cout << "Destroying Derived object:" << endl;
  return 0;
}
Creating Derived object:
Base constructor called
Derived constructor called
Destroying Derived object:
Derived destructor called
Base destructor called
Constructor Order: Base class constructors are called before derived class constructors.
Destructor Order: Derived class destructors are called before base class destructors.

Function Overriding

Derived classes can override base class functions to provide specialized behavior:

function_override.cpp
class Animal {
  public:
    virtual void speak() {
      cout << "Animal speaks" << endl;
    }
};

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

class Cat : public Animal {
  public:
    void speak() override {
      cout << "Meow!" << endl;
    }
};
Important: Use the virtual keyword in the base class and override keyword in derived classes for proper function overriding and polymorphism.

Best Practices for Inheritance

  • Use public inheritance for "is-a" relationships
  • Prefer composition over inheritance when "has-a" relationship makes more sense
  • Make destructors virtual in base classes when using polymorphism
  • Use protected members sparingly - they break encapsulation to some extent
  • Avoid deep inheritance hierarchies - they can be hard to maintain
  • Use abstract base classes to define interfaces
Real-World Application: Inheritance is widely used in GUI frameworks, game development, and enterprise applications to create hierarchical relationships between objects while maximizing code reuse.

Polymorphism

Polymorphism: A Greek word meaning "many forms." In OOP, it allows objects of different classes to be treated as objects of a common superclass, and enables the same operation to behave differently on different classes.

What is Polymorphism?

Polymorphism is the ability of different objects to respond to the same message (method call) in different ways. It's like having a universal remote control that works with different devices - pressing the "power" button turns on a TV, stereo, or game console, but each device implements "power on" differently.

Simple Analogy: Think of a "shape" class. Different shapes (circle, square, triangle) can all "draw" themselves, but each does it differently. Polymorphism lets you treat all shapes uniformly while each maintains its unique behavior.

Why Polymorphism Matters

  • Code Flexibility: Write code that works with general types rather than specific implementations
  • Extensibility: Add new classes without modifying existing code
  • Maintainability: Reduce complex conditional logic
  • Real-world Modeling: Better represents real-world relationships
  • Interface Simplicity: Provides a consistent interface for different objects

Types of Polymorphism in C++

C++ supports two main types of polymorphism:

Type Description Implementation When to Use
Compile-time Polymorphism Resolved during compilation Function Overloading, Operator Overloading When behavior is known at compile time
Runtime Polymorphism Resolved during program execution Virtual Functions, Function Overriding When behavior depends on actual object type

1. Compile-time Polymorphism (Static Binding)

Also called early binding, this is resolved by the compiler at compile time.

Function Overloading

Multiple functions with the same name but different parameters:

function_overloading.cpp
#include
using namespace std;

class Calculator {
  public:
    // Same function name, different parameters
    int add(int a, int b) {
      cout << "Adding two integers: ";
      return a + b;
    }

    double add(double a, double b) {
      cout << "Adding two doubles: ";
      return a + b;
    }

    int add(int a, int b, int c) {
      cout << "Adding three integers: ";
      return a + b + c;
    }

    string add(string a, string b) {
      cout << "Concatenating strings: ";
      return a + b;
    }
};

int main() {
  Calculator calc;

  cout << calc.add(5, 10) << endl;
  cout << calc.add(3.14, 2.71) << endl;
  cout << calc.add(1, 2, 3) << endl;
  cout << calc.add("Hello, ", "World!") << endl;

  return 0;
}
Adding two integers: 15
Adding two doubles: 5.85
Adding three integers: 6
Concatenating strings: Hello, World!
How it works: The compiler determines which function to call based on the number and types of arguments at compile time.

2. Runtime Polymorphism (Dynamic Binding)

Resolved during program execution using virtual functions and pointers/references.

Virtual Functions and Function Overriding

The cornerstone of runtime polymorphism in C++:

runtime_polymorphism.cpp
#include
#include
using namespace std;

// Base class
class Animal {
  protected:
    string name;
    string sound;

  public:
    Animal(string n, string s) : name(n), sound(s) {}

    // Virtual function - can be overridden by derived classes
    virtual void speak() {
      cout << name << " says: " << sound << endl;
    }

    // Virtual function for movement
    virtual void move() {
      cout << name << " is moving" << endl;
    }

    // Virtual destructor - IMPORTANT for polymorphism
    virtual ~Animal() {
      cout << "Animal destructor: " << name << endl;
    }
};

// Derived class 1
class Dog : public Animal {
  private:
    string breed;

  public:
    Dog(string n, string b) : Animal(n, "Woof"), breed(b) {}

    // Override base class function
    void speak() override {
      cout << name << " the " << breed << " says: " << sound << "! " << sound << "!" << endl;
    }

    // Override movement
    void move() override {
      cout << name << " is running happily with its tail wagging" << endl;
    }

    // Dog-specific method
    void fetch() {
      cout << name << " is fetching the ball!" << endl;
    }
};

// Derived class 2
class Cat : public Animal {
  private:
    int lives;

  public:
    Cat(string n, int l) : Animal(n, "Meow"), lives(l) {}

    // Override base class function
    void speak() override {
      cout << name << " purrs and says: " << sound << "..." << endl;
    }

    // Override movement
    void move() override {
      cout << name << " is gracefully walking with " << lives << " lives remaining" << endl;
    }

    // Cat-specific method
    void climb() {
      cout << name << " is climbing the tree!" << endl;
    }
};

// Derived class 3
class Bird : public Animal {
  private:
    double wingspan;

  public:
    Bird(string n, double w) : Animal(n, "Chirp"), wingspan(w) {}

    // Override base class function
    void speak() override {
      cout << name << " sings: " << sound << " ♫ " << sound << " ♫" << endl;
    }

    // Override movement
    void move() override {
      cout << name << " is flying with a wingspan of " << wingspan << " meters" << endl;
    }

    // Bird-specific method
    void fly() {
      cout << name << " is soaring through the sky!" << endl;
    }
};

// Function that demonstrates polymorphism
void animalConcert(Animal* animals[], int count) {
  cout << "=== Animal Concert ===" << endl;
  for (int i = 0; i < count; i++) {
    animals[i]->speak();
    animals[i]->move();
    cout << "---" << endl;
  }
}

int main() {
  // Create different animal objects
  Dog dog("Buddy", "Golden Retriever");
  Cat cat("Whiskers", 9);
  Bird bird("Tweety", 0.5);

  // Demonstrate polymorphism using base class pointers
  Animal* animals[] = {&dog, &cat, &bird};
  animalConcert(animals, 3);

  cout << endl << "=== Direct Method Calls ===" << endl;
  // Direct calls to specific methods
  dog.fetch();
  cat.climb();
  bird.fly();

  return 0;
}
=== Animal Concert ===
Buddy the Golden Retriever says: Woof! Woof!
Buddy is running happily with its tail wagging
---
Whiskers purrs and says: Meow...
Whiskers is gracefully walking with 9 lives remaining
---
Tweety sings: Chirp ♫ Chirp ♫
Tweety is flying with a wingspan of 0.5 meters
---

=== Direct Method Calls ===
Buddy is fetching the ball!
Whiskers is climbing the tree!
Tweety is soaring through the sky!

How Virtual Functions Work

Virtual functions enable runtime polymorphism through a mechanism called vtable (virtual table) and vptr (virtual pointer):

  • vtable: A table of function pointers created for each class with virtual functions
  • vptr: A hidden pointer in each object that points to its class's vtable
  • When a virtual function is called, the program uses vptr to find the correct function in vtable
vtable_explanation.cpp
// Conceptual representation - not actual code
class Animal {
  virtual void speak(); // Creates vtable entry
  virtual ~Animal(); // Creates vtable entry
};

// Animal vtable: [Animal::speak, Animal::~Animal]
// Dog vtable: [Dog::speak, Dog::~Dog]
// Cat vtable: [Cat::speak, Cat::~Cat]

Abstract Classes and Pure Virtual Functions

Sometimes you want to define an interface without implementation:

abstract_classes.cpp
#include
#include
using namespace std;

// Abstract class - cannot be instantiated
class Shape {
  protected:
    string color;

  public:
    Shape(string c) : color(c) {}

    // Pure virtual function - must be implemented by derived classes
    virtual double area() = 0;

    // Pure virtual function
    virtual double perimeter() = 0;

    // Virtual function with implementation
    virtual void display() {
      cout << "Shape color: " << color << endl;
    }

    // Virtual destructor
    virtual ~Shape() {}
};

class Circle : public Shape {
  private:
    double radius;

  public:
    Circle(string c, double r) : Shape(c), radius(r) {}

    // Implement pure virtual functions
    double area() override {
      return 3.14159 * radius * radius;
    }

    double perimeter() override {
      return 2 * 3.14159 * radius;
    }

    void display() override {
      cout << "Circle - Color: " << color << ", Radius: " << radius
            << ", Area: " << area() << ", Perimeter: " << perimeter() << endl;
    }
};

class Rectangle : public Shape {
  private:
    double width, height;

  public:
    Rectangle(string c, double w, double h)
      : Shape(c), width(w), height(h) {}

    // Implement pure virtual functions
    double area() override {
      return width * height;
    }

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

    void display() override {
      cout << "Rectangle - Color: " << color << ", Width: " << width
            << ", Height: " << height << ", Area: " << area()
            << ", Perimeter: " << perimeter() << endl;
    }
};

int main() {
  // Cannot create Shape object - it's abstract
  // Shape s("red"); // ERROR!

  // But we can use Shape pointers
  vector shapes;
  shapes.push_back(new Circle("Red", 5.0));
  shapes.push_back(new Rectangle("Blue", 4.0, 6.0));
  shapes.push_back(new Circle("Green", 3.0));

  cout << "=== Shape Information ===" << endl;
  for (Shape* shape : shapes) {
    shape->display();
  }

  // Calculate total area using polymorphism
  double totalArea = 0;
  for (Shape* shape : shapes) {
    totalArea += shape->area();
  }
  cout << "Total area of all shapes: " << totalArea << endl;

  // Clean up memory
  for (Shape* shape : shapes) {
    delete shape;
  }

  return 0;
}
=== Shape Information ===
Circle - Color: Red, Radius: 5, Area: 78.5397, Perimeter: 31.4159
Rectangle - Color: Blue, Width: 4, Height: 6, Area: 24, Perimeter: 20
Circle - Color: Green, Radius: 3, Area: 28.2743, Perimeter: 18.8495
Total area of all shapes: 130.814

Polymorphism in Real-World Applications

1. GUI Systems

Different UI elements (buttons, textboxes, checkboxes) all inherit from a common "Widget" class but implement their own draw() and handleClick() methods.

2. Game Development

Different game characters (players, enemies, NPCs) inherit from "GameEntity" but have different update() and render() behaviors.

3. Plugin Architectures

Different plugins implement the same interface but provide different functionality.

Best Practices for Polymorphism

Always make destructors virtual in base classes when you have virtual functions. This ensures proper cleanup of derived class objects when deleted through base class pointers.
Don't call virtual functions from constructors or destructors. The object may not be fully constructed or already partially destroyed, leading to unexpected behavior.
  • Use override keyword to explicitly indicate function overriding
  • Use final keyword to prevent further overriding
  • Prefer references over pointers when using polymorphism to avoid memory management issues
  • Use abstract classes to define clear interfaces
  • Consider performance implications - virtual functions have slight overhead

Common Polymorphism Pitfalls

common_pitfalls.cpp
class Base {
  public:
    // WRONG: Non-virtual destructor
    ~Base() {} // Memory leak if derived class has resources
    
    // CORRECT: Virtual destructor
    virtual ~Base() {}
};

class Derived : public Base {
  public:
    // WRONG: Forgetting override keyword
    void someFunction() {} // Is this overriding?
    
    // CORRECT: Using override keyword
    void someFunction() override {}
};
Remember: Polymorphism is about behavior, not just syntax. The real power comes from designing your classes so that client code can work with base class interfaces without knowing the specific derived types.

Abstraction

Abstraction: The process of hiding complex implementation details and showing only the essential features of an object. It focuses on what an object does rather than how it does it.

What is Abstraction?

Abstraction is about simplifying complex reality by modeling classes appropriate to the problem, and working at the most relevant level of inheritance for a particular aspect of the problem.

Think of abstraction like driving a car: You don't need to know how the engine works internally. You just need to know how to use the steering wheel, pedals, and gear shift. The complex mechanics are abstracted away from you.

Key Insight: Abstraction is about creating a simple model that represents the complex reality, focusing on the essential characteristics while ignoring irrelevant details.

Why Abstraction Matters

  • Reduces Complexity: Hides unnecessary details from the user
  • Increases Reusability: Abstract components can be reused in different contexts
  • Improves Maintainability: Changes to implementation don't affect the interface
  • Enhances Security: Internal data and implementation are protected
  • Promotes Modularity: Systems can be divided into manageable, abstract components

Abstraction vs. Encapsulation

While often confused, abstraction and encapsulation are distinct concepts:

Aspect Abstraction Encapsulation
Focus Hiding complexity and showing essentials Bundling data and methods together
Purpose Solve problems at design level Implement abstraction at code level
Implementation Through abstract classes and interfaces Through access modifiers (private, public)
Analogy Car dashboard (what you see) Car engine cover (protection mechanism)

Implementing Abstraction in C++

C++ provides several ways to implement abstraction:

Method Description When to Use
Abstract Classes Classes with pure virtual functions When you want to define an interface with some implementation
Interfaces Classes with only pure virtual functions When you want to define a pure contract
Access Modifiers Using private/protected to hide implementation For data hiding and implementation hiding
Header Files Separating interface (.h) from implementation (.cpp) For large projects and library development

Abstract Classes and Pure Virtual Functions

Abstract classes cannot be instantiated and serve as blueprints for other classes:

abstract_classes.cpp
#include
#include
#include
using namespace std;

// Abstract class - cannot create objects of this class
class Shape {
  protected:
    string name;
    string color;

  public:
    Shape(string n, string c) : name(n), color(c) {
      cout << "Creating shape: " << name << endl;
    }

    // Pure virtual function - MUST be implemented by derived classes
    virtual double calculateArea() = 0;

    // Pure virtual function
    virtual double calculatePerimeter() = 0;

    // Virtual function with implementation - can be overridden
    virtual void displayInfo() {
      cout << "Shape: " << name << ", Color: " << color << endl;
    }

    // Virtual destructor - crucial for proper cleanup
    virtual ~Shape() {
      cout << "Destroying shape: " << name << endl;
    }

    // Regular member function
    void setColor(string newColor) {
      color = newColor;
    }

    string getColor() const {
      return color;
    }
};

// Concrete class - implements all pure virtual functions
class Circle : public Shape {
  private:
    double radius;

  public:
    Circle(string n, string c, double r)
      : Shape(n, c), radius(r) {}

    // Implement pure virtual functions
    double calculateArea() override {
      return 3.14159 * radius * radius;
    }

    double calculatePerimeter() override {
      return 2 * 3.14159 * radius;
    }

    // Override the display function
    void displayInfo() override {
      cout << "Circle - Name: " << name << ", Color: " << color
            << ", Radius: " << radius << ", Area: " << calculateArea()
            << ", Circumference: " << calculatePerimeter() << endl;
    }

    // Circle-specific method
    double getDiameter() {
      return 2 * radius;
    }
};

class Rectangle : public Shape {
  private:
    double width, height;

  public:
    Rectangle(string n, string c, double w, double h)
      : Shape(n, c), width(w), height(h) {}

    // Implement pure virtual functions
    double calculateArea() override {
      return width * height;
    }

   &td>double calculatePerimeter() override {
      return 2 * (width + height);
    }

    void displayInfo() override {
      cout << "Rectangle - Name: " << name << ", Color: " << color
            << ", Width: " << width << ", Height: " << height
            << ", Area: " << calculateArea()
            << ", Perimeter: " << calculatePerimeter() << endl;
    }

    // Rectangle-specific method
    bool isSquare() {
      return width == height;
    }
};

class Triangle : public Shape {
  private:
    double base, height, side1, side2;

  public:
    Triangle(string n, string c, double b, double h, double s1, double s2)
      : Shape(n, c), base(b), height(h), side1(s1), side2(s2) {}

    double calculateArea() override {
      return 0.5 * base * height;
    }

    double calculatePerimeter() override {
      return base + side1 + side2;
    }

    void displayInfo() override {
      cout << "Triangle - Name: " << name << ", Color: " << color
            << ", Base: " << base << ", Height: " << height
            << ", Area: " << calculateArea()
            << ", Perimeter: " << calculatePerimeter() << endl;
    }
};

int main() {
  cout << "=== Abstraction with Shapes ===" << endl << endl;

  // Cannot create object of abstract class
  // Shape s("Abstract", "Red"); // ERROR!

  // But we can use pointers to abstract class
  vector shapes;
  shapes.push_back(new Circle("Sun", "Yellow", 10.0));
  shapes.push_back(new Rectangle("Door", "Brown", 4.0, 8.0));
  shapes.push_back(new Triangle("Roof", "Red", 6.0, 4.0, 5.0, 5.0));
  shapes.push_back(new Circle("Moon", "White", 5.0));

  // Use abstraction - we don't know the concrete types, just the interface
  cout << "Displaying all shapes:" << endl;
  for (Shape* shape : shapes) {
    shape->displayInfo();
  }

  // Calculate total area using abstraction
  double totalArea = 0;
  for (Shape* shape : shapes) {
    totalArea += shape->calculateArea();
  }
  cout << endl << "Total area of all shapes: " << totalArea << endl;

  // Change color using abstraction
  shapes[0]->setColor("Orange");
  cout << endl << "After color change:" << endl;
  shapes[0]->displayInfo();

  // Clean up memory
  for (Shape* shape : shapes) {
    delete shape;
  }

  return 0;
}
=== Abstraction with Shapes ===

Creating shape: Sun
Creating shape: Door
Creating shape: Roof
Creating shape: Moon
Displaying all shapes:
Circle - Name: Sun, Color: Yellow, Radius: 10, Area: 314.159, Circumference: 62.8318
Rectangle - Name: Door, Color: Brown, Width: 4, Height: 8, Area: 32, Perimeter: 24
Triangle - Name: Roof, Color: Red, Base: 6, Height: 4, Area: 12, Perimeter: 16
Circle - Name: Moon, Color: White, Radius: 5, Area: 78.5397, Circumference: 31.4159

Total area of all shapes: 436.699

After color change:
Circle - Name: Sun, Color: Orange, Radius: 10, Area: 314.159, Circumference: 62.8318
Destroying shape: Sun
Destroying shape: Door
Destroying shape: Roof
Destroying shape: Moon

Interfaces in C++

In C++, interfaces are implemented using abstract classes with only pure virtual functions:

interfaces.cpp
#include
#include
using namespace std;

// Interface - only pure virtual functions
class IVehicle {
  public:
    virtual void start() = 0;
    virtual void stop() = 0;
    virtual void accelerate(double speed) = 0;
    virtual string getInfo() = 0;
    virtual ~IVehicle() {}
};

class Car : public IVehicle {
  private:
    string brand;
    string model;
    double currentSpeed;

  public:
    Car(string b, string m) : brand(b), model(m), currentSpeed(0) {}

    // Implement interface methods
    void start() override {
      cout << brand << " " << model << " engine started with key" << endl;
    }

    void stop() override {
      currentSpeed = 0;
      cout << brand << " " << model << " has stopped" << endl;
    }

    void accelerate(double speed) override {
      currentSpeed += speed;
      cout << brand << " " << model << " accelerating to " << currentSpeed << " km/h" << endl;
    }

    string getInfo() override {
      return brand + " " + model + " - Current speed: " + to_string(currentSpeed) + " km/h";
    }
};

class Bicycle : public IVehicle {
  private:
    string type;
    double currentSpeed;

  public:
    Bicycle(string t) : type(t), currentSpeed(0) {}

    void start() override {
      cout << type << " bicycle ready to pedal" << endl;
    }

    void stop() override {
      currentSpeed = 0;
      cout << type << " bicycle stopped" << endl;
    }

    void accelerate(double speed) override {
      currentSpeed += speed;
      cout << type << " bicycle pedaling at " << currentSpeed << " km/h" << endl;
    }

    string getInfo() override {
      return type + " bicycle - Current speed: " + to_string(currentSpeed) + " km/h";
    }
};

// Function that works with any vehicle through abstraction
void testVehicle(IVehicle* vehicle) {
  cout << "=== Testing Vehicle ===" << endl;
  cout << "Initial: " << vehicle->getInfo() << endl;
  vehicle->start();
  vehicle->accelerate(50.0);
  vehicle->accelerate(30.0);
  cout << "During operation: " << vehicle->getInfo() << endl;
  vehicle->stop();
  cout << "Final: " << vehicle->getInfo() << endl << endl;
}

int main() {
  Car car("Toyota", "Camry");
  Bicycle bicycle("Mountain");

  // Both objects can be used through the same interface
  testVehicle(&car);
  testVehicle(&bicycle);

  return 0;
}
=== Testing Vehicle ===
Initial: Toyota Camry - Current speed: 0 km/h
Toyota Camry engine started with key
Toyota Camry accelerating to 50 km/h
Toyota Camry accelerating to 80 km/h
During operation: Toyota Camry - Current speed: 80 km/h
Toyota Camry has stopped
Final: Toyota Camry - Current speed: 0 km/h

=== Testing Vehicle ===
Initial: Mountain bicycle - Current speed: 0 km/h
Mountain bicycle ready to pedal
Mountain bicycle pedaling at 50 km/h
Mountain bicycle pedaling at 80 km/h
During operation: Mountain bicycle - Current speed: 80 km/h
Mountain bicycle stopped
Final: Mountain bicycle - Current speed: 0 km/h

Levels of Abstraction

Abstraction can be implemented at different levels:

1. Data Abstraction

Hiding complex data structures and providing simple interfaces:

data_abstraction.cpp
class BankAccount {
  private:
    string accountNumber;
    double balance;
    vector transactionHistory;

  public:
    // Simple interface hides complex data management
    bool deposit(double amount);
    bool withdraw(double amount);
    double getBalance();
    vector getRecentTransactions(int count);
};

2. Control Abstraction

Hiding complex control flow and algorithms:

control_abstraction.cpp
class DatabaseManager {
  public:
    // Simple method hides complex database operations
    bool saveUser(const User& user) {
      // Hidden complexity: connection pooling, transaction management,
      // error handling, SQL generation, etc.

      return executeSaveOperation(user);
    }
};

Real-World Abstraction Examples

1. Operating System APIs

You use open(), read(), write() functions without knowing how the file system works internally.

2. Database Connectivity

You use SQL commands without knowing how the database stores data on disk.

3. Graphics Libraries

You call drawCircle() without knowing the pixel-level rendering algorithms.

Benefits of Abstraction

Design Benefit: Abstraction allows you to focus on high-level design without getting bogged down in implementation details early in the development process.
  • Separation of Concerns: Different teams can work on different abstraction levels
  • Easier Testing: You can test interfaces without complete implementations
  • Future-Proofing: Implementation can change without affecting clients
  • Team Collaboration: Clear contracts between different system components
  • Code Understanding: Easier to understand high-level architecture

Best Practices for Abstraction

Avoid Over-Abstraction: Don't create abstractions that are more complex than the problem they're solving. Sometimes simple, concrete implementations are better.
  • Define clear interfaces that are easy to understand and use
  • Keep abstractions focused on a single responsibility
  • Use meaningful names that clearly indicate purpose
  • Document expected behavior of abstract methods
  • Follow the Liskov Substitution Principle - derived classes should be substitutable for base classes
  • Test abstractions thoroughly with different implementations

Common Abstraction Mistakes

abstraction_mistakes.cpp
// WRONG: Leaking implementation details
class BadDatabase {
  public:
    void connectUsingMySQLProtocol(); // Too specific!
    void saveToBinaryFile(); // Implementation detail!
};

// CORRECT: Proper abstraction
class GoodDatabase {
  public:
    bool connect();
    bool save(const Data& data);
};
Remember: The goal of abstraction is to make complex systems understandable and manageable. A good abstraction provides the right level of detail for the task at hand, hiding what's not immediately relevant while exposing what's necessary.

Abstraction in Large Systems

In enterprise applications, abstraction is used at multiple levels:

  • Presentation Layer: UI components abstracting business logic
  • Business Layer: Services abstracting domain logic
  • Data Access Layer: Repositories abstracting database operations
  • Infrastructure Layer: Abstractions for external services

This layered abstraction allows teams to work independently and technologies to be swapped without affecting the entire system.

Function Overloading

Function Overloading: A feature in C++ that allows multiple functions to have the same name but different parameters. The compiler distinguishes between them based on the number, type, and order of parameters.

What is Function Overloading?

Function overloading is a form of compile-time polymorphism (static polymorphism) that enables you to create multiple functions with the same name to perform similar operations on different types of data.

Think of function overloading like a Swiss Army knife: the same tool (function name) can perform different operations (implementations) depending on which attachment (parameters) you use.

Key Insight: Function overloading makes your code more readable and intuitive. Instead of having addIntegers(), addDoubles(), addStrings(), you can simply have multiple add() functions.

Why Function Overloading Matters

  • Code Readability: Same logical operation uses the same function name
  • Flexibility: Functions can handle different data types seamlessly
  • Maintainability: Related functionality is grouped under one name
  • Intuitive API Design: Users don't need to remember multiple function names
  • Type Safety: Compiler ensures correct function is called based on parameters

How Function Overloading Works

The compiler uses a process called name mangling to create unique names for overloaded functions based on their parameter lists.

Function Signature Mangled Name (Conceptual) Description
void print(int) print_int Function taking integer parameter
void print(double) print_double Function taking double parameter
void print(string) print_string Function taking string parameter

Basic Function Overloading Examples

1. Overloading by Parameter Types

basic_overloading.cpp
#include
#include
using namespace std;

// Overloaded print functions
void print(int value) {
  cout << "Integer: " << value << endl;
}

void print(double value) {
  cout << "Double: " << value << endl;
}

void print(const string& value) {
  cout << "String: " << value << endl;
}

void print(char value) {
  cout << "Character: '" << value << "'" << endl;
}

void print(bool value) {
  cout << "Boolean: " << (value ? "true" : "false") << endl;
}

int main() {
  cout << "=== Basic Function Overloading ===" << endl;
  
  print(42); // Calls print(int)
  print(3.14159); // Calls print(double)
  print("Hello"); // Calls print(const string&)
  print('A'); // Calls print(char)
  print(true); // Calls print(bool)
  
  return 0;
}
=== Basic Function Overloading ===
Integer: 42
Double: 3.14159
String: Hello
Character: 'A'
Boolean: true

2. Overloading by Number of Parameters

parameter_count.cpp
#include
using namespace std;

class Calculator {
  public:
    // Add two integers
    int add(int a, int b) {
      cout << "Adding two integers: ";
      return a + b;
    }

    // Add three integers
    int add(int a, int b, int c) {
      cout << "Adding three integers: ";
      return a + b + c;
    }

    // Add four integers
    int add(int a, int b, int c, int d) {
      cout << "Adding four integers: ";
      return a + b + c + d;
    }

    // Add two doubles
    double add(double a, double b) {
      cout << "Adding two doubles: ";
      return a + b;
    }

    // Add three doubles
    double add(double a, double b, double c) {
      cout << "Adding three doubles: ";
      return a + b + c;
    }
};

int main() {
  Calculator calc;
  
  cout << "=== Overloading by Parameter Count ===" << endl;
  cout << calc.add(5, 10) << endl;
  cout << calc.add(1, 2, 3) << endl;
  cout << calc.add(1, 2, 3, 4) << endl;
  cout << calc.add(1.5, 2.5) << endl;
  cout << calc.add(1.1, 2.2, 3.3) << endl;
  
  return 0;
}
=== Overloading by Parameter Count ===
Adding two integers: 15
Adding three integers: 6
Adding four integers: 10
Adding two doubles: 4
Adding three doubles: 6.6

Advanced Function Overloading

1. Overloading with Different Parameter Orders

parameter_order.cpp
#include
#include
using namespace std;

class Message {
  public:
    // Different parameter orders create different signatures
    void display(string text, int times) {
      cout << "Displaying string " << times << " times:" << endl;
      for (int i = 0; i < times; i++) {
        cout << text << endl;
      }
    }

    void display(int number, string label) {
      cout << label << ": " << number << endl;
    }

    void display(double value, string unit, bool showUnit) {
      if (showUnit) {
        cout << "Value: " << value << " " << unit << endl;
      } else {
        cout << "Value: " << value << endl;
      }
    }
};

int main() {
  Message msg;
  
  cout << "=== Overloading by Parameter Order ===" << endl;
  msg.display("Hello", 3);
  cout << endl;
  msg.display(42, "Answer");
  msg.display(98.6, "F", true);
  msg.display(98.6, "F", false);
  
  return 0;
}
=== Overloading by Parameter Order ===
Displaying string 3 times:
Hello
Hello
Hello

Answer: 42
Value: 98.6 F
Value: 98.6

2. Overloading Constructors

constructor_overloading.cpp
#include
#include
using namespace std;

class Student {
  private:
    string name;
    int age;
    double gpa;
    string major;

  public:
    // Default constructor
    Student() : name("Unknown"), age(0), gpa(0.0), major("Undeclared") {
      cout << "Default constructor called" << endl;
    }

    // Constructor with name only
    Student(string n) : name(n), age(0), gpa(0.0), major("Undeclared") {
      cout << "Name-only constructor called" << endl;
    }

    // Constructor with name and age
    Student(string n, int a) : name(n), age(a), gpa(0.0), major("Undeclared") {
      cout << "Name and age constructor called" << endl;
    }

    // Constructor with all details
    Student(string n, int a, double g, string m)
      : name(n), age(a), gpa(g), major(m) {
      cout << "Full details constructor called" << endl;
    }

    // Copy constructor
    Student(const Student& other)
      : name(other.name + " (copy)"), age(other.age), gpa(other.gpa), major(other.major) {
      cout << "Copy constructor called" << endl;
    }

    void display() const {
      cout << "Name: " << name << ", Age: " << age
            << ", GPA: " << gpa << ", Major: " << major << endl;
    }
};

int main() {
  cout << "=== Constructor Overloading ===" << endl;
  
  Student s1; // Default constructor
  s1.display();
  cout << endl;
  
  Student s2("Alice"); // Name-only constructor
  s2.display();
  cout << endl;
  
  Student s3("Bob", 20); // Name and age constructor
  s3.display();
  cout << endl;
  
  Student s4("Charlie", 22, 3.8, "Computer Science"); // Full constructor
  s4.display();
  cout << endl;
  
  Student s5 = s4; // Copy constructor
  s5.display();
  
  return 0;
}
=== Constructor Overloading ===
Default constructor called
Name: Unknown, Age: 0, GPA: 0, Major: Undeclared

Name-only constructor called
Name: Alice, Age: 0, GPA: 0, Major: Undeclared

Name and age constructor called
Name: Bob, Age: 20, GPA: 0, Major: Undeclared

Full details constructor called
Name: Charlie, Age: 22, GPA: 3.8, Major: Computer Science

Copy constructor called
Name: Charlie (copy), Age: 22, GPA: 3.8, Major: Computer Science

Function Overloading Rules and Resolution

What Can Be Overloaded

  • Regular functions
  • Class member functions
  • Constructors
  • Static member functions

What Cannot Be Overloaded

  • Functions differentiated only by return type
  • Functions differentiated only by const on value parameters
  • Functions where one has a parameter with a default argument that makes it ambiguous
overloading_rules.cpp
// VALID OVERLOADING
void process(int x);
void process(double x);
void process(int x, int y);
void process(const string& s); // Different parameter type

// INVALID OVERLOADING
// int calculate(int x);
// double calculate(int x); // ERROR: Only return type differs

// AMBIGUOUS (with default arguments)
// void display(string s, int times = 1);
// void display(string s); // AMBIGUOUS: display("hello") could call either

Overloading with Default Arguments

Be careful when combining overloading with default arguments to avoid ambiguity:

default_arguments.cpp
#include
using namespace std;

class Printer {
  public:
    // Safe overloading with default arguments
    void print(int value, bool newline = true) {
      cout << "Integer: " << value;
      if (newline) cout << endl;
    }

    void print(double value, bool newline = true) {
      cout << "Double: " << value;
      if (newline) cout << endl;
    }

    // Different number of parameters - no ambiguity
    void print(const string& text, int times, bool newline = true) {
      for (int i = 0; i < times; i++) {
        cout << text;
        if (i < times - 1) cout << " ";
      }
      if (newline) cout << endl;
    }
};

int main() {
  Printer p;
  
  p.print(42); // Calls print(int, bool) with default true
  p.print(42, false); // Calls print(int, bool) with explicit false
  p.print(3.14); // Calls print(double, bool) with default true
  p.print("Hi", 3); // Calls print(string, int, bool) with default true
  p.print("Hello", 2, false);
  
  cout << " [end of output]" << endl;
  return 0;
}
Integer: 42
Integer: 42Double: 3.14
Hi Hi Hi
Hello Hello [end of output]

Overloading and const Qualifiers

const_overloading.cpp
#include
#include
using namespace std;

class TextProcessor {
  private:
    string data;

  public:
    // Overloading based on const-ness (references/pointers)
    void process(string& str) {
      cout << "Processing modifiable string: " << str << endl;
      str += " [modified]";
    }

    void process(const string& str) {
      cout << "Processing const string: " << str << endl;
      // Cannot modify str - it's const
    }

    // const member function overloading
    string getData() {
      cout << "Non-const getData called" << endl;
      return data;
    }

    string getData() const {
      cout << "Const getData called" << endl;
      return data;
    }
};

int main() {
  TextProcessor processor;
  string mutableStr = "Hello";
  const string constStr = "World";
  
  processor.process(mutableStr); // Calls process(string&)
  processor.process(constStr); // Calls process(const string&)
  processor.process("Literal"); // Calls process(const string&)
  
  cout << "Mutable string after processing: " << mutableStr << endl;
  
  TextProcessor nonConstProcessor;
  const TextProcessor constProcessor;
  
  nonConstProcessor.getData(); // Calls non-const version
  constProcessor.getData(); // Calls const version
  
  return 0;
}
Processing modifiable string: Hello
Processing const string: World
Processing const string: Literal
Mutable string after processing: Hello [modified]
Non-const getData called
Const getData called

Best Practices for Function Overloading

Consistent Semantics: Overloaded functions should perform conceptually similar operations. Don't overload functions that do completely different things just because they can have the same name.
  • Maintain consistent behavior across all overloads
  • Use descriptive parameter names to indicate differences
  • Avoid ambiguous overloads that could confuse callers
  • Document each overload clearly indicating when to use which version
  • Consider using default arguments instead of multiple overloads when appropriate

Common Pitfalls and How to Avoid Them

Avoid Ambiguity: The most common problem with function overloading is creating ambiguous function calls that the compiler cannot resolve.
common_pitfalls.cpp
// PROBLEMATIC OVERLOADING
/*
void process(int x, double y = 0.0);
void process(int x); // AMBIGUOUS: process(5) could call either
*/

// SOLUTION: Make overloads clearly distinct
void processWithDefault(int x, double y = 0.0);
void process(int x); // Now clearly different

// PROBLEM: Implicit conversions causing ambiguity
/*
void display(float f);
void display(double d);
display(1.5); // AMBIGUOUS: 1.5 is double, but float is also possible
*/

// SOLUTION: Be explicit or avoid similar types
void displayFloat(float f);
void displayDouble(double d);

Real-World Applications

1. Mathematical Libraries

Math functions like abs(), pow(), sqrt() are overloaded for different numeric types.

2. I/O Streams

The << and >> operators are overloaded for different data types.

3. Container Classes

STL containers have overloaded constructors and methods like insert(), find().

Remember: Function overloading is a powerful tool for creating intuitive APIs, but it should be used judiciously. When in doubt, prefer clarity over cleverness - sometimes separate function names are better than overloaded ones.

Advanced: Template vs Overloading

Sometimes function templates can be an alternative to overloading:

templates_vs_overloading.cpp
// Using overloading
int max(int a, int b) { return (a > b) ? a : b; }
double max(double a, double b) { return (a > b) ? a : b; }
string max(const string& a, const string& b) { return (a > b) ? a : b; }

// Using templates (often better for generic operations)
template<typename T>
T maxTemplate(T a, T b) {
  return (a > b) ? a : b;
}

Use overloading when different types require significantly different implementations. Use templates when the algorithm is the same but the types differ.

Operator Overloading

Operator Overloading: A compile-time polymorphism feature in C++ that allows you to redefine the behavior of operators (+, -, *, /, etc.) for user-defined types (classes and structures). It enables objects to behave like built-in types with intuitive syntax.

Why Operator Overloading?

Operator overloading makes your code more intuitive and readable:

  • Natural Syntax: Allows objects to use familiar operators like a + b instead of a.add(b)
  • Code Readability: Makes complex operations look simple and intuitive
  • Consistency: User-defined types can behave similarly to built-in types
  • Mathematical Expressions: Essential for mathematical classes like Complex numbers, Vectors, Matrices
Note: You cannot change the precedence, associativity, or number of operands of operators. You can only define how operators work with your custom types.

Operator Overloading Syntax

Operator overloading can be implemented in two ways:

1. Member Function Syntax

member_function_syntax.cpp
class ClassName {
  public:
    // Overload + operator as member function
    ClassName operator+(const ClassName& obj) {
      // implementation
    }
};

2. Friend Function Syntax

friend_function_syntax.cpp
class ClassName {
  public:
    // Declare friend function
    friend ClassName operator+(const ClassName& obj1, const ClassName& obj2);
};

// Define friend function
ClassName operator+(const ClassName& obj1, const ClassName& obj2) {
  // implementation
}

Example: Complex Number Class

Let's create a Complex number class with overloaded operators:

complex_number.cpp
#include
using namespace std;

class Complex {
  private:
    double real;
    double imag;

  public:
    // Constructor
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}

    // Overload + operator (member function)
    Complex operator+(const Complex& other) const {
      return Complex(real + other.real, imag + other.imag);
    }

    // Overload - operator (member function)
    Complex operator-(const Complex& other) const {
      return Complex(real - other.real, imag - other.imag);
    }

    // Overload * operator (member function)
    Complex operator*(const Complex& other) const {
      return Complex(real * other.real - imag * other.imag,
                    real * other.imag + imag * other.real);
    }

    // Overload == operator (member function)
    bool operator==(const Complex& other) const {
      return (real == other.real) && (imag == other.imag);
    }

    // Overload << operator (friend function)
    friend ostream& operator<<(ostream& os, const Complex& c);

    // Overload >> operator (friend function)
    friend istream& operator>>(istream& is, Complex& c);
};

// Define << operator (friend function)
ostream& operator<<(ostream& os, const Complex& c) {
  os << c.real;
  if (c.imag >= 0)
    os << " + " << c.imag << "i";
  else
    os << " - " << -c.imag << "i";
  return os;
}

// Define >> operator (friend function)
istream& operator>>(istream& is, Complex& c) {
  cout << "Enter real part: ";
  is >> c.real;
  cout << "Enter imaginary part: ";
  is >> c.imag;
  return is;
}

int main() {
  Complex c1(3.0, 4.0);
  Complex c2(1.0, 2.0);
  Complex c3;

  cout << "c1 = " << c1 << endl;
  cout << "c2 = " << c2 << endl;

  // Using overloaded operators
  c3 = c1 + c2;
  cout << "c1 + c2 = " << c3 << endl;

  c3 = c1 - c2;
  cout << "c1 - c2 = " << c3 << endl;

  c3 = c1 * c2;
  cout << "c1 * c2 = " << c3 << endl;

  cout << "c1 == c2? " << (c1 == c2 ? "Yes" : "No") << endl;

  // Using overloaded >> operator
  Complex c4;
  cin >> c4;
  cout << "You entered: " << c4 << endl;

  return 0;
}
c1 = 3 + 4i
c2 = 1 + 2i
c1 + c2 = 4 + 6i
c1 - c2 = 2 + 2i
c1 * c2 = -5 + 10i
c1 == c2? No
Enter real part: 5
Enter imaginary part: 6
You entered: 5 + 6i

Rules and Guidelines for Operator Overloading

Rule Description
Cannot create new operators You can only overload existing C++ operators
Cannot change precedence Operator precedence remains the same as built-in operators
Cannot change associativity Left-to-right or right-to-left associativity cannot be changed
Cannot change operand count Binary operators must remain binary, unary must remain unary
Some operators cannot be overloaded ., ::, ?:, sizeof, typeid, .*, etc.

Commonly Overloaded Operators

1. Arithmetic Operators (+, -, *, /)

Typically implemented as member functions or friend functions that return new objects.

2. Comparison Operators (==, !=, <, >, <=, >=)

Should return boolean values and maintain logical consistency.

3. Stream Operators (<<, >>)

Must be implemented as friend functions since the left operand is a stream object.

4. Assignment Operator (=)

Should handle self-assignment and return *this by reference.

assignment_operator.cpp
class MyClass {
  public:
    // Overload assignment operator
    MyClass& operator=(const MyClass& other) {
      // Check for self-assignment
      if (this == &other) {
        return *this;
      }
      // Copy data from other to this
      // ... implementation ...
      return *this;
    }
};
Warning: Always check for self-assignment in the assignment operator to avoid unnecessary work and potential bugs.

Best Practices

  • Maintain Intuitive Behavior: Overloaded operators should behave similarly to their built-in counterparts
  • Be Consistent: Related operators should work together logically
  • Use Friend Functions for Symmetric Operators: When the operator should work the same regardless of operand order
  • Return References for Assignment Operators: To allow chaining (a = b = c)
  • Document Your Overloads: Make it clear what your overloaded operators do
Tip: When overloading operators, ask yourself: "Does this make the code more readable and intuitive?" If not, consider using regular member functions instead.

Friend Function & Class

Friend Function: A special function in C++ that is not a member of a class but has access to all private and protected members of the class. It is declared using the friend keyword inside the class.
Friend Class: A class that can access all private and protected members of another class in which it is declared as a friend.

Why Friend Functions and Classes?

Friend features provide controlled access to private members while maintaining encapsulation:

  • Operator Overloading: Essential for overloading operators when the left operand is not an object of the class
  • Utility Functions: Create helper functions that need access to private data
  • Inter-Class Communication: Allow closely related classes to share private data efficiently
  • Stream Operations: Necessary for overloading << and >> operators
Note: Friendship is granted, not taken. A class must explicitly declare another function or class as its friend. Friendship is not mutual or inherited.

Friend Function Syntax and Characteristics

friend_function_syntax.cpp
class ClassName {
  private:
    // private data members
    int privateData;

  public:
    // Friend function declaration
    friend returnType friendFunctionName(parameters);
};

// Friend function definition (NOT a member function)
returnType friendFunctionName(parameters) {
  // Can access private members of ClassName
}

Key Characteristics of Friend Functions:

  • Not Member Functions: They are ordinary functions with special access privileges
  • No this Pointer: They don't have access to the this pointer
  • Can be Defined Anywhere: Can be defined in the class or outside
  • One-Way Friendship: Friendship is not bidirectional unless explicitly declared
  • Not Inherited: Derived classes don't inherit friend functions

Example: Friend Function with Rectangle Class

Let's create a Rectangle class with friend functions for area comparison:

friend_function_example.cpp
#include
using namespace std;

class Rectangle {
  private:
    double length;
    double width;

  public:
    // Constructor
    Rectangle(double l = 0.0, double w = 0.0) : length(l), width(w) {}

    // Member function to calculate area
    double getArea() const {
      return length * width;
    }

    // Friend function declaration - compares areas of two rectangles
    friend bool isAreaEqual(const Rectangle& r1, const Rectangle& r2);

    // Friend function declaration - calculates total area
    friend double getTotalArea(const Rectangle& r1, const Rectangle& r2);

    // Friend function to display private data
    friend void displayDimensions(const Rectangle& r);
};

// Friend function definition - compares areas
// Explanation: This function can directly access private members 'length' and 'width'
// even though it's not a member function of Rectangle class
bool isAreaEqual(const Rectangle& r1, const Rectangle& r2) {
  return (r1.length * r1.width) == (r2.length * r2.width);
}

// Friend function definition - calculates total area
// Explanation: Accesses private members of both objects to compute total
double getTotalArea(const Rectangle& r1, const Rectangle& r2) {
  return (r1.length * r1.width) + (r2.length * r2.width);
}

// Friend function definition - displays private dimensions
// Explanation: Can access and display private data that regular functions can't
void displayDimensions(const Rectangle& r) {
  cout << "Length: " << r.length << ", Width: " << r.width << endl;
}

int main() {
  Rectangle rect1(5.0, 3.0);
  Rectangle rect2(4.0, 4.0);
  Rectangle rect3(6.0, 2.5);

  cout << "Rectangle 1: ";
  displayDimensions(rect1);
  cout << "Area: " << rect1.getArea() << endl << endl;

  cout << "Rectangle 2: ";
  displayDimensions(rect2);
  cout << "Area: " << rect2.getArea() << endl << endl;

  cout << "Rectangle 3: ";
  displayDimensions(rect3);
  cout << "Area: " << rect3.getArea() << endl << endl;

  // Using friend functions
  cout << "Are rect1 and rect2 equal? " <<
       (isAreaEqual(rect1, rect2) ? "Yes" : "No") << endl;
  cout << "Are rect1 and rect3 equal? " <<
       (isAreaEqual(rect1, rect3) ? "Yes" : "No") << endl;
  cout << "Total area of rect1 and rect2: " << getTotalArea(rect1, rect2) << endl;

  return 0;
}
Rectangle 1: Length: 5, Width: 3
Area: 15

Rectangle 2: Length: 4, Width: 4
Area: 16

Rectangle 3: Length: 6, Width: 2.5
Area: 15

Are rect1 and rect2 equal? No
Are rect1 and rect3 equal? Yes
Total area of rect1 and rect2: 31

Friend Class Syntax and Usage

friend_class_syntax.cpp
class ClassA {
  private:
    int privateData;

  public:
    // Friend class declaration
    friend class ClassB;
};

class ClassB {
  public:
    // Can access private members of ClassA
    void accessClassA(ClassA& obj) {
      obj.privateData = 100; // Allowed because ClassB is friend of ClassA
    }
};

Example: Friend Class with Student and GradeBook

Let's demonstrate friend classes with Student and GradeBook classes:

friend_class_example.cpp
#include
using namespace std;

// Forward declaration
class Student;

class GradeBook {
  private:
    string courseName;

  public:
    GradeBook(const string& name) : courseName(name) {}

    // Member function that can access Student's private data
    // Explanation: GradeBook is declared as friend in Student class,
    // so it can access all private members of Student objects
    void displayStudentInfo(const Student& student);

    // Can modify Student's private grades
    void updateStudentGrade(Student& student, double newGrade);
};

class Student {
  private:
    string name;
    int id;
    double grade;

  public:
    Student(const string& n, int i, double g) : name(n), id(i), grade(g) {}

    // Public member functions
    string getName() const { return name; }
    int getId() const { return id; }

    // Friend class declaration
    // Explanation: This grants GradeBook complete access to all
    // private and protected members of Student class
    friend class GradeBook;
};

// GradeBook member function definitions
// Explanation: These functions can access Student's private members
// directly because GradeBook is a friend of Student
void GradeBook::displayStudentInfo(const Student& student) {
  cout << "Course: " << courseName << endl;
  cout << "Student Name: " << student.name << endl;
  cout << "Student ID: " << student.id << endl;
  cout << "Grade: " << student.grade << endl;
  cout << "-------------------" << endl;
}

void GradeBook::updateStudentGrade(Student& student, double newGrade) {
  student.grade = newGrade;
  cout << "Updated grade for " << student.name << " to " << newGrade << endl;
}

int main() {
  // Create students
  Student student1("Alice Johnson", 1001, 85.5);
  Student student2("Bob Smith", 1002, 92.0);

  // Create grade book
  GradeBook mathBook("Mathematics 101");

  // Display student info using GradeBook (friend class)
  cout << "Initial Student Information:" << endl;
  mathBook.displayStudentInfo(student1);
  mathBook.displayStudentInfo(student2);

  // Update grades using GradeBook
  mathBook.updateStudentGrade(student1, 88.0);
  mathBook.updateStudentGrade(student2, 94.5);

  // Display updated information
  cout << endl << "Updated Student Information:" << endl;
  mathBook.displayStudentInfo(student1);
  mathBook.displayStudentInfo(student2);

  return 0;
}
Initial Student Information:
Course: Mathematics 101
Student Name: Alice Johnson
Student ID: 1001
Grade: 85.5
-------------------
Course: Mathematics 101
Student Name: Bob Smith
Student ID: 1002
Grade: 92
-------------------
Updated grade for Alice Johnson to 88
Updated grade for Bob Smith to 94.5

Updated Student Information:
Course: Mathematics 101
Student Name: Alice Johnson
Student ID: 1001
Grade: 88
-------------------
Course: Mathematics 101
Student Name: Bob Smith
Student ID: 1002
Grade: 94.5
-------------------

Important Rules and Characteristics

Aspect Friend Function Friend Class
Declaration Inside class with friend keyword Inside class with friend class ClassName
Access Scope All private/protected members of the class All private/protected members of the class
Membership Not a member function All member functions are friends
Inheritance Not inherited Not inherited
Transitivity Not transitive Not transitive

When to Use Friend Functions and Classes

Use Friend Functions When:

  • Operator Overloading: For operators like <<, >> where the left operand is not of the class type
  • Utility Functions: When a function needs to operate on two different classes' private data
  • Performance: When you need to avoid the overhead of member function calls for frequently used operations

Use Friend Classes When:

  • Close Relationship: When two classes are conceptually tightly related
  • Container Classes: When one class acts as a container or manager for another
  • Implementation Details: When you want to separate interface from implementation while maintaining access
Warning: Overuse of friend functions and classes can break encapsulation. Use them judiciously and only when necessary. Friendship should be used to maintain encapsulation, not violate it.

Best Practices

  • Minimize Usage: Use friends sparingly - they violate encapsulation principles
  • Document Relationships: Clearly document why friendship is necessary
  • Consider Alternatives: First consider if public member functions or getters/setters can solve the problem
  • One-Way Only: Remember friendship is not bidirectional unless explicitly declared both ways
  • Group Related Functions: If multiple functions need friend access, consider making a helper class instead
Tip: Before using friend functions or classes, ask yourself: "Is this necessary, or can I achieve the same functionality with public member functions while maintaining better encapsulation?"

Static Members

Static Members: Class members (variables or functions) that are associated with the class itself rather than individual objects. They are shared by all objects of the class and exist independently of any object instances.

Why Static Members?

Static members provide class-level functionality and data sharing:

  • Shared Data: Maintain data common to all objects (e.g., object count)
  • Class-Level Operations: Perform operations that don't require object instances
  • Memory Efficiency: Single copy shared across all instances
  • Utility Functions: Provide helper functions that don't need object state
  • Global Access: Accessible without creating objects
Note: Static members belong to the class, not to any specific object. They are created when the program starts and destroyed when the program ends, similar to global variables but with class scope.

Static Data Members

Declaration and Definition

static_data_syntax.cpp
class ClassName {
  public:
    // Declaration inside class (with static keyword)
    static int staticVariable;
};

// Definition outside class (without static keyword)
// Memory is allocated here
int ClassName::staticVariable = 0;

Key Characteristics of Static Data Members:

  • Single Copy: Only one instance exists for the entire class
  • Class Scope: Accessed using class name and scope resolution operator
  • Independent of Objects: Exists even when no objects are created
  • Must be Defined: Declaration inside class, definition outside class
  • Shared Access: All objects share the same static variable

Example: Object Counter with Static Data Member

Let's create a Student class that tracks how many objects are created:

static_data_example.cpp
#include
using namespace std;

class Student {
  private:
    string name;
    int id;
    static int studentCount; // Declaration of static data member
    // Explanation: This variable is shared by ALL Student objects
    // It keeps track of total number of Student objects created

  public:
    // Constructor
    Student(const string& n, int i) : name(n), id(i) {
      studentCount++; // Increment count when new object is created
      cout << "Constructor called for " << name << " (Total students: " << studentCount << ")" << endl;
    }

    // Destructor
    ~Student() {
      studentCount--; // Decrement count when object is destroyed
      cout << "Destructor called for " << name << " (Total students: " << studentCount << ")" << endl;
    }

    // Static member function to access static data
    static int getStudentCount() {
      return studentCount;
    }

    // Regular member function
    void display() const {
      cout << "Student: " << name << " (ID: " << id << ")" << endl;
    }
};

// Definition of static data member
// Explanation: This is where memory is actually allocated for the static variable
// It must be defined outside the class, exactly once in the program
int Student::studentCount = 0;

int main() {
  cout << "Initial student count: " << Student::getStudentCount() << endl << endl;
  // Explanation: We can access static member function without creating any objects

  {
    // Create some students in a block to see destructor calls
    Student s1("Alice", 1001);
    Student s2("Bob", 1002);
    Student s3("Charlie", 1003);

    cout << endl << "Current student count: " << Student::getStudentCount() << endl;
    cout << "Accessing count through object: " << s1.getStudentCount() << endl << endl;
    // Explanation: All objects share the same static variable
    // So s1.getStudentCount(), s2.getStudentCount() all return the same value

    s1.display();
    s2.display();
    s3.display();
  } // Destructors called here when objects go out of scope

  cout << endl << "Final student count: " << Student::getStudentCount() << endl;
  return 0;
}
Initial student count: 0

Constructor called for Alice (Total students: 1)
Constructor called for Bob (Total students: 2)
Constructor called for Charlie (Total students: 3)

Current student count: 3
Accessing count through object: 3

Student: Alice (ID: 1001)
Student: Bob (ID: 1002)
Student: Charlie (ID: 1003)
Destructor called for Charlie (Total students: 2)
Destructor called for Bob (Total students: 1)
Destructor called for Alice (Total students: 0)

Final student count: 0

Static Member Functions

Declaration and Definition

static_function_syntax.cpp
class ClassName {
  public:
    // Declaration (with static keyword)
    static returnType functionName(parameters);
};

// Definition (without static keyword)
returnType ClassName::functionName(parameters) {
  // implementation
}

Key Characteristics of Static Member Functions:

  • No this Pointer: Cannot access non-static members directly
  • Class-Level Access: Can be called without object instances
  • Can Access Static Members: Can access other static data and functions
  • Cannot be Virtual: Static functions cannot be declared as virtual
  • Cannot be const: Static functions cannot have const qualifier

Example: Utility Class with Static Functions

Let's create a MathUtility class with static member functions:

static_function_example.cpp
#include
using namespace std;

class MathUtility {
  private:
    // Private static data member
    static int functionCallCount;
    // Explanation: Tracks how many times utility functions are called

  public:
    // Static utility functions
    // Explanation: These functions don't need object state - they operate only on parameters

    static double pi() {
      functionCallCount++;
      return 3.141592653589793;
    }

    static double square(double x) {
      functionCallCount++;
      return x * x;
    }

    static double circleArea(double radius) {
      functionCallCount++;
      return pi() * square(radius);
    }

    static double factorial(int n) {
      functionCallCount++;
      if (n <= 1) return 1;
      double result = 1;
      for (int i = 2; i <= n; i++) {
        result *= i;
      }
      return result;
    }

    // Static function to access private static data
    static int getFunctionCallCount() {
      return functionCallCount;
    }

    // Static function to reset counter
    static void resetCounter() {
      functionCallCount = 0;
    }
};

// Definition of private static data member
int MathUtility::functionCallCount = 0;

int main() {
  cout << "=== Math Utility Demo ===" << endl;
  cout << "Initial function call count: " << MathUtility::getFunctionCallCount() << endl << endl;

  // Using static functions without creating objects
  // Explanation: No objects needed - we call functions directly using class name
  cout << "Value of PI: " << MathUtility::pi() << endl;
  cout << "Square of 5: " << MathUtility::square(5) << endl;
  cout << "Area of circle (radius=3): " << MathUtility::circleArea(3) << endl;
  cout << "Factorial of 6: " << MathUtility::factorial(6) << endl;

  cout << endl << "Function call count after operations: " << MathUtility::getFunctionCallCount() << endl;

  // Reset counter and demonstrate again
  MathUtility::resetCounter();
  cout << "Counter reset. New count: " << MathUtility::getFunctionCallCount() << endl;

  // Use some more functions
  cout << "Square of 7: " << MathUtility::square(7) << endl;
  cout << "Factorial of 4: " << MathUtility::factorial(4) << endl;

  cout << "Final function call count: " << MathUtility::getFunctionCallCount() << endl;

  return 0;
}
=== Math Utility Demo ===
Initial function call count: 0

Value of PI: 3.14159
Square of 5: 25
Area of circle (radius=3): 28.2743
Factorial of 6: 720

Function call count after operations: 6

Counter reset. New count: 0

Square of 7: 49
Factorial of 4: 24

Final function call count: 2

Static Members vs Regular Members

Aspect Static Members Regular Members
Memory Allocation Single copy for entire class Separate copy for each object
Lifetime Entire program duration Object lifetime
Access Class name or object Object only
this Pointer Not available Available
Initialization Outside class definition In constructor or inline

Common Use Cases for Static Members

1. Object Counting and Tracking

Track how many objects of a class exist, as shown in the Student example.

2. Utility Functions

Provide helper functions that don't require object state, like the MathUtility class.

3. Shared Resources

Manage resources shared across all objects, like database connections or configuration settings.

4. Constants

Define class-level constants that are common to all objects.

static_constants.cpp
class Physics {
  public:
    static const double GRAVITY;
    static const double LIGHT_SPEED;
    static const int MAX_ITERATIONS = 1000;
};

// Definition of static constants (for non-integral types)
const double Physics::GRAVITY = 9.81;
const double Physics::LIGHT_SPEED = 299792458.0;
Warning: Static members must be defined exactly once in the program. Forgetting to define static data members will cause linking errors. Also, be cautious with static members in multi-threaded environments as they are shared across threads.

Best Practices

  • Use for True Class-Level Data: Only use static members for data/functions that are genuinely shared across all objects
  • Initialize Properly: Always define static data members outside the class
  • Thread Safety: Consider synchronization for static members in multi-threaded programs
  • Minimize Global State: Avoid overusing static members as they create global state
  • Document Purpose: Clearly document why a member needs to be static
  • Use const when possible: Make static data members const if they shouldn't be modified
Tip: For integral static constants, you can initialize them inside the class definition in modern C++. For other types, you still need to define them outside the class. Use static member functions to provide controlled access to private static data members.

Virtual Functions

Virtual Functions: Member functions in a base class that can be redefined (overridden) in derived classes. They enable runtime polymorphism, allowing the program to decide which function to call at runtime based on the actual object type rather than the reference/pointer type.

Why Virtual Functions?

Virtual functions are fundamental to achieving runtime polymorphism in C++:

  • Runtime Polymorphism: Decide which function to call at runtime
  • Flexible Design: Write code that works with base class pointers but executes derived class behavior
  • Extensibility: Add new derived classes without modifying existing code
  • Interface Definition: Define common interfaces in base classes
  • Dynamic Binding: Bind function calls to actual object type at runtime
Note: Virtual functions are the mechanism that enables "one interface, multiple implementations" - a core principle of object-oriented programming. Without virtual functions, C++ uses static binding (compile-time) instead of dynamic binding (runtime).

Virtual Function Syntax and Mechanism

Basic Syntax

virtual_function_syntax.cpp
class Base {
  public:
    // Virtual function declaration
    virtual returnType functionName(parameters);
};

class Derived : public Base {
  public:
    // Function override (virtual keyword optional in derived class)
    returnType functionName(parameters) override;
};

How Virtual Functions Work

Virtual functions work through a mechanism called the Virtual Function Table (vtable):

  • Each class with virtual functions has a vtable (created by the compiler)
  • The vtable contains pointers to the virtual functions
  • Each object contains a pointer to its class's vtable (vptr)
  • When a virtual function is called, the program uses the vptr to find the correct function in the vtable

Example: Basic Virtual Function Demonstration

Let's create a simple hierarchy to demonstrate virtual function behavior:

basic_virtual_example.cpp
#include
using namespace std;

class Animal {
  public:
    // Virtual function - can be overridden by derived classes
    // Explanation: The 'virtual' keyword enables runtime polymorphism
    // The function that gets called depends on the actual object type
    virtual void makeSound() {
      cout << "Animal makes a sound" << endl;
    }

    // Regular (non-virtual) function
    // Explanation: This uses static binding - always calls Animal's version
    void sleep() {
      cout << "Animal is sleeping" << endl;
    }
};

class Dog : public Animal {
  public:
    // Override the virtual function
    // Explanation: When called through Animal pointer/reference,
    // this version will be executed for Dog objects
    void makeSound() override {
      cout << "Dog barks: Woof! Woof!" << endl;
    }

    // Override non-virtual function (not recommended)
    void sleep() {
      cout << "Dog is sleeping" << endl;
    }
};

class Cat : public Animal {
  public:
    // Override the virtual function
    void makeSound() override {
      cout << "Cat meows: Meow! Meow!" << endl;
    }
};

int main() {
  cout << "=== Virtual Function Demonstration ===" << endl << endl;

  // Create objects
  Animal animal;
  Dog dog;
  Cat cat;

  cout << "Direct object calls:" << endl;
  animal.makeSound();
  dog.makeSound();
  cat.makeSound();
  cout << endl;

  // Using base class pointers - DEMONSTRATES RUNTIME POLYMORPHISM
  Animal* animalPtr;
  
  cout << "Using Animal pointer to Animal object:" << endl;
  animalPtr = &animal;
  animalPtr->makeSound(); // Calls Animal::makeSound()

  cout << "Using Animal pointer to Dog object:" << endl;
  animalPtr = &dog;
  animalPtr->makeSound(); // Calls Dog::makeSound() - VIRTUAL FUNCTION WORKING!

  cout << "Using Animal pointer to Cat object:" << endl;
  animalPtr = &cat;
  animalPtr->makeSound(); // Calls Cat::makeSound() - VIRTUAL FUNCTION WORKING!
  cout << endl;

  // Demonstration of non-virtual function behavior
  cout << "=== Non-virtual Function Behavior ===" << endl;
  animalPtr = &dog;
  animalPtr->sleep(); // Calls Animal::sleep() - NOT Dog::sleep()!
  cout << "Explanation: Non-virtual functions use static binding - " << endl;
  cout << "the function called depends on the pointer type, not the object type." << endl;

  return 0;
}
=== Virtual Function Demonstration ===

Direct object calls:
Animal makes a sound
Dog barks: Woof! Woof!
Cat meows: Meow! Meow!

Using Animal pointer to Animal object:
Animal makes a sound
Using Animal pointer to Dog object:
Dog barks: Woof! Woof!
Using Animal pointer to Cat object:
Cat meows: Meow! Meow!

=== Non-virtual Function Behavior ===
Animal is sleeping
Explanation: Non-virtual functions use static binding -
the function called depends on the pointer type, not the object type.

Pure Virtual Functions and Abstract Classes

Pure Virtual Functions

pure_virtual_syntax.cpp
class AbstractClass {
  public:
    // Pure virtual function - must be overridden by derived classes
    virtual void pureVirtualFunction() = 0;
    
    // Regular virtual function with implementation
    virtual void regularVirtualFunction() {
      // Default implementation
    }
};

Example: Abstract Class with Pure Virtual Functions

Let's create a shape hierarchy to demonstrate abstract classes and pure virtual functions:

abstract_class_example.cpp
#include
using namespace std;

class Shape {
  protected:
    string name;
    // Explanation: Protected so derived classes can access

  public:
    Shape(const string& n) : name(n) {}
    
    // Pure virtual functions - MUST be implemented by derived classes
    // Explanation: These make Shape an abstract class
    // You cannot create objects of abstract classes
    virtual double area() const = 0;
    virtual double perimeter() const = 0;

    // Regular virtual function with default implementation
    virtual void display() const {
      cout << "Shape: " << name << endl;
    }

    // Virtual destructor - CRUCIAL for proper cleanup
    // Explanation: Ensures derived class destructors are called
    // when deleting through base class pointer
    virtual ~Shape() {
      cout << "Shape destructor: " << name << endl;
    }
};

class Circle : public Shape {
  private:
    double radius;
    static const double PI;

  public:
    Circle(double r) : Shape("Circle"), radius(r) {}

    // MUST implement pure virtual functions
    double area() const override {
      return PI * radius * radius;
    }

    double perimeter() const override {
      return 2 * PI * radius;
    }

    // Override display function
    void display() const override {
      cout << "Circle - Radius: " << radius
             << ", Area: " << area()
             << ", Perimeter: " << perimeter() << endl;
    }

    ~Circle() override {
      cout << "Circle destructor" << endl;
    }
};

class Rectangle : public Shape {
  private:
    double length, width;

  public:
    Rectangle(double l, double w) : Shape("Rectangle"), length(l), width(w) {}

    // MUST implement pure virtual functions
    double area() const override {
      return length * width;
    }

    double perimeter() const override {
      return 2 * (length + width);
    }

    // Override display function
    void display() const override {
      cout << "Rectangle - Length: " << length
             << ", Width: " << width
             << ", Area: " << area()
             << ", Perimeter: " << perimeter() << endl;
    }

    ~Rectangle() override {
      cout << "Rectangle destructor" << endl;
    }
};

// Static member definition
const double Circle::PI = 3.141592653589793;

int main() {
  cout << "=== Abstract Class and Virtual Functions ===" << endl << endl;

  // Cannot create Shape objects - it's abstract
  // Shape shape; // This would cause compilation error

  // Create derived objects
  Circle circle(5.0);
  Rectangle rectangle(4.0, 6.0);

  // Use objects directly
  cout << "Direct object calls:" << endl;
  circle.display();
  rectangle.display();
  cout << endl;

  // Demonstrate polymorphism with base class pointers
  Shape* shapes[] = {&circle, &rectangle};
  
  cout << "Using Shape pointers (Polymorphism):" << endl;
  for (int i = 0; i < 2; i++) {
    shapes[i]->display();
  }
  cout << endl;

  // Calculate total area using polymorphism
  double totalArea = 0;
  for (int i = 0; i < 2; i++) {
    totalArea += shapes[i]->area();
  }
  cout << "Total area of all shapes: " << totalArea << endl;
  cout << endl;

  // Demonstrate virtual destructor
  cout << "=== Virtual Destructor Demonstration ===" << endl;
  Shape* shapePtr = new Circle(3.0);
  shapePtr->display();
  delete shapePtr; // Calls Circle destructor THEN Shape destructor

  return 0;
}
=== Abstract Class and Virtual Functions ===

Direct object calls:
Circle - Radius: 5, Area: 78.5398, Perimeter: 31.4159
Rectangle - Length: 4, Width: 6, Area: 24, Perimeter: 20

Using Shape pointers (Polymorphism):
Circle - Radius: 5, Area: 78.5398, Perimeter: 31.4159
Rectangle - Length: 4, Width: 6, Area: 24, Perimeter: 20

Total area of all shapes: 102.54

=== Virtual Destructor Demonstration ===
Circle - Radius: 3, Area: 28.2743, Perimeter: 18.8496
Circle destructor
Shape destructor: Circle

Virtual Function Rules and Characteristics

Feature Description
Virtual Function Can be overridden in derived classes, has implementation in base class
Pure Virtual Function Must be overridden in derived classes, no implementation in base class (= 0)
Abstract Class Class with at least one pure virtual function, cannot be instantiated
Virtual Destructor Ensures proper cleanup of derived classes when deleted through base pointer
Override Specifier C++11 feature that explicitly indicates function overriding
Final Specifier Prevents further overriding of virtual function in derived classes

Virtual Destructors

Always declare destructors virtual in base classes when you might delete derived objects through base pointers:

virtual_destructor.cpp
class Base {
  public:
    virtual ~Base() { // Virtual destructor
      cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
  public:
    ~Derived() override {
      cout << "Derived destructor" << endl;
    }
};

// Usage:
Base* ptr = new Derived();
delete ptr; // Calls Derived::~Derived() then Base::~Base()
Warning: If you delete a derived class object through a base class pointer without a virtual destructor, only the base class destructor will be called. This can lead to resource leaks and undefined behavior.

Best Practices

  • Use Virtual Destructors: Always make base class destructors virtual
  • Use override Specifier: Clearly indicate when overriding virtual functions
  • Design for Polymorphism: Use abstract classes to define interfaces
  • Avoid Virtual in Constructors/Destructors: Virtual calls in constructors/destructors don't behave polymorphically
  • Consider Performance: Virtual functions have slight overhead due to vtable lookup
  • Use final Judiciously: Use final to prevent further overriding when appropriate
Tip: The override specifier (C++11) helps prevent errors by causing compilation to fail if the function doesn't actually override a virtual function. Always use it when overriding virtual functions to make your code safer and more readable.

Common Pitfalls

  • Non-virtual destructors in polymorphic base classes
  • Calling virtual functions from constructors (they don't call derived versions)
  • Forgetting to implement pure virtual functions in derived classes
  • Slicing when copying derived objects to base objects
  • Performance assumptions without proper profiling

Abstract Classes

Abstract Class: A class that contains at least one pure virtual function. Abstract classes cannot be instantiated (you cannot create objects of abstract classes) and are designed to be inherited by derived classes that provide implementations for the pure virtual functions.
Pure Virtual Function: A virtual function that is declared but not defined in the base class. It is specified by setting the function equal to 0. Derived classes must override and provide implementations for all pure virtual functions.

Why Abstract Classes?

Abstract classes provide a powerful mechanism for creating interfaces and enforcing design contracts:

  • Interface Definition: Define a common interface for all derived classes
  • Enforce Implementation: Force derived classes to implement specific functions
  • Prevent Instantiation: Prevent creating objects of incomplete classes
  • Framework Design: Create extensible frameworks where users provide implementations
  • Polymorphic Base: Serve as base classes for runtime polymorphism
Note: Abstract classes are also known as "interface classes" when they contain only pure virtual functions. They define a contract that derived classes must fulfill, enabling true polymorphism and flexible system design.

Abstract Class Syntax and Rules

Basic Syntax

abstract_class_syntax.cpp
class AbstractClass {
  public:
    // Pure virtual function - must be implemented by derived classes
    virtual void pureVirtualFunction() = 0;
    
    // Regular virtual function with default implementation
    virtual void regularVirtualFunction() {
      // Default implementation - can be overridden
    }
    
    // Virtual destructor - CRUCIAL for proper cleanup
    virtual ~AbstractClass() = default;
};

Key Rules for Abstract Classes

  • Cannot Instantiate: You cannot create objects of abstract classes
  • Must Override: Derived classes must implement all pure virtual functions
  • Can Have Data Members: Abstract classes can have data members and constructors
  • Can Have Implementations: Can provide implementations for regular virtual functions
  • Virtual Destructor: Should always have a virtual destructor
  • Can Inherit: Abstract classes can inherit from other abstract classes

Example: Vehicle Hierarchy with Abstract Base Class

Let's create a vehicle hierarchy to demonstrate abstract classes and pure virtual functions:

abstract_class_example.cpp
#include
using namespace std;

class Vehicle {
  protected:
    string brand;
    string model;
    int year;
    // Explanation: Protected members are accessible to derived classes
    // but not to outside code, maintaining encapsulation

  public:
    // Constructor - abstract classes CAN have constructors
    // Explanation: Used to initialize common data members
    Vehicle(const string& b, const string& m, int y)
      : brand(b), model(m), year(y) {
      cout << "Vehicle constructor: " << brand << " " << model << endl;
    }

    // PURE VIRTUAL FUNCTIONS - must be implemented by derived classes
    // These define the interface that all vehicles must implement

    // Pure virtual function for starting the vehicle
    virtual void start() const = 0;
    
    // Pure virtual function for stopping the vehicle
    virtual void stop() const = 0;
    
    // Pure virtual function for getting vehicle type
    virtual string getType() const = 0;

    // REGULAR VIRTUAL FUNCTION - has default implementation
    // Derived classes can override this, but it's not required
    virtual void displayInfo() const {
      cout << "Brand: " << brand << ", Model: " << model
           << ", Year: " << year << ", Type: " << getType() << endl;
    }

    // VIRTUAL DESTRUCTOR - essential for proper cleanup
    // Ensures derived class destructors are called when deleting through base pointer
    virtual ~Vehicle() {
      cout << "Vehicle destructor: " << brand << " " << model << endl;
    }
};

class Car : public Vehicle {
  private:
    int doors;
    string fuelType;

  public:
    Car(const string& b, const string& m, int y, int d, const string& f)
      : Vehicle(b, m, y), doors(d), fuelType(f) {
      cout << "Car constructor" << endl;
    }

    // MUST IMPLEMENT all pure virtual functions from Vehicle
    // These provide Car-specific implementations

    void start() const override {
      cout << "Car starting: Turn key or push button" << endl;
    }

    void stop() const override {
      cout << "Car stopping: Apply brakes and turn off engine" << endl;
    }

    string getType() const override {
      return "Car";
    }

    // OVERRIDE regular virtual function (optional)
    // Provides Car-specific information display
    void displayInfo() const override {
      cout << "CAR - Brand: " << brand << ", Model: " << model
           << ", Year: " << year << ", Doors: " << doors
           << ", Fuel: " << fuelType << endl;
    }

    ~Car() override {
      cout << "Car destructor" << endl;
    }
};

class Motorcycle : public Vehicle {
  private:
    bool hasFairing;
    string style;

  public:
    Motorcycle(const string& b, const string& m, int y, bool fairing, const string& s)
      : Vehicle(b, m, y), hasFairing(fairing), style(s) {
      cout << "Motorcycle constructor" << endl;
    }

    // MUST IMPLEMENT all pure virtual functions from Vehicle
    // These provide Motorcycle-specific implementations

    void start() const override {
      cout << "Motorcycle starting: Kick start or electric start" << endl;
    }

    void stop() const override {
      cout << "Motorcycle stopping: Apply both brakes" << endl;
    }

    string getType() const override {
      return "Motorcycle";
    }

    // OVERRIDE regular virtual function (optional)
    void displayInfo() const override {
      cout << "MOTORCYCLE - Brand: " << brand << ", Model: " << model
           << ", Year: " << year << ", Fairing: "
           << (hasFairing ? "Yes" : "No") << ", Style: " << style << endl;
    }

    ~Motorcycle() override {
      cout << "Motorcycle destructor" << endl;
    }
};

int main() {
  cout << "=== Abstract Class Demonstration ===" << endl << endl;

  // CANNOT create Vehicle objects - it's abstract
  // Vehicle vehicle("Generic", "Model", 2023); // COMPILATION ERROR!
  cout << "Cannot create Vehicle objects (abstract class)" << endl << endl;

  // Create derived objects
  Car car("Toyota", "Camry", 2023, 4, "Gasoline");
  Motorcycle bike("Harley-Davidson", "Sportster", 2022, true, "Cruiser");

  cout << endl << "=== Direct Object Usage ===" << endl;
  car.displayInfo();
  car.start();
  car.stop();
  cout << endl;

  bike.displayInfo();
  bike.start();
  bike.stop();
  cout << endl;

  // DEMONSTRATE POLYMORPHISM with abstract base class pointers
  cout << "=== Polymorphism with Abstract Base Class ===" << endl;
  Vehicle* vehicles[] = {&car, &bike};
  
  for (int i = 0; i < 2; i++) {
    vehicles[i]->displayInfo();
    vehicles[i]->start();
    vehicles[i]->stop();
    cout << endl;
  }

  // Demonstrate virtual destructor with dynamic allocation
  cout << "=== Virtual Destructor Demonstration ===" << endl;
  Vehicle* vehiclePtr = new Car("Honda", "Civic", 2023, 4, "Hybrid");
  vehiclePtr->displayInfo();
  delete vehiclePtr; // Properly calls Car destructor then Vehicle destructor

  cout << endl << "=== Program Ending - Local objects destroyed ===" << endl;
  return 0;
}
=== Abstract Class Demonstration ===

Cannot create Vehicle objects (abstract class)

Vehicle constructor: Toyota Camry
Car constructor
Vehicle constructor: Harley-Davidson Sportster
Motorcycle constructor

=== Direct Object Usage ===
CAR - Brand: Toyota, Model: Camry, Year: 2023, Doors: 4, Fuel: Gasoline
Car starting: Turn key or push button
Car stopping: Apply brakes and turn off engine

MOTORCYCLE - Brand: Harley-Davidson, Model: Sportster, Year: 2022, Fairing: Yes, Style: Cruiser
Motorcycle starting: Kick start or electric start
Motorcycle stopping: Apply both brakes

=== Polymorphism with Abstract Base Class ===
CAR - Brand: Toyota, Model: Camry, Year: 2023, Doors: 4, Fuel: Gasoline
Car starting: Turn key or push button
Car stopping: Apply brakes and turn off engine

MOTORCYCLE - Brand: Harley-Davidson, Model: Sportster, Year: 2022, Fairing: Yes, Style: Cruiser
Motorcycle starting: Kick start or electric start
Motorcycle stopping: Apply both brakes

=== Virtual Destructor Demonstration ===
Vehicle constructor: Honda Civic
Car constructor
CAR - Brand: Honda, Model: Civic, Year: 2023, Doors: 4, Fuel: Hybrid
Car destructor
Vehicle destructor: Honda Civic

=== Program Ending - Local objects destroyed ===
Motorcycle destructor
Vehicle destructor: Harley-Davidson Sportster
Car destructor
Vehicle destructor: Toyota Camry

Interface Classes (Pure Abstract Classes)

Interface classes are abstract classes that contain only pure virtual functions and no data members:

interface_class.cpp
class Drawable {
  public:
    // Pure virtual functions only - defines an interface
    virtual void draw() const = 0;
    virtual void resize(double factor) = 0;
    virtual double area() const = 0;
    
    // Virtual destructor
    virtual ~Drawable() = default;
};

class Circle : public Drawable {
  private:
    double radius;
    
  public:
    Circle(double r) : radius(r) {}
    
    void draw() const override {
      cout << "Drawing Circle with radius " << radius << endl;
    }
    
    void resize(double factor) override {
      radius *= factor;
      cout << "Circle resized to radius " << radius << endl;
    }
    
    double area() const override {
      return 3.14159 * radius * radius;
    }
};

Abstract Classes vs Concrete Classes

Aspect Abstract Classes Concrete Classes
Instantiation Cannot create objects Can create objects
Pure Virtual Functions At least one pure virtual function No pure virtual functions
Purpose Define interfaces, base for inheritance Implement functionality, create objects
Implementation May have partial implementation Complete implementation
Inheritance Designed to be inherited from May or may not be inherited from

Common Use Cases for Abstract Classes

1. Framework Development

Create extensible frameworks where users provide concrete implementations.

2. Plugin Architectures

Define interfaces for plugins that can be loaded dynamically.

3. Game Development

Define base classes for game objects with common interfaces.

4. GUI Frameworks

Create base classes for UI components with standardized behavior.

Warning: If a derived class does not implement all pure virtual functions from its base abstract class, the derived class also becomes abstract. You must provide implementations for ALL pure virtual functions to make a class concrete.

Best Practices

  • Use Virtual Destructors: Always declare destructors virtual in abstract classes
  • Use override Specifier: Clearly indicate when implementing pure virtual functions
  • Design Clear Interfaces: Abstract classes should define clear, focused interfaces
  • Follow Liskov Substitution Principle: Derived classes should be substitutable for their base classes
  • Use Interface Segregation: Create focused interfaces rather than large, general ones
  • Document Expectations: Clearly document what derived classes must implement
Tip: Use the override keyword when implementing pure virtual functions in derived classes. This makes your code more readable and helps catch errors at compile time if the function signature doesn't match the base class virtual function.

Common Mistakes to Avoid

  • Forgetting to implement all pure virtual functions in derived classes
  • Non-virtual destructors in abstract base classes
  • Trying to instantiate abstract classes directly
  • Overly complex interfaces that are difficult to implement
  • Violating the interface contract in derived classes
  • Forgetting the = 0 syntax for pure virtual functions

Interfaces

Interface: In C++, an interface is a pure abstract class that contains only pure virtual functions and no data members (except possibly static constants). Interfaces define a contract that implementing classes must fulfill, specifying what operations must be supported without dictating how they are implemented.
Interface Segregation Principle: A design principle that states clients should not be forced to depend on interfaces they don't use. Instead, create multiple specific interfaces rather than one general-purpose interface.

Why Interfaces?

Interfaces provide the foundation for flexible, maintainable, and testable software design:

  • Decoupling: Separate interface from implementation, reducing dependencies
  • Polymorphism: Enable different implementations to be used interchangeably
  • Testability: Allow mocking and testing of components in isolation
  • Extensibility: Add new implementations without modifying existing code
  • Multiple Inheritance: A class can implement multiple interfaces
  • Contract Enforcement: Ensure implementing classes provide required functionality
Note: Unlike some other programming languages, C++ doesn't have a separate interface keyword. Interfaces are implemented using pure abstract classes (classes with only pure virtual functions and a virtual destructor).

Interface Syntax and Characteristics

Basic Interface Syntax

interface_syntax.cpp
class InterfaceName {
  public:
    // Pure virtual functions - define the interface contract
    virtual returnType operation1(parameters) = 0;
    virtual returnType operation2(parameters) = 0;
    
    // Virtual destructor - ESSENTIAL for proper cleanup
    virtual ~InterfaceName() = default;
    
    // Optional: static constants
    static const int DEFAULT_VALUE = 100;
};

Key Characteristics of Interfaces

  • Pure Abstract: Contains only pure virtual functions (except destructor)
  • No Data Members: Should not contain non-static data members
  • No Implementation: Provides no implementation details
  • Virtual Destructor: Must have a virtual destructor
  • Contract Only: Defines what, not how
  • Multiple Inheritance: Classes can implement multiple interfaces

Example: Payment Processing System with Interfaces

Let's create a payment processing system demonstrating interface design principles:

payment_interfaces.cpp
#include
#include
#include
using namespace std;

// ==================== INTERFACE DEFINITIONS ====================

// Interface for payment processing
// Explanation: Defines the contract for all payment processors
// Any class that implements this interface can process payments
class IPaymentProcessor {
  public:
    virtual bool processPayment(double amount, const string& currency) = 0;
    virtual string getProcessorName() const = 0;
    virtual ~IPaymentProcessor() = default;
};

// Interface for refund operations
// Explanation: Separate interface for refund functionality
// Follows Interface Segregation Principle
class IRefundable {
  public:
    virtual bool processRefund(double amount, const string& transactionId) = 0;
    virtual ~IRefundable() = default;
};

// Interface for payment validation
// Explanation: Another segregated interface for validation
class IValidatable {
  public:
    virtual bool validatePayment(double amount) = 0;
    virtual ~IValidatable() = default;
};

// ==================== CONCRETE IMPLEMENTATIONS ====================

// Credit Card Processor - implements multiple interfaces
// Explanation: This class implements three interfaces, demonstrating
// how a class can fulfill multiple contracts
class CreditCardProcessor : public IPaymentProcessor, public IRefundable, public IValidatable {
  private:
    string merchantId;
    double transactionFee;

  public:
    CreditCardProcessor(const string& id, double fee)
      : merchantId(id), transactionFee(fee) {}

    // Implement IPaymentProcessor interface
    bool processPayment(double amount, const string& currency) override {
      double totalAmount = amount + transactionFee;
      cout << "CreditCard: Processing $" << totalAmount << " " << currency
           << " (Fee: $" << transactionFee << ")" << endl;
      return true;
    }

    string getProcessorName() const override {
      return "Credit Card Processor";
    }

    // Implement IRefundable interface
    bool processRefund(double amount, const string& transactionId) override {
      cout << "CreditCard: Refunding $" << amount
           << " for transaction " << transactionId << endl;
      return true;
    }

    // Implement IValidatable interface
    bool validatePayment(double amount) override {
      bool isValid = amount > 0 && amount <= 10000;
      cout << "CreditCard: Validation " << (isValid ? "PASSED" : "FAILED") << endl;
      return isValid;
    }
};

// PayPal Processor - implements only payment processing
// Explanation: This class only implements the basic payment interface
// demonstrating that not all implementations need to support all features
class PayPalProcessor : public IPaymentProcessor {
  private:
    string apiKey;
    bool sandboxMode;

  public:
    PayPalProcessor(const string& key, bool sandbox = false)
      : apiKey(key), sandboxMode(sandbox) {}

    // Implement IPaymentProcessor interface
    bool processPayment(double amount, const string& currency) override {
      cout << "PayPal: Processing $" << amount << " " << currency
           << " (Sandbox: " << (sandboxMode ? "ON" : "OFF") << ")" << endl;
      return true;
    }

    string getProcessorName() const override {
      return "PayPal Processor";
    }
};

// Crypto Payment Processor - implements payment and validation
class CryptoProcessor : public IPaymentProcessor, public IValidatable {
  private:
    string walletAddress;
    string cryptocurrency;

  public:
    CryptoProcessor(const string& wallet, const string& crypto)
      : walletAddress(wallet), cryptocurrency(crypto) {}

    // Implement IPaymentProcessor interface
    bool processPayment(double amount, const string& currency) override {
      cout << "Crypto: Processing " << amount << " " << cryptocurrency
           << " to wallet " << walletAddress << endl;
      return true;
    }

    string getProcessorName() const override {
 &span class="keyword">return "Crypto Processor (" + cryptocurrency + ")";
    }

    // Implement IValidatable interface
    bool validatePayment(double amount) override {
      bool isValid = amount >= 0.001; // Minimum crypto amount
      cout << "Crypto: Validation " << (isValid ? "PASSED" : "FAILED") << endl;
      return isValid;
    }
};

// ==================== SERVICE CLASS USING INTERFACES ====================

// Payment Service that works with any IPaymentProcessor
// Explanation: This class demonstrates dependency injection and
  // programming to interfaces rather than implementations
class PaymentService {
  private:
    IPaymentProcessor& processor;
    string serviceName;

  public:
    // Constructor takes IPaymentProcessor reference
    // Explanation: This is dependency injection - the service
    // doesn't know or care about the concrete implementation
    PaymentService(IPaymentProcessor& proc, const string& name)
      : processor(proc), serviceName(name) {}

    void makePayment(double amount, const string& currency) {
      cout << endl << "=== " << serviceName << " ===" << endl;
      cout << "Using: " << processor.getProcessorName() << endl;
      
      // Check if processor supports validation
      IValidatable* validatable = dynamic_cast<IValidatable*>(&processor);
      if (validatable) {
        if (!validatable->validatePayment(amount)) {
          cout << "Payment validation failed!" << endl;
          return;
        }
      }
      
      // Process payment
      bool success = processor.processPayment(amount, currency);
      cout << "Payment result: " << (success ? "SUCCESS" : "FAILED") << endl;
    }
};

int main() {
  cout << "=== Payment Processing System with Interfaces ===" << endl << endl;

  // Create different payment processors
  CreditCardProcessor creditCard("MERCHANT_123", 2.50);
  PayPalProcessor paypal("API_KEY_456", true);
  CryptoProcessor bitcoin("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "Bitcoin");

  // Create payment services using different processors
  PaymentService service1(creditCard, "E-commerce Store");
  PaymentService service2(paypal, "Mobile App");
  PaymentService service3(bitcoin, "Crypto Exchange");

  // Process payments using different services
  service1.makePayment(100.0, "USD");
  service2.makePayment(50.0, "EUR");
  service3.makePayment(0.1, "BTC");

  // Demonstrate refund functionality
  cout << endl << "=== Refund Demonstration ===" << endl;
  IRefundable* refundable = dynamic_cast<IRefundable*>(&creditCard);
  if (refundable) {
    refundable->processRefund(100.0, "TXN_12345");
  }

  // Try to refund with PayPal (which doesn't support IRefundable)
  refundable = dynamic_cast<IRefundable*>(&paypal);
  if (refundable) {
    refundable->processRefund(50.0, "TXN_67890");
  } else {
    cout << "PayPal does not support refunds through this interface" << endl;
  }

  return 0;
}
=== Payment Processing System with Interfaces ===

=== E-commerce Store ===
Using: Credit Card Processor
CreditCard: Validation PASSED
CreditCard: Processing $102.5 USD (Fee: $2.5)
Payment result: SUCCESS

=== Mobile App ===
Using: PayPal Processor
PayPal: Processing $50 EUR (Sandbox: ON)
Payment result: SUCCESS

=== Crypto Exchange ===
Using: Crypto Processor (Bitcoin)
Crypto: Validation PASSED
Crypto: Processing 0.1 Bitcoin to wallet 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
Payment result: SUCCESS

=== Refund Demonstration ===
CreditCard: Refunding $100 for transaction TXN_12345
PayPal does not support refunds through this interface

Interface Design Principles

1. Interface Segregation Principle (ISP)

Create focused, specific interfaces rather than large, general ones:

interface_segregation.cpp
// BAD: One large interface
class IMonster {
  public:
    virtual void attack() = 0;
    virtual void defend() = 0;
    virtual void fly() = 0;
    virtual void swim() = 0;
};

// GOOD: Segregated interfaces
class IAttacker {
  public:
    virtual void attack() = 0;
};
class IDefender {
  public:
    virtual void defend() = 0;
};
class IFlyer {
  public:
    virtual void fly() = 0;
};
class ISwimmer {
  public:
    virtual void swim() = 0;
};

2. Dependency Inversion Principle (DIP)

Depend on abstractions (interfaces) rather than concrete implementations:

dependency_inversion.cpp
// BAD: Depending on concrete class
class PaymentService {
  CreditCardProcessor processor; // Concrete dependency
};

// GOOD: Depending on interface
class PaymentService {
  IPaymentProcessor& processor; // Abstract dependency
};

Interfaces vs Abstract Classes

Aspect Interfaces Abstract Classes
Definition Pure abstract class (only pure virtual functions) Can have implemented methods and data members
Data Members Should not have non-static data members Can have data members
Inheritance Multiple interface inheritance supported Single class inheritance (multiple interfaces ok)
Purpose Define contracts and capabilities Provide partial implementation and interface
Usage "Can-do" relationships (capabilities) "Is-a" relationships (inheritance)

Advanced Interface Techniques

1. Default Implementations (C++11)

default_implementations.cpp
class ILogger {
  public:
    virtual void log(const string& message) = 0;
    virtual void logError(const string& message) {
      log("ERROR: " + message);
    }
    virtual ~ILogger() = default;
};

2. Interface with Static Methods

static_interface.cpp
class IFactory {
  public:
    virtual Product* createProduct() = 0;
    static IFactory* getDefaultFactory();
    virtual ~IFactory() = default;
};
Warning: Be cautious with default implementations in interfaces. They can lead to the "Fragile Base Class" problem where changes to the base interface break derived classes. Use them sparingly and only for truly default behavior.

Best Practices

  • Name Interfaces Clearly: Use "I" prefix or "able" suffix (IPayment, Drawable)
  • Keep Interfaces Focused: Follow Single Responsibility Principle
  • Use Virtual Destructors: Always declare virtual destructors in interfaces
  • Program to Interfaces: Use interface types in function parameters and returns
  • Document Contracts: Clearly document preconditions, postconditions, and invariants
  • Test Interfaces: Create interface tests that all implementations must pass
  • Avoid Data Members: Interfaces should define behavior, not state
Tip: When designing interfaces, think about the clients that will use them rather than the implementations that will provide them. Good interfaces make client code simple, clear, and decoupled from implementation details.

Common Interface Patterns

1. Factory Interface

For creating objects without specifying concrete classes.

2. Strategy Interface

For defining families of interchangeable algorithms.

3. Observer Interface

For implementing publish-subscribe communication.

4. Command Interface

For encapsulating requests as objects.

5. Repository Interface

For abstracting data access operations.

Note: Interfaces are the foundation of many design patterns. Understanding interfaces is crucial for applying patterns like Strategy, Factory, Observer, and many others effectively in C++.

Exception Handling

Exception Handling: A mechanism in C++ that allows programs to handle runtime errors and exceptional conditions in a structured and controlled manner. It separates error handling code from normal program logic, making code more readable and maintainable.
Exception Safety: The guarantee that a program remains in a valid state when exceptions occur. There are different levels of exception safety: basic, strong, and no-throw guarantees.

Why Exception Handling?

Exception handling provides a robust way to deal with errors and exceptional conditions:

  • Separation of Concerns: Error handling code is separated from normal logic
  • Propagation: Errors can be propagated up the call stack automatically
  • Resource Management: Proper cleanup with RAII (Resource Acquisition Is Initialization)
  • Type Safety: Exceptions are type-safe unlike error codes
  • Unrecoverable Errors: Handle errors that can't be handled locally
  • Debugging: Provide detailed error information with stack unwinding
Note: Exception handling should be used for exceptional conditions - situations that are unexpected and prevent normal program continuation. Don't use exceptions for normal control flow.

Exception Handling Syntax and Mechanism

Basic Syntax

exception_syntax.cpp
try {
  // Code that might throw exceptions
  // This block is monitored for exceptions
}
catch (const ExceptionType1& e) {
  // Handle ExceptionType1
}
catch (const ExceptionType2& e) {
  // Handle ExceptionType2
}
catch (...) {
  // Handle any other exceptions
}

Exception Handling Flow

  1. Throw: An exception is thrown using the throw keyword
  2. Unwind: The call stack is unwound until a matching catch block is found
  3. Catch: The exception is caught and handled by the appropriate catch block
  4. Continue: Program continues after the try-catch block

Example: Basic Exception Handling

Let's demonstrate basic exception handling with different scenarios:

basic_exceptions.cpp
#include
#include
#include
using namespace std;

// Custom exception class
class DivisionByZeroError : public exception {
  private:
    string message;
  public:
    DivisionByZeroError(const string& msg) : message(msg) {}
    const char* what() const noexcept override {
      return message.c_str();
    }
};

// Function that might throw exceptions
double safeDivide(double numerator, double denominator) {
  if (denominator == 0) {
    // Throw custom exception
    throw DivisionByZeroError("Division by zero attempted in safeDivide");
  }
  if (numerator == 0 && denominator == 0) {
    // Throw standard exception
    throw invalid_argument("Both numerator and denominator are zero");
  }
  return numerator / denominator;
}

int main() {
  cout << "=== Basic Exception Handling Demonstration ===" << endl << endl;

  // Test case 1: Normal division
  cout << "Test 1: Normal division (10 / 2)" << endl;
  try {
    double result = safeDivide(10, 2);
    cout << "Result: " << result << endl;
  }
  catch (const exception& e) {
    cout << "Exception caught: " << e.what() << endl;
  }
  cout << "Program continues normally after try-catch" << endl << endl;

  // Test case 2: Division by zero
  cout << "Test 2: Division by zero (5 / 0)" << endl;
  try {
    double result = safeDivide(5, 0);
    cout << "Result: " << result << endl;
  }
  catch (const DivisionByZeroError& e) {
    cout << "Custom exception caught: " << e.what() << endl;
  }
  catch (const exception& e) {
    cout << "Generic exception caught: " << e.what() << endl;
  }
  cout << "Program continues normally after handling division by zero" << endl << endl;

  // Test case 3: Multiple exception types
  cout << "Test 3: Zero divided by zero (0 / 0)" << endl;
  try {
    double result = safeDivide(0, 0);
    cout << "Result: " << result << endl;
  }
  catch (const DivisionByZeroError& e) {
    cout << "DivisionByZeroError caught: " << e.what() << endl;
  }
  catch (const invalid_argument& e) {
    cout << "Invalid argument caught: " << e.what() << endl;
  }
  catch (const exception& e) {
    cout << "Generic exception caught: " << e.what() << endl;
  }
  cout << "Program continues normally after handling invalid argument" << endl << endl;

  // Test case 4: Unhandled exception demonstration
  cout << "Test 4: Nested exception handling" << endl;
  try {
    try {
      throw runtime_error("Inner exception");
    }
    catch (const runtime_error& e) {
      cout << "Inner catch: " << e.what() << endl;
      // Rethrow the exception
      throw; // Rethrows the current exception
    }
  }
  catch (const runtime_error& e) {
    cout << "Outer catch: " << e.what() << endl;
  }

  return 0;
}
=== Basic Exception Handling Demonstration ===

Test 1: Normal division (10 / 2)
Result: 5
Program continues normally after try-catch

Test 2: Division by zero (5 / 0)
Custom exception caught: Division by zero attempted in safeDivide
Program continues normally after handling division by zero

Test 3: Zero divided by zero (0 / 0)
Invalid argument caught: Both numerator and denominator are zero
Program continues normally after handling invalid argument

Test 4: Nested exception handling
Inner catch: Inner exception
Outer catch: Inner exception

Standard Exception Hierarchy

C++ provides a standard exception hierarchy in the header:

standard_exceptions.cpp
#include
#include

// Standard exception types:
// - logic_error: Errors preventable by programming
// - runtime_error: Errors detectable only at runtime

void demonstrateStandardExceptions() {
  try {
    // Logic errors
    throw invalid_argument("Invalid argument provided");
    // throw domain_error("Domain error occurred");
    // throw length_error("Length exceeds maximum");
    // throw out_of_range("Index out of range");
  }
  catch (const logic_error& e) {
    cout << "Logic error: " << e.what() << endl;
  }
  
  try {
    // Runtime errors
    throw runtime_error("Runtime error occurred");
    // throw range_error("Range error in computation");
    // throw overflow_error("Arithmetic overflow");
    // throw underflow_error("Arithmetic underflow");
  }
  catch (const runtime_error& e) {
    cout << "Runtime error: " << e.what() << endl;
  }
}

RAII (Resource Acquisition Is Initialization)

RAII is a fundamental C++ technique that ensures proper resource cleanup even when exceptions occur:

raii_example.cpp
#include
#include
#include
using namespace std;

class FileHandler {
  private:
    ofstream file;
    string filename;
  public:
    FileHandler(const string& name) : filename(name) {
      file.open(filename);
      if (!file.is_open()) {
        throw runtime_error("Cannot open file: " + filename);
      }
      cout << "File opened: " << filename << endl;
    }
    
    ~FileHandler() {
      if (file.is_open()) {
        file.close();
        cout << "File closed: " << filename << endl;
      }
    }
    
    void write(const string& data) {
      if (!file.is_open()) {
        throw runtime_error("File is not open");
      }
      file << data << endl;
      if (file.fail()) {
        throw runtime_error("Write failed");
      }
    }
};

void processFileWithRAII() {
  // RAII: Resource is acquired in constructor and automatically
  // released in destructor, even if exceptions occur
  FileHandler file("data.txt");
  file.write("Hello, World!");
  file.write("This is RAII in action.");
  // Even if an exception occurs here, the file will be closed properly
  // because the destructor will be called during stack unwinding
}

int main() {
  try {
    processFileWithRAII();
  }
  catch (const exception& e) {
    cout << "Exception caught: " << e.what() << endl;
  }
  return 0;
}
File opened: data.txt
File closed: data.txt

Exception Safety Guarantees

Guarantee Level Description Example
No-throw Guarantee Operation never throws exceptions Destructors, swap operations
Strong Guarantee Operation succeeds or has no effect (transactional) vector::push_back with copyable elements
Basic Guarantee No resource leaks, objects in valid state Most standard library operations
No Guarantee No safety guarantees (avoid this) Poorly written C code

Advanced Exception Handling Features

1. Exception Specifications (C++11 and later)

exception_specifications.cpp
// C++11 noexcept specification
void noThrowFunction() noexcept {
  // This function guarantees it won't throw exceptions
  // If it does, std::terminate is called
}

// Conditional noexcept
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(move(a))) && noexcept(a = move(b))) {
  // This swap is noexcept if T's move operations are noexcept
}

2. Nested Exceptions (C++11)

nested_exceptions.cpp
#include

void handleException() {
  try {
    throw runtime_error("Original error");
  }
  catch (...) {
    // Capture current exception and throw new one with nested info
    throw_with_nested(logic_error("Additional context"));
  }
}

void printException(const exception& e, int level = 0) {
  cerr << string(level, ' ') << "Exception: " << e.what() << endl;
  try {
    rethrow_if_nested(e);
  }
  catch (const exception& nested) {
    printException(nested, level + 1);
  }
}

Best Practices

  • Use RAII: Always manage resources with RAII to prevent leaks
  • Throw by Value, Catch by Reference: Avoid object slicing and enable polymorphism
  • Derive from std::exception: Create custom exception classes from the standard hierarchy
  • Be Specific in Catch Blocks: Catch specific exceptions before general ones
  • Don't Throw from Destructors: Destructors should be noexcept
  • Use noexcept Appropriately: Mark functions that truly won't throw
  • Document Exception Guarantees: Clearly state what exceptions functions can throw
  • Avoid Exception Masking: Don't catch exceptions you can't handle
Warning: Never allow exceptions to escape from destructors. If a destructor throws an exception during stack unwinding (when another exception is already active), the program will call std::terminate and exit abruptly.

Common Exception Handling Patterns

1. Resource Management Pattern

Use smart pointers and RAII classes to manage resources automatically.

2. Exception Translation Pattern

Catch low-level exceptions and throw higher-level, more meaningful ones.

3. Null Object Pattern

Return a harmless null object instead of throwing exceptions in some cases.

4. Retry Pattern

Attempt an operation multiple times before giving up.

retry_pattern.cpp
template<typename Func>
auto retry(Func func, int maxAttempts) -> decltype(func()) {
  for (int attempt = 1; attempt <= maxAttempts; ++attempt) {
    try {
      return func();
    }
    catch (const exception& e) {
      if (attempt == maxAttempts) throw;
      cout << "Attempt " << attempt << " failed: " << e.what() << ", retrying..." << endl;
    }
  }
  throw runtime_error("All retry attempts failed");
}
Tip: When designing functions, consider the exception safety guarantees you can provide. Strive for at least the basic guarantee (no resource leaks), and the strong guarantee whenever possible. Document these guarantees clearly for users of your code.

Performance Considerations

  • Zero-Cost When Not Thrown: Exception handling has minimal overhead when no exceptions occur
  • Expensive When Thrown: Throwing exceptions is expensive due to stack unwinding
  • Use for Exceptional Conditions: Don't use exceptions for normal control flow
  • Consider Error Codes: For performance-critical paths where exceptions are frequent
  • noexcept Optimization: Compilers can optimize noexcept functions better
Note: Modern C++ exception handling is designed to have minimal performance impact when no exceptions are thrown. The cost is primarily in the binary size due to exception handling tables, not in runtime performance during normal execution.

Templates

Templates: A powerful C++ feature that allows writing generic code that works with any data type. Templates enable writing functions and classes that operate on different types without duplicating code, providing type safety and performance benefits.

Why Templates?

Templates provide a way to write flexible and reusable code:

  • Code Reusability: Write once, use with multiple types
  • Type Safety: Compile-time type checking
  • Performance: No runtime overhead - code generated at compile time
  • Generic Programming: Write algorithms that work with any data type
  • STL Foundation: Standard Template Library is built on templates
  • Avoid Code Duplication: No need to write similar functions for different types
Note: Templates are a compile-time mechanism. The compiler generates specific versions of template code for each type used, a process called "template instantiation."

Function Templates

Basic Function Template Syntax

function_template_syntax.cpp
template <typename T>
returnType functionName(T parameter) {
  // Function body using type T
}

Example: Simple Function Template

simple_function_template.cpp
#include
using namespace std;

// Template function to find maximum of two values
// Explanation: 'typename T' means T can be any type
// The compiler will generate specific versions for each type used
template <typename T>
T getMax(T a, T b) {
  return (a > b) ? a : b;
}

int main() {
  // Using with integers
  // Explanation: Compiler generates getMax(int, int)
  int intMax = getMax(10, 20);
  cout << "Max of 10 and 20: " << intMax << endl;

  // Using with doubles
  // Explanation: Compiler generates getMax(double, double)
  double doubleMax = getMax(3.14, 2.71);
  cout << "Max of 3.14 and 2.71: " << doubleMax << endl;

  // Using with characters
  char charMax = getMax('x', 'y');
  cout << "Max of 'x' and 'y': " << charMax << endl;

  return 0;
}
Max of 10 and 20: 20
Max of 3.14 and 2.71: 3.14
Max of 'x' and 'y': y

Class Templates

Basic Class Template Syntax

class_template_syntax.cpp
template <typename T>
class ClassName {
  private:
    T data;
  public:
    ClassName(T value);
    T getData();
    void setData(T value);
};

// Member function definition outside class
template <typename T>
ClassName::ClassName(T value) : data(value) {}

template <typename T>
T ClassName::getData() { return data; }

Example: Simple Class Template

simple_class_template.cpp
#include
using namespace std;

// Template class for a Box that can hold any type
// Explanation: T represents the type of data stored in the Box
template <typename T>
class Box {
  private:
    T content;
  public:
    // Constructor
    Box(T item) : content(item) {
      cout << "Box created with type: " << typeid(T).name() << endl;
    }
    
    // Get the content
    T getContent() const {
      return content;
    }
    
    // Set the content
    void setContent(T item) {
      content = item;
    }
    
    // Display content
    void display() const {
      cout << "Box contains: " << content << endl;
    }
};

int main() {
  // Create Box with integer
  // Explanation: Box is a specific type generated by compiler
  Box<int> intBox(42);
  intBox.display();

  // Create Box with string
  Box<string> stringBox("Hello Templates!");
  stringBox.display();

  // Create Box with double
  Box<double> doubleBox(3.14159);
  doubleBox.display();

  return 0;
}
Box created with type: i
Box contains: 42
Box created with type: NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
Box contains: Hello Templates!
Box created with type: d
Box contains: 3.14159

Multiple Template Parameters

multiple_parameters.cpp
#include
using namespace std;

// Function template with two different types
template <typename T1, typename T2>
void printPair(T1 first, T2 second) {
  cout << "First: " << first << ", Second: " << second << endl;
}

// Class template with two types
template <typename T1, typename T2>
class Pair {
  private:
    T1 first;
    T2 second;
  public:
    Pair(T1 f, T2 s) : first(f), second(s) {}
    
    T1 getFirst() const { return first; }
    T2 getSecond() const { return second; }
    
    void display() const {
      cout << "Pair: (" << first << ", " << second << ")" << endl;
    }
};

int main() {
  // Using function template with different types
  printPair(10, "Hello");
  printPair(3.14, true);

  // Using class template with different types
  Pair<int, string> idName(1, "Alice");
  idName.display();

  Pair<string, double> nameScore("Bob", 95.5);
  nameScore.display();

  return 0;
}
First: 10, Second: Hello
First: 3.14, Second: 1
Pair: (1, Alice)
Pair: (Bob, 95.5)

Template Specialization

Function Template Specialization

function_specialization.cpp
#include
using namespace std;

// General template
template <typename T>
void printType(T value) {
  cout << "Generic type: " << value << endl;
}

// Specialization for const char* (C-style strings)
template <>
void printType<const char*>(const char* value) {
  cout << "C-string: \"" << value << "\"" << endl;
}

int main() {
  printType(42); // Uses generic version
  printType(3.14); // Uses generic version
  printType("Hello"); // Uses specialized version for const char*
  printType(true); // Uses generic version

  return 0;
}
Generic type: 42
Generic type: 3.14
C-string: "Hello"
Generic type: 1

Class Template Specialization

class_specialization.cpp
#include
using namespace std;

// General template
template <typename T>
class Wrapper {
  private:
    T data;
  public:
    Wrapper(T d) : data(d) {}
    void print() {
      cout << "Wrapper value: " << data << endl;
    }
};

// Specialization for bool
template <>
class Wrapper<bool> {
  private:
    bool data;
  public:
    Wrapper(bool d) : data(d) {}
    void print() {
      cout << "Boolean Wrapper: " << (data ? "TRUE" : "FALSE") << endl;
    }
};

int main() {
  Wrapper<int> intWrap(100);
  intWrap.print();

  Wrapper<bool> boolWrap(true);
  boolWrap.print();

  Wrapper<bool> boolWrap2(false);
  boolWrap2.print();

  return 0;
}
Wrapper value: 100
Boolean Wrapper: TRUE
Boolean Wrapper: FALSE

Non-Type Template Parameters

non_type_parameters.cpp
#include
using namespace std;

// Template with non-type parameter (integer)
template <typename T, int size>
class FixedArray {
  private:
    T data[size];
  public:
    FixedArray() {
      // Initialize array
      for (int i = 0; i < size; i++) {
        data[i] = T(); // Default value
      }
    }
    
    void set(int index, T value) {
      if (index >= 0 && index < size) {
        data[index] = value;
      }
    }
    
    T get(int index) const {
      if (index >= 0 && index < size) {
        return data[index];
      }
      return T();
    }
    
    void print() const {
      cout << "Array[" << size << "]: ";
      for (int i = 0; i < size; i++) {
        cout << data[i] << " ";
      }
      cout << endl;
    }
};

int main() {
  // Create arrays of different sizes and types
  FixedArray<int, 5> intArray;
  intArray.set(0, 10);
  intArray.set(1, 20);
  intArray.print();

  FixedArray<double, 3> doubleArray;
  doubleArray.set(0, 1.1);
  doubleArray.set(1, 2.2);
  doubleArray.set(2, 3.3);
  doubleArray.print();

  return 0;
}
Array[5]: 10 20 0 0 0
Array[3]: 1.1 2.2 3.3

Template Type Deduction

type_deduction.cpp
#include
using namespace std;

template <typename T>
void checkType(T value) {
  cout << "Type: " << typeid(T).name()
       << ", Value: " << value << endl;
}

int main() {
  // Compiler deduces types automatically
  checkType(42); // T deduced as int
  checkType(3.14); // T deduced as double
  checkType("Hello"); // T deduced as const char*
  checkType(true); // T deduced as bool

  // Explicit type specification
  checkType<double>(5); // T explicitly set to double, 5 converted to 5.0
  checkType<int>(3.14); // T explicitly set to int, 3.14 converted to 3

  return 0;
}
Type: i, Value: 42
Type: d, Value: 3.14
Type: PKc, Value: Hello
Type: b, Value: 1
Type: d, Value: 5
Type: i, Value: 3

Template Compilation Process

Step Description
Template Definition Write template code (not compiled yet)
Template Instantiation Compiler generates specific code when template is used
Type Checking Compiler checks if operations are valid for the type
Code Generation Compiler generates machine code for the specific type
Warning: Template errors can be difficult to read because they occur during instantiation. The error messages often show the internal template code rather than your original code. Modern compilers are getting better at providing clearer error messages.

Best Practices

  • Use Descriptive Names: Use meaningful template parameter names (T, U vs Type1, Type2)
  • Place in Header Files: Template definitions usually belong in header files
  • Document Requirements: Document what operations the template type must support
  • Use Concepts (C++20): Use concepts to specify template requirements clearly
  • Avoid Complex Logic: Keep template code simple and focused
  • Test with Different Types: Test templates with various types to ensure correctness
  • Use auto (C++14+): Use auto return type deduction for complex template functions
Tip: When you get a template compilation error, look for the first error message that references your code (not the standard library internals). The error is usually that you're using a type that doesn't support the operations the template requires.

Common Template Patterns

1. Generic Algorithms

Write algorithms that work with any container type.

2. Policy-Based Design

Use template parameters to specify behavior policies.

3. CRTP (Curiously Recurring Template Pattern)

A class derived from a template base using itself as parameter.

4. Type Traits

Template classes that provide information about types at compile time.

Note: Templates are the foundation of generic programming in C++. The Standard Template Library (STL) is built entirely on templates, providing containers, algorithms, and iterators that work with any data type.

Standard Template Library (STL)

Standard Template Library (STL): A powerful library in C++ that provides a collection of template classes and functions for common data structures and algorithms. The STL is built on three core components: containers, algorithms, and iterators.

Why STL?

The STL provides ready-to-use, efficient, and type-safe components:

  • Reusability: Pre-built data structures and algorithms
  • Efficiency: Highly optimized implementations
  • Type Safety: Compile-time type checking
  • Consistency: Uniform interface across components
  • Productivity: Faster development with tested components
  • Generic Programming: Works with any data type
Note: The STL is part of the C++ Standard Library and is included with all modern C++ compilers. It represents one of the most powerful features of C++ for practical programming.

STL Components Overview

Component Description Examples
Containers Data structures that store collections of objects vector, list, map, set
Algorithms Functions that operate on containers sort, find, copy, transform
Iterators Objects that traverse through containers begin(), end(), forward, bidirectional
Functors Function objects that can be used as parameters less, greater, custom function objects

Sequence Containers

1. vector - Dynamic Array

vector_example.cpp
#include
#include
using namespace std;

int main() {
  // Create a vector of integers
  vector<int> numbers;

  // Add elements
  numbers.push_back(10);
  numbers.push_back(20);
  numbers.push_back(30);

  // Access elements
  cout << "First element: " << numbers[0] << endl;
  cout << "Size: " << numbers.size() << endl;

  // Iterate using range-based for loop (C++11)
  cout << "All elements: ";
  for (int num : numbers) {
    cout << num << " ";
  }
  cout << endl;

  return 0;
}
First element: 10
Size: 3
All elements: 10 20 30

2. list - Doubly Linked List

list_example.cpp
#include
#include
using namespace std;

int main() {
  list<string> names;

  // Add elements to both ends
  names.push_back("Alice");
  names.push_front("Bob");
  names.push_back("Charlie");

  // Iterate using iterators
  cout << "Names: ";
  for (auto it = names.begin(); it != names.end(); it++) {
    cout << *it << " ";
  }
  cout << endl;

  return 0;
}
Names: Bob Alice Charlie

Associative Containers

1. map - Key-Value Pairs

map_example.cpp
#include
#include
using namespace std;

int main() {
  // Create a map of student IDs to names
  map<int, string> students;

  // Insert key-value pairs
  students[101] = "Alice";
  students[102] = "Bob";
  students[103] = "Charlie";

  // Access and display
  cout << "Student 102: " << students[102] << endl;

  // Iterate through all elements
  cout << "All students:" << endl;
  for (const auto& pair : students) {
    cout << "ID: " << pair.first << ", Name: " << pair.second << endl;
  }

  return 0;
}
Student 102: Bob
All students:
ID: 101, Name: Alice
ID: 102, Name: Bob
ID: 103, Name: Charlie

2. set - Unique Elements Collection

set_example.cpp
#include
#include
using namespace std;

int main() {
  set<int> uniqueNumbers;

  // Insert elements (duplicates are ignored)
  uniqueNumbers.insert(5);
  uniqueNumbers.insert(2);
  uniqueNumbers.insert(5); // Duplicate - ignored
  uniqueNumbers.insert(8);
  uniqueNumbers.insert(2); // Duplicate - ignored

  // Display (automatically sorted)
  cout << "Unique numbers: ";
  for (int num : uniqueNumbers) {
    cout << num << " ";
  }
  cout << endl;

  return 0;
}
Unique numbers: 2 5 8

STL Algorithms

1. Sorting and Searching

algorithms_example.cpp
#include
#include
#include
using namespace std;

int main() {
  vector<int> numbers = {5, 2, 8, 1, 9};

  // Sort the vector
  sort(numbers.begin(), numbers.end());
  
  cout << "Sorted numbers: ";
  for (int num : numbers) {
    cout << num << " ";
  }
  cout << endl;

  // Find an element
  auto it = find(numbers.begin(), numbers.end(), 8);
  if (it != numbers.end()) {
    cout << "Found 8 at position: " << distance(numbers.begin(), it) << endl;
  } else {
    cout << "8 not found" << endl;
  }

  return 0;
}
Sorted numbers: 1 2 5 8 9
Found 8 at position: 3

2. Counting and Accumulation

counting_example.cpp
#include
#include
#include
#include
using namespace std;

int main() {
  vector<int> numbers = {1, 2, 3, 4, 5, 2, 3, 2};

  // Count occurrences of 2
  int count2 = count(numbers.begin(), numbers.end(), 2);
  cout << "Number 2 appears " << count2 << " times" << endl;

  // Calculate sum
  int sum = accumulate(numbers.begin(), numbers.end(), 0);
  cout << "Sum of all numbers: " << sum << endl;

  return 0;
}
Number 2 appears 3 times
Sum of all numbers: 22

Iterators

iterators_example.cpp
#include
#include
using namespace std;

int main() {
  vector<string> fruits = {"apple", "banana", "cherry"};

  // Different ways to iterate
  cout << "Method 1 - Range-based for loop:" << endl;
  for (const string& fruit : fruits) {
    cout << fruit << " ";
  }
  cout << endl << endl;

  cout << "Method 2 - Using iterators:" << endl;
  for (auto it = fruits.begin(); it != fruits.end(); it++) {
    cout << *it << " ";
  }
  cout << endl << endl;

  cout << "Method 3 - Reverse iteration:" << endl;
  for (auto it = fruits.rbegin(); it != fruits.rend(); it++) {
    cout << *it << " ";
  }
  cout << endl;

  return 0;
}
Method 1 - Range-based for loop:
apple banana cherry

Method 2 - Using iterators:
apple banana cherry

Method 3 - Reverse iteration:
cherry banana apple

Common STL Algorithms

Algorithm Purpose Example
sort Sort elements in a range sort(v.begin(), v.end())
find Find element in a range find(v.begin(), v.end(), value)
copy Copy elements from one range to another copy(src.begin(), src.end(), dest.begin())
transform Apply function to each element transform(v.begin(), v.end(), result.begin(), func)
count Count elements with specific value count(v.begin(), v.end(), value)
accumulate Calculate sum of elements accumulate(v.begin(), v.end(), initial)

Function Objects (Functors)

functors_example.cpp
#include
#include
#include
using namespace std;

// Custom functor
class IsEven {
  public:
    bool operator()(int n) const {
      return n % 2 == 0;
    }
};

int main() {
  vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

  // Count even numbers using functor
  int evenCount = count_if(numbers.begin(), numbers.end(), IsEven());
  cout << "Even numbers count: " << evenCount << endl;

  // Using lambda function (C++11)
  int oddCount = count_if(numbers.begin(), numbers.end(),
    [](int n) { return n % 2 != 0; });
  cout << "Odd numbers count: " << oddCount << endl;

  return 0;
}
Even numbers count: 5
Odd numbers count: 5

Container Adapters

1. stack - LIFO Structure

stack_example.cpp
#include
#include
using namespace std;

int main() {
  stack<string> books;

  // Push elements
  books.push("C++ Primer");
  books.push("Effective C++");
  books.push("STL Tutorial");

  // Pop and display (LIFO order)
  cout << "Books in stack (LIFO):" << endl;
  while (!books.empty()) {
    cout << books.top() << endl;
    books.pop();
  }

  return 0;
}
Books in stack (LIFO):
STL Tutorial
Effective C++
C++ Primer

2. queue - FIFO Structure

queue_example.cpp
#include
#include
using namespace std;

int main() {
  queue<string> customers;

  // Add customers to queue
  customers.push("Alice");
  customers.push("Bob");
  customers.push("Charlie");

  // Serve customers (FIFO order)
  cout << "Serving customers (FIFO):" << endl;
  while (!customers.empty()) {
    cout << "Now serving: " << customers.front() << endl;
    customers.pop();
  }

  return 0;
}
Serving customers (FIFO):
Now serving: Alice
Now serving: Bob
Now serving: Charlie

STL Best Practices

  • Choose the Right Container: Select containers based on usage patterns
  • Use auto with Iterators: Let compiler deduce iterator types
  • Prefer Algorithms over Loops: Use STL algorithms for common operations
  • Reserve Space for vectors: Use reserve() to avoid reallocations
  • Use emplace Methods: Use emplace_back() for efficient construction
  • Check Container Emptyness: Use empty() instead of size() == 0
  • Understand Iterator Invalidation: Know when iterators become invalid
Warning: Be careful with iterator invalidation. Operations like insert() and erase() can invalidate iterators for some containers (especially vectors). Always check documentation or use the returned iterators from these operations.

Container Selection Guide

Use Case Recommended Container Reason
Fast random access vector Contiguous memory, O(1) access
Frequent insertions/deletions list O(1) insert/delete anywhere
Key-value mapping map Sorted key-value pairs
Unique elements set Automatic duplicate removal
LIFO operations stack Last-in-first-out semantics
FIFO operations queue First-in-first-out semantics
Tip: When in doubt, start with vector. It's the most generally useful container with good performance for most operations. Switch to other containers only when you have specific performance requirements that vector doesn't meet.

Common STL Headers

  • - Dynamic array
  • - Doubly linked list
  • - Key-value map
  • - Unique elements collection
  • - Algorithms like sort, find, transform
  • - Iterator utilities
  • - Function objects and operations
  • - Stack adapter
  • - Queue and priority_queue
Note: The STL is designed with efficiency in mind. Most operations have well-defined time complexities, and the library takes advantage of template specialization and other optimization techniques to provide high performance.

File Handling

File Handling: The process of reading from and writing to files on a storage device. C++ provides stream-based file handling through the fstream library, allowing programs to persistently store and retrieve data.

Why File Handling?

File handling enables programs to work with persistent data storage:

  • Data Persistence: Store data between program executions
  • Configuration Files: Read and write program settings
  • Data Processing: Process large datasets from files
  • Log Files: Record program activities and errors
  • Data Import/Export: Exchange data with other applications
  • Backup and Recovery: Save and restore program state
Note: C++ file handling is stream-based, similar to console I/O (cin/cout). The same stream operators (<< and >>) used for console I/O can be used for file I/O.

File Stream Classes

Class Purpose Header File
ifstream Input file stream (reading from files)
ofstream Output file stream (writing to files)
fstream File stream (both reading and writing)

File Opening Modes

Mode Description
ios::in Open for reading (default for ifstream)
ios::out Open for writing (default for ofstream)
ios::app Append to end of file
ios::trunc Truncate file if it exists
ios::binary Open in binary mode
ios::ate Open and seek to end of file

Writing to Files

Basic File Writing

file_writing.cpp
#include
#include
using namespace std;

int main() {
  // Create output file stream
  ofstream outFile;

  // Open file for writing
  // Explanation: Creates file if it doesn't exist, overwrites if it exists
  outFile.open("example.txt");

  // Check if file opened successfully
  if (!outFile) {
    cerr << "Error: Could not open file for writing!" << endl;
    return 1;
  }

  // Write data to file
  outFile << "Hello, File Handling!" << endl;
  outFile << "This is line 2." << endl;
  outFile << "Number: " << 42 << endl;
  outFile << "PI: " << 3.14159 << endl;

  // Close the file
  outFile.close();

  cout << "Data written to file successfully!" << endl;
  return 0;
}
Data written to file successfully!

Appending to Files

file_appending.cpp
#include
#include
using namespace std;

int main() {
  // Open file for appending
  // Explanation: ios::app mode adds to end without overwriting
  ofstream outFile("log.txt", ios::app);

  if (!outFile) {
    cerr << "Error opening log file!" << endl;
    return 1;
  }

  // Append log entries
  outFile << "Program started." << endl;
  outFile << "Processing data..." << endl;
  outFile << "Program finished." << endl;

  outFile.close();
  cout << "Log entries appended successfully!" << endl;

  return 0;
}
Log entries appended successfully!

Reading from Files

Reading Line by Line

file_reading.cpp
#include
#include
#include
using namespace std;

int main() {
  // Create input file stream
  ifstream inFile("example.txt");

  // Check if file opened successfully
  if (!inFile) {
    cerr << "Error: Could not open file for reading!" << endl;
    return 1;
  }

  string line;
  int lineNumber = 1;

  cout << "Reading file contents:" << endl;
  cout << "=====================" << endl;

  // Read file line by line
  while (getline(inFile, line)) {
    cout << "Line " << lineNumber << ": " << line << endl;
    lineNumber++;
  }

  // Close the file
  inFile.close();

  return 0;
}
Reading file contents:
=====================
Line 1: Hello, File Handling!
Line 2: This is line 2.
Line 3: Number: 42
Line 4: PI: 3.14159

Reading Word by Word

word_reading.cpp
#include
#include
using namespace std;

int main() {
  ifstream inFile("example.txt");

  if (!inFile) {
    cerr << "Error opening file!" << endl;
    return 1;
  }

  string word;
  int wordCount = 0;

  cout << "Words in file:" << endl;
  cout << "==============" << endl;

  // Read words one by one
  while (inFile >> word) {
    cout << word << endl;
    wordCount++;
  }

  cout << "Total words: " << wordCount << endl;

  inFile.close();
  return 0;
}
Words in file:
==============
Hello,
File
Handling!
This
is
line
2.
Number:
42
PI:
3.14159
Total words: 11

Checking File States

file_states.cpp
#include
#include
using namespace std;

void checkFile(const string& filename) {
  ifstream file(filename);

  cout << "Checking file: " << filename << endl;

  if (file.is_open()) {
    cout << "- File is open successfully" << endl;
  } else {
    cout << "- Failed to open file" << endl;
  }

  if (file.good()) {
    cout << "- File stream is in good state" << endl;
  }

  if (file.eof()) {
    cout << "- Reached end of file" << endl;
  }

  if (file.fail()) {
    cout << "- Logical error occurred" << endl;
  }

  if (file.bad()) {
    cout << "- Read/writing error occurred" << endl;
  }

  file.close();
}

int main() {
  checkFile("example.txt");
  cout << endl;
  checkFile("nonexistent.txt");

  return 0;
}
Checking file: example.txt
- File is open successfully
- File stream is in good state

Checking file: nonexistent.txt
- Failed to open file

Binary File Handling

binary_files.cpp
#include
#include
using namespace std;

struct Student {
  int id;
  char name[50];
  double gpa;
};

int main() {
  // Write binary data
  ofstream outFile("students.dat", ios::binary);

  Student s1 = {101, "Alice", 3.8};
  Student s2 = {102, "Bob", 3.5};

  outFile.write((char*)&s1, sizeof(Student));
  outFile.write((char*)&s2, sizeof(Student));

  outFile.close();
  cout << "Binary data written successfully!" << endl;

  // Read binary data
  ifstream inFile("students.dat", ios::binary);
  Student student;

  cout << "Reading student data:" << endl;
  while (inFile.read((char*)&student, sizeof(Student))) {
    cout << "ID: " << student.id
       << ", Name: " << student.name
       << ", GPA: " << student.gpa << endl;
  }

  inFile.close();
  return 0;
}
Binary data written successfully!
Reading student data:
ID: 101, Name: Alice, GPA: 3.8
ID: 102, Name: Bob, GPA: 3.5

File Position Pointers

file_position.cpp
#include
#include
using namespace std;

int main() {
  fstream file("data.txt", ios::in | ios::out | ios::trunc);

  // Write some data
  file << "1234567890" << endl;
  file << "ABCDEFGHIJ" << endl;

  // Get current position
  streampos pos = file.tellg();
  cout << "Current position: " << pos << endl;

  // Move to beginning
  file.seekg(0, ios::beg);
  cout << "Moved to beginning" << endl;

  // Read first 5 characters
  char buffer[6];
  file.read(buffer, 5);
  buffer[5] = '\0';
  cout << "First 5 chars: " << buffer << endl;

  // Move 10 bytes from current position
  file.seekg(10, ios::cur);
  pos = file.tellg();
  cout << "New position: " << pos << endl;

  file.close();
  return 0;
}
Current position: 22
Moved to beginning
First 5 chars: 12345
New position: 15

Error Handling in File Operations

error_handling.cpp
#include
#include
#include
using namespace std;

bool safeFileCopy(const string& source, const string& destination) {
  ifstream src(source, ios::binary);
  ofstream dest(destination, ios::binary);

  // Check if both files opened successfully
  if (!src) {
    cerr << "Error: Cannot open source file '" << source << "'" << endl;
    return false;
  }

  if (!dest) {
    cerr << "Error: Cannot create destination file '" << destination << "'" << endl;
    return false;
  }

  // Copy file content
  dest << src.rdbuf();

  // Check if copy was successful
  if (!dest) {
    cerr << "Error: Copy operation failed" << endl;
    return false;
  }

  cout << "File copied successfully: " << source
     << " -> " << destination << endl;
  return true;
}

int main() {
  if (safeFileCopy("example.txt", "copy.txt")) {
    cout << "Copy operation completed successfully!" << endl;
  } else {
    cout << "Copy operation failed!" << endl;
  }

  // Try with non-existent file
  safeFileCopy("nonexistent.txt", "output.txt");

  return 0;
}
File copied successfully: example.txt -> copy.txt
Copy operation completed successfully!
Error: Cannot open source file 'nonexistent.txt'

Best Practices

  • Always Check File Open: Verify files opened successfully before operations
  • Use RAII: Let destructors handle file closing automatically
  • Close Files Explicitly: Close files when done to free resources
  • Handle Errors Gracefully: Provide meaningful error messages
  • Use Binary Mode Carefully: Only use binary mode when necessary
  • Check File States: Monitor file stream states during operations
  • Use Relative Paths: Prefer relative paths for portability
  • Backup Important Files: Create backups before modifying critical files
Warning: Always check if file operations succeed. Never assume a file will open or read/write operations will work. File operations can fail due to permissions, disk space, network issues, or many other reasons.

Common File Operations

Operation Method Example
Check if file exists Try to open for reading ifstream file("name"); bool exists = file.is_open();
Get file size seekg and tellg file.seekg(0, ios::end); size = file.tellg();
Read entire file rdbuf into stringstream stringstream buffer; buffer << file.rdbuf();
Copy file Read source, write destination dest << src.rdbuf();
Append to file Open with ios::app mode ofstream file("name", ios::app);
Tip: For simple file reading, you can use the RAII pattern by creating file stream objects in conditions. Example: if (ifstream file{"data.txt"}) { /* file is open and ready */ }. The file will automatically close when it goes out of scope.

Useful File Handling Patterns

1. Configuration File Reader

Read key-value pairs from configuration files.

2. Log File Writer

Append timestamped log entries to files.

3. Data File Processor

Read, process, and write data in specific formats.

4. Backup File Creator

Create timestamped backup copies of important files.

Note: File handling is essential for most real-world applications. Whether it's reading configuration, writing logs, processing data, or saving user preferences, understanding file I/O is crucial for C++ developers.

Advanced OOP Concepts

Advanced OOP Concepts: Sophisticated object-oriented programming techniques that build upon basic OOP principles to create more flexible, maintainable, and efficient software designs. These concepts include design patterns, advanced inheritance, and modern C++ features.

Why Advanced OOP Concepts?

Advanced OOP concepts address complex software design challenges:

  • Design Patterns: Proven solutions to common design problems
  • Flexible Architecture: Systems that can evolve and extend easily
  • Code Reusability: Maximize code reuse through sophisticated techniques
  • Maintainability: Easier to modify and extend complex systems
  • Performance Optimization: Efficient object creation and management
  • Testability: Designs that are easier to test and verify
Note: Advanced OOP concepts build upon the fundamental principles of encapsulation, inheritance, and polymorphism, adding layers of sophistication for complex software systems.

Multiple Inheritance

Basic Multiple Inheritance

multiple_inheritance.cpp
#include
using namespace std;

class Printable {
  public:
    virtual void print() const = 0;
    virtual ~Printable() = default;
};

class Drawable {
  public:
    virtual void draw() const = 0;
    virtual ~Drawable() = default;
};

// Multiple inheritance: inherits from both Printable and Drawable
class Shape : public Printable, public Drawable {
  protected:
    string name;
  public:
    Shape(const string& n) : name(n) {}
    
    void print() const override {
      cout << "Printing shape: " << name << endl;
    }
    
    void draw() const override {
      cout << "Drawing shape: " << name << endl;
    }
};

int main() {
  Shape circle("Circle");
  
  // Use as Printable
  circle.print();
  
  // Use as Drawable
  circle.draw();
  
  // Use through base class pointers
  Printable* p = &circle;
  Drawable* d = &circle;
  
  p->print();
  d->draw();
  
  return 0;
}
Printing shape: Circle
Drawing shape: Circle
Printing shape: Circle
Drawing shape: Circle

Diamond Problem and Virtual Inheritance

virtual_inheritance.cpp
#include
using namespace std;

class Animal {
  protected:
    int age;
  public:
    Animal(int a) : age(a) {
      cout << "Animal constructor: age=" << age << endl;
    }
    virtual ~Animal() {
      cout << "Animal destructor" << endl;
    }
};

// Virtual inheritance to solve diamond problem
class Mammal : public virtual Animal {
  public:
    Mammal(int a) : Animal(a) {
      cout << "Mammal constructor" << endl;
    }
};

class WingedAnimal : public virtual Animal {
  public:
    WingedAnimal(int a) : Animal(a) {
      cout << "WingedAnimal constructor" << endl;
    }
};

// Bat inherits from both Mammal and WingedAnimal
// Without virtual inheritance, Animal would be constructed twice
class Bat : public Mammal, public WingedAnimal {
  public:
    Bat(int a) : Animal(a), Mammal(a), WingedAnimal(a) {
      cout << "Bat constructor" << endl;
    }
    
    void display() const {
      cout << "Bat age: " << age << endl;
    }
};

int main() {
  cout << "Creating Bat object:" << endl;
  Bat bat(5);
  bat.display();
  cout << endl;
  
  cout << "Bat object destruction:" << endl;
  return 0;
}
Creating Bat object:
Animal constructor: age=5
Mammal constructor
WingedAnimal constructor
Bat constructor
Bat age: 5

Bat object destruction:
Animal destructor

Factory Pattern

factory_pattern.cpp
#include
#include
#include
using namespace std;

// Product interface
class Document {
  public:
    virtual void open() = 0;
    virtual void save() = 0;
    virtual ~Document() = default;
};

// Concrete products
class TextDocument : public Document {
  public:
    void open() override {
      cout << "Opening Text Document" << endl;
    }
    void save() override {
      cout << "Saving Text Document" << endl;
    }
};

class SpreadsheetDocument : public Document {
  public:
    void open() override {
      cout << "Opening Spreadsheet Document" << endl;
    }
    void save() override {
      cout << "Saving Spreadsheet Document" << endl;
    }
};

// Factory class
class DocumentFactory {
  public:
    static unique_ptr createDocument(const string& type) {
      if (type == "text") {
        return make_unique();
      } else if (type == "spreadsheet") {
        return make_unique();
      }
      return nullptr;
    }
};

int main() {
  // Create documents using factory
  auto doc1 = DocumentFactory::createDocument("text");
  auto doc2 = DocumentFactory::createDocument("spreadsheet");
  
  if (doc1) {
    doc1->open();
    doc1->save();
  }
  
  cout << endl;
  
  if (doc2) {
    doc2->open();
    doc2->save();
  }
  
  return 0;
}
Opening Text Document
Saving Text Document

Opening Spreadsheet Document
Saving Spreadsheet Document

Singleton Pattern

singleton_pattern.cpp
#include
#include
using namespace std;

class Logger {
  private:
    static Logger* instance;
    string logFile;
    
    // Private constructor
    Logger() : logFile("app.log") {
      cout << "Logger instance created" << endl;
    }
    
    // Prevent copying
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
  
  public:
    // Static method to get instance
    static Logger* getInstance() {
      if (instance == nullptr) {
        instance = new Logger();
      }
      return instance;
    }
    
    void log(const string& message) {
      cout << "LOG [" << logFile << "]: " << message << endl;
    }
    
    void setLogFile(const string& filename) {
      logFile = filename;
    }
    
    static void destroy() {
      delete instance;
      instance = nullptr;
    }
};

// Initialize static member
Logger* Logger::instance = nullptr;

int main() {
  // Get logger instance
  Logger* logger1 = Logger::getInstance();
  logger1->log("Application started");
  
  // Get same instance again
  Logger* logger2 = Logger::getInstance();
  logger2->setLogFile("new_app.log");
  logger2->log("Configuration updated");
  
  // Both pointers point to same instance
  cout << "Are loggers the same? " << (logger1 == logger2 ? "Yes" : "No") << endl;
  
  // Clean up
  Logger::destroy();
  
  return 0;
}
Logger instance created
LOG [app.log]: Application started
LOG [new_app.log]: Configuration updated
Are loggers the same? Yes

Observer Pattern

observer_pattern.cpp
#include
#include
#include
using namespace std;

// Observer interface
class Observer {
  public:
    virtual void update(const string& message) = 0;
    virtual ~Observer() = default;
};

// Subject (Observable)
class NewsAgency {
  private:
    vector observers;
    string news;
  public:
    void addObserver(Observer* observer) {
      observers.push_back(observer);
    }
    
    void removeObserver(Observer* observer) {
      observers.erase(
        remove(observers.begin(), observers.end(), observer),
        observers.end()
      );
    }
    
    void setNews(const string& newNews) {
      news = newNews;
      notifyObservers();
    }
    
    void notifyObservers() {
      for (Observer* observer : observers) {
        observer->update(news);
      }
    }
};

// Concrete Observers
class NewsChannel : public Observer {
  private:
    string name;
  public:
    NewsChannel(const string& n) : name(n) {}
    
    void update(const string& message) override {
      cout << name << " broadcasting: " << message << endl;
    }
};

int main() {
  NewsAgency agency;
  
  // Create news channels
  NewsChannel channel1("CNN");
  NewsChannel channel2("BBC");
  NewsChannel channel3("Al Jazeera");
  
  // Register observers
  agency.addObserver(&channel1);
  agency.addObserver(&channel2);
  agency.addObserver(&channel3);
  
  // Set news - all observers get notified
  cout << "=== Breaking News ===" << endl;
  agency.setNews("Earthquake hits the region!");
  
  cout << endl << "=== Sports Update ===" << endl;
  agency.setNews("Local team wins championship!");
  
  return 0;
}
=== Breaking News ===
CNN broadcasting: Earthquake hits the region!
BBC broadcasting: Earthquake hits the region!
Al Jazeera broadcasting: Earthquake hits the region!

=== Sports Update ===
CNN broadcasting: Local team wins championship!
BBC broadcasting: Local team wins championship!
Al Jazeera broadcasting: Local team wins championship!

Strategy Pattern

strategy_pattern.cpp
#include
#include
using namespace std;

// Strategy interface
class PaymentStrategy {
  public:
    virtual void pay(double amount) = 0;
    virtual ~PaymentStrategy() = default;
};

// Concrete strategies
class CreditCardPayment : public PaymentStrategy {
  public:
    void pay(double amount) override {
      cout << "Paying $" << amount << " using Credit Card" << endl;
    }
};

class PayPalPayment : public PaymentStrategy {
  public:
    void pay(double amount) override {
      cout << "Paying $" << amount << " using PayPal" << endl;
    }
};

class BitcoinPayment : public PaymentStrategy {
  public:
    void pay(double amount) override {
      cout << "Paying $" << amount << " using Bitcoin" << endl;
    }
};

// Context class
class ShoppingCart {
  private:
    unique_ptr paymentStrategy;
    double totalAmount;
  public:
    ShoppingCart() : totalAmount(0) {}
    
    void addItem(double price) {
      totalAmount += price;
    }
    
    void setPaymentStrategy(unique_ptr strategy) {
      paymentStrategy = move(strategy);
    }
    
    void checkout() {
      if (paymentStrategy) {
        cout << "Checking out with total: $" << totalAmount << endl;
        paymentStrategy->pay(totalAmount);
      } else {
        cout << "No payment method selected!" << endl;
      }
    }
};

int main() {
  ShoppingCart cart;
  cart.addItem(25.50);
  cart.addItem(15.75);
  
  // Use different payment strategies
  cout << "=== Credit Card Payment ===" << endl;
  cart.setPaymentStrategy(make_unique());
  cart.checkout();
  
  cout << endl << "=== PayPal Payment ===" << endl;
  cart.setPaymentStrategy(make_unique());
  cart.checkout();
  
  cout << endl << "=== Bitcoin Payment ===" << endl;
  cart.setPaymentStrategy(make_unique());
  cart.checkout();
  
  return 0;
}
=== Credit Card Payment ===
Checking out with total: $41.25
Paying $41.25 using Credit Card

=== PayPal Payment ===
Checking out with total: $41.25
Paying $41.25 using PayPal

=== Bitcoin Payment ===
Checking out with total: $41.25
Paying $41.25 using Bitcoin

Best Practices for Advanced OOP

  • Prefer Composition over Inheritance: Use object composition for flexibility
  • Use Design Patterns Judiciously: Apply patterns only when they solve real problems
  • Follow SOLID Principles: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion
  • Minimize Multiple Inheritance: Use interfaces and composition instead
  • Use Smart Pointers: Prefer unique_ptr and shared_ptr for resource management
  • Design for Testability: Create classes that are easy to unit test
  • Document Design Decisions: Explain why certain patterns were chosen
Warning: Be cautious with the Singleton pattern - it can introduce global state and make testing difficult. Consider dependency injection as an alternative for better testability and flexibility.

Common Design Patterns

Pattern Purpose When to Use
Factory Create objects without specifying exact class When object creation logic is complex
Singleton Ensure only one instance of a class exists For shared resources like configuration
Observer Notify dependent objects of state changes For event handling and notifications
Strategy Define family of interchangeable algorithms When you need different variants of an algorithm
Adapter Make incompatible interfaces work together When integrating with legacy code
Tip: When learning design patterns, focus on understanding the problem they solve rather than memorizing the implementation. The same pattern can be implemented differently in various programming languages and contexts.

Advanced Inheritance Techniques

1. Private and Protected Inheritance

Control the visibility of base class members in derived classes.

2. CRTP (Curiously Recurring Template Pattern)

A class that inherits from a template class using itself as template parameter.

3. Mixin Classes

Small classes that provide specific functionality to be combined.

4. Policy-Based Design

Use template parameters to specify behavior policies.

Note: Advanced OOP concepts are tools, not goals. Use them to solve specific design problems, not just because they are "advanced." The best solution is often the simplest one that meets the requirements.

Memory Management

Memory Management: The process of controlling and coordinating how a program uses computer memory. In C++, memory management involves allocating, using, and freeing memory dynamically during program execution, with manual control over memory resources.

Why Memory Management?

Proper memory management is crucial for efficient and stable programs:

  • Dynamic Memory: Allocate memory at runtime for flexible data structures
  • Resource Control: Manage limited memory resources efficiently
  • Performance: Optimize memory usage for better performance
  • Prevent Leaks: Avoid memory leaks that can crash applications
  • Avoid Corruption: Prevent memory corruption and undefined behavior
  • Large Data: Handle data that doesn't fit in stack memory
Note: C++ gives programmers direct control over memory management, which provides great power but also requires great responsibility. Modern C++ encourages using smart pointers to automate memory management.

Memory Segments in C++

Memory Segment Purpose Lifetime Example
Stack Local variables, function calls Automatic (scope-based) int x = 10;
Heap Dynamic memory allocation Manual (programmer controlled) int* ptr = new int;
Static/Global Global and static variables Program duration static int count = 0;
Code Executable code Program duration Function definitions

Dynamic Memory Allocation

Basic new and delete

basic_allocation.cpp
#include
using namespace std;

int main() {
  // Allocate single integer on heap
  // Explanation: 'new' allocates memory and returns pointer
  int* singleInt = new int;
  *singleInt = 42;
  cout << "Single integer: " << *singleInt << endl;

  // Allocate array of integers
  int* intArray = new int[5];
  for (int i = 0; i < 5; i++) {
    intArray[i] = i * 10;
  }

  cout << "Array elements: ";
  for (int i = 0; i < 5; i++) {
    cout << intArray[i] << " ";
  }
  cout << endl;

  // FREE the allocated memory
  // Explanation: Every 'new' must have corresponding 'delete'
  delete singleInt;
  delete[] intArray;

  cout << "Memory successfully freed!" << endl;
  return 0;
}
Single integer: 42
Array elements: 0 10 20 30 40
Memory successfully freed!

Object Allocation

object_allocation.cpp
#include
using namespace std;

class Student {
  private:
    string name;
    int age;
  public:
    Student(const string& n, int a) : name(n), age(a) {
      cout << "Student constructor: " << name << endl;
    }
    
    ~Student() {
      cout << "Student destructor: " << name << endl;
    }
    
    void display() const {
      cout << "Student: " << name << ", Age: " << age << endl;
    }
};

int main() {
  // Allocate single object
  Student* student = new Student("Alice", 20);
  student->display();

  // Allocate array of objects
  Student* students = new Student[3] {
    Student("Bob", 21),
    Student("Charlie", 22),
    Student("Diana", 23)
  };

  cout << "Student array:" << endl;
  for (int i = 0; i < 3; i++) {
    students[i].display();
  }

  // Free memory - destructors are called
  delete student;
  delete[] students;

  cout << "All objects destroyed and memory freed!" << endl;
  return 0;
}
Student constructor: Alice
Student: Alice, Age: 20
Student constructor: Bob
Student constructor: Charlie
Student constructor: Diana
Student array:
Student: Bob, Age: 21
Student: Charlie, Age: 22
Student: Diana, Age: 23
Student destructor: Alice
Student destructor: Diana
Student destructor: Charlie
Student destructor: Bob
All objects destroyed and memory freed!

Common Memory Management Problems

1. Memory Leaks

memory_leak.cpp
#include
using namespace std;

void createLeak() {
  // Allocate memory but never free it
  // Explanation: This memory becomes unreachable and leaked
  int* leakedMemory = new int[1000];
  leakedMemory[0] = 42;
  cout << "Memory allocated but not freed - LEAK!" << endl;
  // Missing: delete[] leakedMemory;
}

int main() {
  createLeak();
  cout << "Function returned, but memory is still allocated" << endl;
  cout << "This is a memory leak!" << endl;
  return 0;
}
Memory allocated but not freed - LEAK!
Function returned, but memory is still allocated
This is a memory leak!

2. Dangling Pointers

dangling_pointer.cpp
#include
using namespace std;

int main() {
  int* ptr = new int(100);
  cout << "Original value: " << *ptr << endl;

  // Free the memory
  delete ptr;
  cout << "Memory freed" << endl;

  // DANGER: ptr now dangles - points to freed memory
  // Explanation: Using dangling pointer causes undefined behavior
  cout << "WARNING: Using dangling pointer - UNDEFINED BEHAVIOR!" << endl;
  // cout << "Dangling value: " << *ptr << endl; // DON'T DO THIS!

  // Safe practice: set pointer to nullptr after deletion
  ptr = nullptr;
  cout << "Pointer set to nullptr - safe now" << endl;

  return 0;
}
Original value: 100
Memory freed
WARNING: Using dangling pointer - UNDEFINED BEHAVIOR!
Pointer set to nullptr - safe now

3. Double Deletion

double_delete.cpp
#include
using namespace std;

int main() {
  int* ptr = new int(50);
  cout << "Value: " << *ptr << endl;

  // First delete - correct
  delete ptr;
  cout << "First delete - OK" << endl;

  // SECOND DELETE - DANGEROUS!
  // Explanation: Deleting already freed memory causes undefined behavior
  cout << "Attempting second delete - CRASH RISK!" << endl;
  // delete ptr; // DON'T DO THIS - can crash program!

  // Safe practice
  ptr = nullptr;
  delete ptr; // Safe: deleting nullptr is no-op
  cout << "Deleting nullptr is safe" << endl;

  return 0;
}
Value: 50
First delete - OK
Attempting second delete - CRASH RISK!
Deleting nullptr is safe

RAII (Resource Acquisition Is Initialization)

raii_example.cpp
#include
using namespace std;

class IntArray {
  private:
    int* data;
    size_t size;
  public:
    // Constructor acquires resource
    IntArray(size_t s) : size(s) {
      data = new int[size];
      cout << "Array allocated with size " << size << endl;
    }
    
    // Destructor releases resource
    ~IntArray() {
      delete[] data;
      cout << "Array deallocated" << endl;
    }
    
    // Prevent copying (or implement deep copy)
    IntArray(const IntArray&) = delete;
    IntArray& operator=(const IntArray&) = delete;
    
    void set(size_t index, int value) {
      if (index < size) {
        data[index] = value;
      }
    }
    
    int get(size_t index) const {
      return (index < size) ? data[index] : -1;
    }
    
    void print() const {
      cout << "Array: ";
      for (size_t i = 0; i < size; i++) {
        cout << data[i] << " ";
      }
      cout << endl;
    }
};

void testRAII() {
  // RAII: Resource acquired in constructor
  IntArray arr(5);
  
  // Use the array
  for (size_t i = 0; i < 5; i++) {
    arr.set(i, static_cast<int>(i * 10));
  }
  arr.print();
  
  // Resource automatically released when arr goes out of scope
  cout << "Function ending - array will be automatically destroyed" << endl;
}

int main() {
  cout << "=== RAII Demonstration ===" << endl;
  testRAII();
  cout << "Function returned - memory was automatically freed!" << endl;
  return 0;
}
=== RAII Demonstration ===
Array allocated with size 5
Array: 0 10 20 30 40
Function ending - array will be automatically destroyed
Array deallocated
Function returned - memory was automatically freed!

Memory Management Best Practices

Practice Description Example
Use RAII Acquire resources in constructors, release in destructors Smart pointers, container classes
Prefer Stack Use stack allocation when possible int x = 10; instead of new int(10)
nullptr after delete Set pointers to nullptr after deletion ptr = nullptr;
Match new/delete Use delete for new, delete[] for new[] delete single; delete[] array;
Check allocation Verify new succeeded (or use std::nothrow) if (ptr == nullptr) handle_error();

Memory Debugging Techniques

memory_debugging.cpp
#include
#include
using namespace std;

#ifdef _DEBUG
void* operator new(size_t size) {
  cout << "Allocating " << size << " bytes" << endl;
  return malloc(size);
}

void operator delete(void* memory) noexcept {
  cout << "Deallocating memory" << endl;
  free(memory);
}
#endif

class MemoryTracker {
  private:
    static int allocationCount;
  public:
    static void increment() { allocationCount++; }
    static void decrement() { allocationCount--; }
    static int getCount() { return allocationCount; }
};

int MemoryTracker::allocationCount = 0;

int main() {
  cout << "Initial allocations: " << MemoryTracker::getCount() << endl;
  
  int* ptr1 = new int(10);
  MemoryTracker::increment();
  
  int* ptr2 = new int(20);
  MemoryTracker::increment();
  
  cout << "Current allocations: " << MemoryTracker::getCount() << endl;
  
  delete ptr1;
  MemoryTracker::decrement();
  
  delete ptr2;
  MemoryTracker::decrement();
  
  cout << "Final allocations: " << MemoryTracker::getCount() << endl;
  
  if (MemoryTracker::getCount() != 0) {
    cout << "MEMORY LEAK DETECTED!" << endl;
  }
  
  return 0;
}
Initial allocations: 0
Current allocations: 2
Final allocations: 0

Common Memory Management Patterns

1. Object Pool Pattern

Reuse objects instead of creating and destroying them frequently.

2. Copy-on-Write

Share memory until modification is needed.

3. Memory Arena/Pool

Allocate large blocks and manage sub-allocations manually.

4. Reference Counting

Track references to objects and delete when count reaches zero.

Warning: Manual memory management is error-prone. Always prefer RAII and smart pointers in modern C++. Use raw new/delete only when absolutely necessary and always within well-designed resource-managing classes.

Tools for Memory Management

  • Valgrind: Memory leak detector for Linux
  • AddressSanitizer: Fast memory error detector
  • Visual Studio Debugger: Built-in memory diagnostics
  • Smart Pointers: std::unique_ptr, std::shared_ptr, std::weak_ptr
  • Containers: std::vector, std::string, etc. manage memory automatically
Tip: When debugging memory issues, use the "sandwich pattern": allocate memory, use it immediately, then free it. Avoid complex lifetime management across multiple functions until you're confident in your memory management skills.

Modern C++ Memory Management

Smart Pointers (C++11 and later)

  • std::unique_ptr: Exclusive ownership, automatic deletion
  • std::shared_ptr: Shared ownership with reference counting
  • std::weak_ptr: Non-owning reference to shared_ptr managed object
  • std::make_unique / std::make_shared: Safe object creation
Note: Modern C++ philosophy is to avoid manual memory management whenever possible. Use standard library containers and smart pointers to automate memory management and eliminate common errors like memory leaks and dangling pointers.

Smart Pointers

Smart Pointers: C++ objects that act like pointers but provide automatic memory management. They automatically delete the managed object when it's no longer needed, preventing memory leaks and making memory management safer and easier.

Why Smart Pointers?

Smart pointers solve common memory management problems:

  • Automatic Cleanup: No need to manually call delete
  • Memory Safety: Prevent memory leaks and dangling pointers
  • Exception Safety: Guarantee cleanup even when exceptions occur
  • Ownership Semantics: Clear ownership of dynamically allocated objects
  • RAII Compliance: Follow Resource Acquisition Is Initialization principle
  • Simpler Code: Reduce boilerplate memory management code
Note: Smart pointers are part of the C++ Standard Library since C++11. They are defined in the header and should be preferred over raw pointers for managing dynamically allocated memory.

Smart Pointer Types

Smart Pointer Ownership Use Case Header
std::unique_ptr Exclusive ownership Single owner scenarios
std::shared_ptr Shared ownership Multiple owners needed
std::weak_ptr Non-owning reference Break circular references
std::auto_ptr Exclusive (deprecated) Legacy code (avoid in new code)

std::unique_ptr - Exclusive Ownership

Basic unique_ptr Usage

unique_ptr_basic.cpp
#include
#include
using namespace std;

class Resource {
  private:
    string name;
  public:
    Resource(const string& n) : name(n) {
      cout << "Resource acquired: " << name << endl;
    }
    
    ~Resource() {
      cout << "Resource destroyed: " << name << endl;
    }
    
    void use() const {
      cout << "Using resource: " << name << endl;
    }
};

int main() {
  cout << "=== unique_ptr Demonstration ===" << endl;
  
  // Create unique_ptr using make_unique (C++14)
  // Explanation: make_unique is safer and more efficient than 'new'
  unique_ptr res1 = make_unique("Database Connection");
  res1->use();

  // unique_ptr cannot be copied (exclusive ownership)
  // unique_ptr res2 = res1; // COMPILE ERROR!

  // But it can be moved (transfer ownership)
  unique_ptr res2 = move(res1);
  cout << "After move:" << endl;
  cout << "res1 is " << (res1 ? "not null" : "null") << endl;
  cout << "res2 is " << (res2 ? "not null" : "null") << endl;

  if (res2) {
    res2->use();
  }

  // Memory automatically freed when res2 goes out of scope
  cout << "End of scope - automatic cleanup!" << endl;
  return 0;
}
=== unique_ptr Demonstration ===
Resource acquired: Database Connection
Using resource: Database Connection
After move:
res1 is null
res2 is not null
Using resource: Database Connection
End of scope - automatic cleanup!
Resource destroyed: Database Connection

unique_ptr with Arrays

unique_ptr_array.cpp
#include
#include
using namespace std;

int main() {
  cout << "=== unique_ptr with Arrays ===" << endl;
  
  // Create unique_ptr for array
  // Explanation: unique_ptr automatically uses delete[] for arrays
  unique_ptr<int[]> arr = make_unique<int[]>(5);
  
  // Initialize array
  for (int i = 0; i < 5; i++) {
    arr[i] = (i + 1) * 10;
  }
  
  // Use array
  cout << "Array elements: ";
  for (int i = 0; i < 5; i++) {
    cout << arr[i] << " ";
  }
  cout << endl;
  
  // Array automatically deleted when arr goes out of scope
  cout << "No manual delete needed!" << endl;
  return 0;
}
=== unique_ptr with Arrays ===
Array elements: 10 20 30 40 50
No manual delete needed!

std::shared_ptr - Shared Ownership

Basic shared_ptr Usage

shared_ptr_basic.cpp
#include
#include
using namespace std;

class SharedResource {
  private:
    string name;
  public:
    SharedResource(const string& n) : name(n) {
      cout << "SharedResource created: " << name << endl;
    }
    
    ~SharedResource() {
      cout << "SharedResource destroyed: " << name << endl;
    }
    
    void access() const {
      cout << "Accessing: " << name << endl;
    }
};

int main() {
  cout << "=== shared_ptr Demonstration ===" << endl;
  
  // Create shared_ptr using make_shared
  // Explanation: make_shared is more efficient than separate 'new'
  shared_ptr ptr1 = make_shared("Network Socket");
  
  // Check reference count
  cout << "Reference count: " << ptr1.use_count() << endl;
  
  // Create additional shared_ptrs that share ownership
  shared_ptr ptr2 = ptr1; // Copy increases reference count
  shared_ptr ptr3 = ptr1; // Another copy
  
  cout << "After copying - Reference count: " << ptr1.use_count() << endl;
  
  // All pointers can access the same object
  ptr1->access();
  ptr2->access();
  ptr3->access();
  
  // Reset some pointers (decrease reference count)
  ptr2.reset();
  cout << "After reset ptr2 - Reference count: " << ptr1.use_count() << endl;
  
  ptr3.reset();
  cout << "After reset ptr3 - Reference count: " << ptr1.use_count() << endl;
  
  // Object destroyed when last shared_ptr is destroyed
  cout << "End of scope - object will be destroyed when last pointer is gone" << endl;
  return 0;
}
=== shared_ptr Demonstration ===
SharedResource created: Network Socket
Reference count: 1
After copying - Reference count: 3
Accessing: Network Socket
Accessing: Network Socket
Accessing: Network Socket
After reset ptr2 - Reference count: 2
After reset ptr3 - Reference count: 1
End of scope - object will be destroyed when last pointer is gone
SharedResource destroyed: Network Socket

std::weak_ptr - Non-owning References

weak_ptr_example.cpp
#include
#include
using namespace std;

class Controller; // Forward declaration

class Display {
  public:
    weak_ptr controller;
    
    void showStatus() {
      if (auto ctrl = controller.lock()) {
        cout << "Display: Controller is alive" << endl;
      } else {
        cout << "Display: Controller has been destroyed" << endl;
      }
    }
};

class Controller {
  public:
    shared_ptr display;
    string name;
    
    Controller(const string& n) : name(n) {
      cout << "Controller created: " << name << endl;
    }
    
    ~Controller() {
      cout << "Controller destroyed: " << name << endl;
    }
    
    void connectDisplay(shared_ptr disp) {
      display = disp;
      disp->controller = shared_ptr(this);
    }
};

int main() {
  cout << "=== weak_ptr Demonstration ===" << endl;
  
  // Create objects
  auto display = make_shared();
  auto controller = make_shared("Main Controller");
  
  // Connect them
  controller->connectDisplay(display);
  
  // Test the connection
  display->showStatus();
  
  // Reset controller - display's weak_ptr becomes expired
  cout << "Resetting controller..." << endl;
  controller.reset();
  
  // Check weak_ptr again
  display->showStatus();
  
  cout << "No memory leak - weak_ptr doesn't prevent destruction" << endl;
  return 0;
}
=== weak_ptr Demonstration ===
Controller created: Main Controller
Display: Controller is alive
Resetting controller...
Controller destroyed: Main Controller
Display: Controller has been destroyed
No memory leak - weak_ptr doesn't prevent destruction

Smart Pointer Best Practices

Practice Description Example
Use make_unique/make_shared Safer and more efficient than direct new make_unique(args) instead of unique_ptr(new T(args))
Prefer unique_ptr Use unique_ptr by default for exclusive ownership unique_ptr for factory returns, local variables
Use shared_ptr sparingly Only when shared ownership is truly needed shared_ptr for cached objects, observer patterns
Use weak_ptr for caching Break circular references and for non-owning observations weak_ptr in observer patterns, cache implementations
Don't mix raw and smart pointers Avoid passing raw pointers from smart pointers Use get() only when necessary for legacy APIs

Converting Between Smart Pointers

smart_ptr_conversion.cpp
#include
#include
using namespace std;

class Data {
  public:
    int value;
    Data(int v) : value(v) {
      cout << "Data created: " << value << endl;
    }
    ~Data() {
      cout << "Data destroyed: " << value << endl;
    }
};

int main() {
  cout << "=== Smart Pointer Conversions ===" << endl;
  
  // 1. unique_ptr to shared_ptr
  unique_ptr uniqueData = make_unique(100);
  shared_ptr sharedData = move(uniqueData);
  cout << "Converted unique_ptr to shared_ptr" << endl;
  cout << "uniqueData is " << (uniqueData ? "valid" : "empty") << endl;
  
  // 2. shared_ptr to weak_ptr
  weak_ptr weakData = sharedData;
  cout << "Created weak_ptr from shared_ptr" << endl;
  
  // 3. Access weak_ptr
  if (auto locked = weakData.lock()) {
    cout << "Weak_ptr locked successfully, value: " << locked->value << endl;
  }
  
  // 4. Reset shared_ptr - weak_ptr becomes expired
  sharedData.reset();
  cout << "Reset shared_ptr" << endl;
  
  if (weakData.expired()) {
    cout << "Weak_ptr is now expired" << endl;
  }
  
  cout << "All conversions completed safely" << endl;
  return 0;
}
=== Smart Pointer Conversions ===
Data created: 100
Converted unique_ptr to shared_ptr
uniqueData is empty
Created weak_ptr from shared_ptr
Weak_ptr locked successfully, value: 100
Reset shared_ptr
Data destroyed: 100
Weak_ptr is now expired
All conversions completed safely

Custom Deleters

custom_deleters.cpp
#include
#include
#include
using namespace std;

// Custom deleter for FILE*
struct FileDeleter {
  void operator()(FILE* file) const {
    if (file) {
      fclose(file);
      cout << "File closed by custom deleter" << endl;
    }
  }
};

int main() {
  cout << "=== Custom Deleters ===" << endl;
  
  // 1. unique_ptr with custom deleter (function object)
  unique_ptr file1(fopen("test.txt", "w"));
  if (file1) {
    fprintf(file1.get(), "Hello, Custom Deleter!");
    cout << "File written successfully" << endl;
  }
  // File automatically closed by custom deleter

  // 2. unique_ptr with lambda deleter
  auto lambdaDeleter = [](int* ptr) {
    cout << "Custom lambda deleting: " << *ptr << endl;
    delete ptr;
  };
  
  unique_ptr<int, decltype(lambdaDeleter)> intPtr(new int(42), lambdaDeleter);
  cout << "Integer value: " << *intPtr << endl;
  // Automatically deleted by lambda

  // 3. shared_ptr with custom deleter
  shared_ptr sharedFile(
    fopen("shared.txt", "w"),
    [](FILE* f) {
      if (f) {
        fclose(f);
        cout << "Shared file closed" << endl;
      }
    }
  );

  cout << "All resources managed with custom deleters" << endl;
  return 0;
}
=== Custom Deleters ===
File written successfully
File closed by custom deleter
Integer value: 42
Custom lambda deleting: 42
All resources managed with custom deleters
Shared file closed

Common Smart Pointer Mistakes

Warning: Avoid these common smart pointer mistakes:
  • Circular References: shared_ptr cycles cause memory leaks - use weak_ptr to break cycles
  • Mixing Ownership: Don't create multiple smart pointers from the same raw pointer
  • Using get() Incorrectly: get() returns raw pointer - don't delete it or create new smart pointers from it
  • Exception Unsafe: Using 'new' directly instead of make_unique/make_shared can leak if constructor throws
  • Overusing shared_ptr: shared_ptr has overhead - use unique_ptr when exclusive ownership suffices
Tip: Follow the "Rule of Zero" - classes should not define any special member functions (destructor, copy/move operations) if they only use smart pointers and standard library types for resource management. Let the compiler generate them automatically.

Smart Pointer Performance

Smart Pointer Overhead When to Use
std::unique_ptr Zero overhead (same as raw pointer) Default choice, exclusive ownership
std::shared_ptr Small overhead (reference counting) Shared ownership required
std::weak_ptr Small overhead Breaking cycles, caching
Note: Smart pointers are one of the most important features added in modern C++. They make memory management automatic and exception-safe while maintaining performance. Always prefer smart pointers over raw pointers for owning pointers.

Move Semantics

Move Semantics: A C++11 feature that allows transferring resources from one object to another without expensive copying. Move semantics enable efficient transfer of ownership for dynamically allocated memory and other resources, significantly improving performance.

Why Move Semantics?

Move semantics solve performance problems with temporary objects and resource management:

  • Performance Optimization: Avoid expensive deep copies
  • Resource Transfer: Efficiently transfer ownership of resources
  • Temporary Objects: Handle temporary objects efficiently
  • RAII Enhancement: Better resource management with movable types
  • Standard Library Integration: STL containers use move semantics
  • Modern C++: Essential for writing efficient modern C++ code
Note: Move semantics work by "stealing" resources from source objects (rvalues) and transferring them to destination objects, leaving the source in a valid but unspecified state.

Lvalues and Rvalues

Category Description Examples
Lvalue Has identity and address variables, references, dereferenced pointers
Rvalue Temporary, no persistent identity literals, temporaries, function returns
Xvalue eXpiring value - can be moved from std::move result, function returning rvalue reference

Basic Move Semantics

Move Constructor and Move Assignment

basic_move_semantics.cpp
#include
#include
using namespace std;

class String {
  private:
    char* data;
    size_t length;
  public:
    // Constructor
    String(const char* str = "") {
      length = strlen(str);
      data = new char[length + 1];
      strcpy(data, str);
      cout << "Constructor: " << data << endl;
    }
    
    // Copy Constructor (deep copy)
    String(const String& other) {
      length = other.length;
      data = new char[length + 1];
      strcpy(data, other.data);
      cout << "Copy Constructor: " << data << endl;
    }
    
    // MOVE CONSTRUCTOR (C++11)
    // Explanation: Steals resources from temporary object
    String(String&& other) noexcept {
      // Steal resources from 'other'
      data = other.data;
      length = other.length;
      
      // Leave 'other' in valid but empty state
      other.data = nullptr;
      other.length = 0;
      
      cout << "Move Constructor: " << data << endl;
    }
    
    // MOVE ASSIGNMENT OPERATOR
    String& operator=(String&& other) noexcept {
      if (this != &other) {
        // Free existing resources
        delete[] data;
        
        // Steal resources from 'other'
        data = other.data;
        length = other.length;
        
        // Leave 'other' in valid state
        other.data = nullptr;
        other.length = 0;
        
        cout << "Move Assignment: " << data << endl;
      }
      return *this;
    }
    
    // Destructor
    ~String() {
      if (data) {
        cout << "Destructor: " << data << endl;
        delete[] data;
      } else {
        cout << "Destructor: (empty)" << endl;
      }
    }
    
    const char* c_str() const { return data; }
};

// Function that returns temporary
String createString() {
  return String("Temporary String");
}

int main() {
  cout << "=== Basic Move Semantics ===" << endl;
  
  // 1. Move constructor called for temporary
  cout << "Creating str1 from function return:" << endl;
  String str1 = createString();
  cout << "str1: " << str1.c_str() << endl << endl;
  
  // 2. Move assignment
  cout << "Move assignment example:" << endl;
  String str2;
  str2 = String("Another Temporary");
  cout << "str2: " << str2.c_str() << endl << endl;
  
  cout << "End of scope - automatic cleanup" << endl;
  return 0;
}
=== Basic Move Semantics ===
Creating str1 from function return:
Constructor: Temporary String
Move Constructor: Temporary String
Destructor: (empty)
str1: Temporary String

Move assignment example:
Constructor:
Constructor: Another Temporary
Move Assignment: Another Temporary
Destructor: (empty)
str2: Another Temporary

End of scope - automatic cleanup
Destructor: Another Temporary
Destructor: Temporary String

std::move - Converting Lvalues to Rvalues

std_move_example.cpp
#include
#include
#include
using namespace std;

class Buffer {
  private:
    int* data;
    size_t size;
  public:
    Buffer(size_t s) : size(s) {
      data = new int[size];
      for (size_t i = 0; i < size; i++) {
        data[i] = static_cast<int>(i);
      }
      cout << "Buffer constructed, size: " << size << endl;
    }
    
    // Move constructor
    Buffer(Buffer&& other) noexcept
      : data(other.data), size(other.size) {
      other.data = nullptr;
      other.size = 0;
      cout << "Buffer moved" << endl;
    }
    
    ~Buffer() {
      if (data) {
        cout << "Buffer destroyed, size: " << size << endl;
        delete[] data;
      } else {
        cout << "Empty buffer destroyed" << endl;
      }
    }
    
    size_t getSize() const { return size; }
};

int main() {
  cout << "=== std::move Demonstration ===" << endl;
  
  // Create a buffer
  Buffer buffer1(5);
  cout << "buffer1 size: " << buffer1.getSize() << endl << endl;
  
  // Use std::move to transfer ownership
  // Explanation: std::move converts lvalue to rvalue reference
  cout << "Using std::move to transfer buffer1:" << endl;
  Buffer buffer2 = move(buffer1);
  
  cout << "After move:" << endl;
  cout << "buffer1 size: " << buffer1.getSize() << endl;
  cout << "buffer2 size: " << buffer2.getSize() << endl << endl;
  
  // Example with STL containers
  vector<Buffer> buffers;
  
  cout << "Adding buffer to vector using move:" << endl;
  Buffer tempBuffer(3);
  buffers.push_back(move(tempBuffer));
  cout << "tempBuffer size after move: " << tempBuffer.getSize() << endl;
  cout << "Vector buffer size: " << buffers[0].getSize() << endl;
  
  cout << "End of scope" << endl;
  return 0;
}
=== std::move Demonstration ===
Buffer constructed, size: 5
buffer1 size: 5

Using std::move to transfer buffer1:
Buffer moved
After move:
buffer1 size: 0
buffer2 size: 5

Adding buffer to vector using move:
Buffer constructed, size: 3
Buffer moved
tempBuffer size after move: 0
Vector buffer size: 3
End of scope
Buffer destroyed, size: 3
Buffer destroyed, size: 5
Empty buffer destroyed

Rule of Five

rule_of_five.cpp
#include
#include
using namespace std;

class Resource {
  private:
    int* data;
    size_t size;
  public:
    // 1. Constructor
    Resource(size_t s = 0) : size(s) {
      data = (size > 0) ? new int[size] : nullptr;
      cout << "Default constructor, size: " << size << endl;
    }
    
    // 2. Copy Constructor
    Resource(const Resource& other) : size(other.size) {
      data = (size > 0) ? new int[size] : nullptr;
      if (data) {
        for (size_t i = 0; i < size; i++) {
          data[i] = other.data[i];
        }
      }
      cout << "Copy constructor, size: " << size << endl;
    }
    
    // 3. Copy Assignment
    Resource& operator=(const Resource& other) {
      if (this != &other) {
        // Free existing resource
        delete[] data;
        
        // Copy from other
        size = other.size;
        data = (size > 0) ? new int[size] : nullptr;
        if (data) {
          for (size_t i = 0; i < size; i++) {
            data[i] = other.data[i];
          }
        }
        cout << "Copy assignment, size: " << size << endl;
      }
      return *this;
    }
    
    // 4. Move Constructor
    Resource(Resource&& other) noexcept
      : data(other.data), size(other.size) {
      other.data = nullptr;
      other.size = 0;
      cout << "Move constructor, size: " << size << endl;
    }
    
    // 5. Move Assignment
    Resource& operator=(Resource&& other) noexcept {
      if (this != &other) {
        // Free existing resource
        delete[] data;
        
        // Steal resources from other
        data = other.data;
        size = other.size;
        
        // Leave other in valid state
        other.data = nullptr;
        other.size = 0;
        cout << "Move assignment, size: " << size << endl;
      }
      return *this;
    }
    
    // 6. Destructor
    ~Resource() {
      delete[] data;
      cout << "Destructor, size: " << size << endl;
    }
    
    size_t getSize() const { return size; }
};

int main() {
  cout << "=== Rule of Five Demonstration ===" << endl;
  
  // Test all operations
  Resource res1(3);
  Resource res2 = res1; // Copy constructor
  Resource res3 = move(res1); // Move constructor
  
  res2 = res3; // Copy assignment
  res3 = Resource(2); // Move assignment from temporary
  
  cout << "Final sizes - res1: " << res1.getSize()
     << ", res2: " << res2.getSize()
     << ", res3: " << res3.getSize() << endl;
  
  cout << "End of scope" << endl;
  return 0;
}
=== Rule of Five Demonstration ===
Default constructor, size: 3
Copy constructor, size: 3
Move constructor, size: 3
Copy assignment, size: 3
Default constructor, size: 2
Move assignment, size: 2
Destructor, size: 0
Final sizes - res1: 0, res2: 3, res3: 2
End of scope
Destructor, size: 2
Destructor, size: 3
Destructor, size: 0

Move Semantics with STL Containers

stl_move_semantics.cpp
#include
#include
#include
#include
using namespace std;

int main() {
  cout << "=== STL Containers and Move Semantics ===" << endl;
  
  // 1. vector with move semantics
  vector<string> strings;
  
  // Without move - creates temporary then copies
  cout << "Adding string without move:" << endl;
  string temp1 = "Hello World";
  strings.push_back(temp1);
  cout << "Original: " << temp1 << endl;
  cout << "In vector: " << strings[0] << endl << endl;
  
  // With move - transfers ownership
  cout << "Adding string with move:" << endl;
  string temp2 = "Goodbye World";
  strings.push_back(move(temp2));
  cout << "Original after move: '" << temp2 << "'" << endl;
  cout << "In vector: " << strings[1] << endl << endl;
  
  // 2. Efficient vector resizing with move
  cout << "Vector resize efficiency:" << endl;
  vector<string> largeVector;
  
  // Reserve space to avoid reallocations
  largeVector.reserve(100);
  
  // Add elements using emplace_back (avoids copies entirely)
  largeVector.emplace_back("Element 1");
  largeVector.emplace_back("Element 2");
  largeVector.emplace_back("Element 3");
  
  cout << "Vector size: " << largeVector.size() << endl;
  cout << "Vector capacity: " << largeVector.capacity() << endl << endl;
  
  // 3. Move entire vector
  cout << "Moving entire vector:" << endl;
  vector<string> newVector = move(largeVector);
  
  cout << "After move:" << endl;
  cout << "largeVector size: " << largeVector.size() << endl;
  cout << "newVector size: " << newVector.size() << endl;
  
  for (const auto& str : newVector) {
    cout << " - " << str << endl;
  }
  
  return 0;
}
=== STL Containers and Move Semantics ===
Adding string without move:
Original: Hello World
In vector: Hello World

Adding string with move:
Original after move: ''
In vector: Goodbye World

Vector resize efficiency:
Vector size: 3
Vector capacity: 100

Moving entire vector:
After move:
largeVector size: 0
newVector size: 3
- Element 1
- Element 2
- Element 3

Best Practices for Move Semantics

Practice Description Example
Use noexcept Mark move operations as noexcept when possible String(String&& other) noexcept
Follow Rule of Five Define all five special member functions if you define any Destructor, copy/move constructor, copy/move assignment
Use std::move wisely Only use std::move when you're done with an object return std::move(local_var); // Don't do this!
Prefer emplace_back Use emplace_back instead of push_back for efficiency vec.emplace_back(args) vs vec.push_back(T(args))
Check self-assignment Always check for self-assignment in move operations if (this != &other) { ... }

Common Move Semantics Mistakes

Warning: Avoid these common move semantics mistakes:
  • Using std::move on const objects: Move constructor won't be called
  • Moving from objects you still need: Leaves source in valid but unspecified state
  • Not marking move operations noexcept: STL can't use them optimally
  • Forgetting Rule of Five: Inconsistent behavior with resource management
  • Using std::move in return statements: Compiler already does Return Value Optimization
Tip: The compiler can perform Return Value Optimization (RVO) and Named Return Value Optimization (NRVO) to eliminate copies. Don't use std::move in return statements - let the compiler optimize naturally.

Performance Benefits

Scenario Without Move With Move Improvement
Returning large object Copy entire object Transfer ownership O(n) to O(1)
STL container resize Copy all elements Move all elements Much faster
Temporary objects Unnecessary copies Efficient transfers Eliminates overhead
Note: Move semantics are one of the most important features in modern C++. They enable writing highly efficient code without sacrificing safety or readability. Understanding move semantics is essential for writing professional C++ code.

Lambda Expressions

Lambda Expressions (also called lambda functions or closures) are anonymous functions that can be defined inline in your code. Introduced in C++11, they provide a concise way to create function objects without needing to define a separate named function.

What are Lambda Expressions?

Lambda expressions allow you to write short, disposable functions right where they're needed, making your code more readable and maintainable, especially when working with algorithms that require function objects.

Basic Lambda Syntax:

lambda_syntax.cpp
// Basic lambda expression syntax:
// [capture-clause] (parameters) -> return-type { body }

// Simple example:
auto lambda = []() {
  cout << "Hello from lambda!" << endl;
};
lambda(); // Call the lambda

Complete Lambda Expression Structure

Component Description Required
Capture Clause [] Specifies which variables from the surrounding scope are captured Yes
Parameters () List of parameters (like regular functions) No
Return Type -> type Explicit return type specification No
Body {} The function implementation Yes

Capture Clauses - The Heart of Lambdas

Capture clauses determine how the lambda accesses variables from the surrounding scope:

capture_clauses.cpp
#include
#include
#include
using namespace std;

int main() {
  int x = 10;
  int y = 20;
  int z = 30;

  // 1. Capture nothing
  auto lambda1 = []() {
    cout << "I capture nothing" << endl;
  };

  // 2. Capture by value (read-only copy)
  auto lambda2 = [x, y]() {
    cout << "Captured x=" << x << ", y=" << y << endl;
    // x = 5; // Error: x is const when captured by value
  };

  // 3. Capture by reference (can modify original)
  auto lambda3 = [&x, &y]() {
    cout << "Original x=" << x << ", y=" << y << endl;
    x = 100; // Modifies the original x
    y = 200; // Modifies the original y
  };

  // 4. Capture all by value
  auto lambda4 = [=]() {
    cout << "All by value: x=" << x << ", y=" << y << ", z=" << z << endl;
  };

  // 5. Capture all by reference
  auto lambda5 = [&]() {
    cout << "All by reference: x=" << x << ", y=" << y << ", z=" << z << endl;
    z = 300; // Modifies original z
  };

  // 6. Mixed capture: some by value, some by reference
  auto lambda6 = [=, &z]() { // All by value, but z by reference
    cout << "Mixed: x=" << x << " (value), z=" << z << " (reference)" << endl;
    z = 400; // Can modify z since it's captured by reference
  };

  // Execute lambdas
  lambda1();
  lambda2();
  cout << "Before lambda3: x=" << x << ", y=" << y << endl;
  lambda3();
  cout << "After lambda3: x=" << x << ", y=" << y << endl;
  lambda4();
  cout << "Before lambda5: z=" << z << endl;
  lambda5();
  cout << "After lambda5: z=" << z << endl;
  lambda6();
  cout << "After lambda6: z=" << z << endl;

  return 0;
}
I capture nothing
Captured x=10, y=20
Before lambda3: x=10, y=20
Original x=10, y=20
After lambda3: x=100, y=200
All by value: x=100, y=200, z=30
Before lambda5: z=30
All by reference: x=100, y=200, z=30
After lambda5: z=300
Mixed: x=100 (value), z=300 (reference)
After lambda6: z=400

Lambda Parameters and Return Types

lambda_parameters.cpp
#include
#include
using namespace std;

int main() {
  // Lambda with parameters and auto return type deduction
  auto add = [](int a, int b) {
    return a + b;
  };

  // Lambda with explicit return type
  auto divide = [](double a, double b) -> double {
    if (b == 0) return 0.0;
    return a / b;
  };

  // Lambda with multiple statements
  auto processString = [](string str) {
    cout << "Processing: " << str << endl;
    return str.length();
  };

  // Lambda that captures and takes parameters
  int multiplier = 5;
  auto multiply = [multiplier](int value) {
    return value * multiplier;
  };

  cout << "Add: " << add(10, 20) << endl;
  cout << "Divide: " << divide(15.0, 3.0) << endl;
  cout << "String length: " << processString("Hello Lambda") << endl;
  cout << "Multiply: " << multiply(7) << endl;

  return 0;
}
Add: 30
Divide: 5
Processing: Hello Lambda
String length: 11
Multiply: 35

Practical Uses of Lambda Expressions

1. With STL Algorithms

lambda_stl.cpp
#include
#include
#include
#include
using namespace std;

int main() {
  vector<int> numbers = {5, 2, 8, 1, 9, 3, 7, 4, 6};

  // Sort with custom comparator lambda
  sort(numbers.begin(), numbers.end(), [](int a, int b) {
    return a > b; // Descending order
  });
  cout << "Sorted descending: ";
  for (int n : numbers) cout << n << " ";
  cout << endl;

  // Find first even number
  auto evenIt = find_if(numbers.begin(), numbers.end(), [](int n) {
    return n % 2 == 0;
  });
  if (evenIt != numbers.end()) {
    cout << "First even number: " << *evenIt << endl;
  }

  // Count numbers greater than 5
  int count = count_if(numbers.begin(), numbers.end(), [](int n) {
    return n > 5;
  });
  cout << "Numbers greater than 5: " << count << endl;

  // Transform: square all numbers
  vector<int> squared;
  transform(numbers.begin(), numbers.end(), back_inserter(squared),
    [](int n) { return n * n; });
  cout << "Squared numbers: ";
  for (int n : squared) cout << n << " ";
  cout << endl;

  return 0;
}
Sorted descending: 9 8 7 6 5 4 3 2 1
First even number: 8
Numbers greater than 5: 4
Squared numbers: 81 64 49 36 25 16 9 4 1

2. Mutable Lambdas

mutable_lambda.cpp
#include
using namespace std;

int main() {
  int counter = 0;

  // Without mutable - cannot modify captured values
  // auto increment = [counter]() { counter++; }; // Error!

  // With mutable - can modify captured-by-value variables
  auto increment = [counter]() mutable {
    counter++;
    cout << "Inside lambda: counter = " << counter << endl;
  };

  cout << "Original counter: " << counter << endl;
  increment();
  increment();
  cout << "After calls - original counter: " << counter << endl;
  // Note: counter inside lambda is a separate copy

  return 0;
}
Original counter: 0
Inside lambda: counter = 1
Inside lambda: counter = 2
After calls - original counter: 0

Advanced Lambda Features (C++14 and later)

advanced_lambda.cpp
#include
#include
#include
using namespace std;

int main() {
  vector<int> numbers = {1, 2, 3, 4, 5};

  // C++14: Generic lambdas with auto parameters
  auto print = [](auto value) {
    cout << value << " ";
  };

  cout << "Generic lambda: ";
  print(42);
  print(3.14);
  print("hello");
  cout << endl;

  // C++14: Capture with initializer
  auto generator = [value = 0]() mutable {
    return value++;
  };

  cout << "Generated values: ";
  for (int i = 0; i < 5; i++) {
    cout << generator() << " ";
  }
  cout << endl;

  // Immediately Invoked Lambda Expression (IILE)
  int result = [](int a, int b) {
    return a * a + b * b;
  }(3, 4); // Called immediately with arguments

  cout << "IILE result: " << result << endl;

  return 0;
}
Generic lambda: 42 3.14 hello
Generated values: 0 1 2 3 4
IILE result: 25
Best Practice: Use lambda expressions for short, disposable functions, especially with STL algorithms. They make code more readable by keeping the logic close to where it's used.
Important: Be careful with capture by reference! If a lambda captures local variables by reference and outlives the scope of those variables, it will lead to undefined behavior (dangling references).

When to Use Lambda Expressions

  • STL Algorithms: Custom comparators, predicates, and operations
  • Callback Functions: Event handlers and asynchronous operations
  • Short Operations: Simple transformations or calculations used once
  • Threading: Passing tasks to threads
  • Resource Management: Custom deleters for smart pointers

Lambda Expression Cheat Sheet

Syntax Meaning
[](){} Basic lambda with no captures or parameters
[x, &y](){} Capture x by value, y by reference
[=](){} Capture all variables by value
[&](){} Capture all variables by reference
[=, &x](){} Capture all by value, but x by reference
[]() mutable {} Allow modification of captured-by-value variables
[](auto x){} Generic lambda (C++14+)

Lambda expressions are one of the most powerful features introduced in modern C++. They enable functional programming styles, make code more expressive, and significantly reduce boilerplate code when working with algorithms and callbacks.

Memory Management

Detailed content about memory management would go here...

Smart Pointers

Detailed content about smart pointers would go here...

Move Semantics

Detailed content about move semantics would go here...

Lambda Expressions

Detailed content about lambda expressions would go here...