Complete OOP in C++ Tutorial
Master Object-Oriented Programming in C++ with comprehensive explanations, practical examples, and professional coding practices.
Introduction to Object-Oriented Programming
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.
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
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++:
// 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:
#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 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:
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;
}
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:
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;
}
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)
Common Mistakes to Avoid
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
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:
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;
}
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:
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;
}
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:
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;
}
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):
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
}
--- 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:
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(5, 10), id: 1, ref: 100
After changing externalVar:
Point(5, 10), id: 1, ref: 200
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
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;
}
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'
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
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
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:
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;
}
After direct modification: Name: Alice, Age: -5
2. Private Access Modifier
Private members can only be accessed within the class itself:
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 Number: ACC123456
Initial Balance: $1000
Deposited: $500
Withdrawn: $200
Withdrawal failed! Insufficient funds or invalid amount.
Final Balance: $1300
3. Protected Access Modifier
Protected members are accessible within the class and by derived classes (inheritance):
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;
}
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:
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;
}
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
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;
}
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
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
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
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:
#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 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%
Benefits Demonstrated in the Example
1. Data Validation
The setter methods validate input before modifying internal state:
deposit()checks if amount is positivewithdraw()checks for sufficient fundssetInterestRate()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:
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;
}
};
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
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:
// 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 |
Example: Single Inheritance - Vehicle Hierarchy
Let's create a vehicle hierarchy to demonstrate single inheritance:
#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;
}
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:
#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;
}
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:
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;
}
Base constructor called
Derived constructor called
Destroying Derived object:
Derived destructor called
Base destructor called
Destructor Order: Derived class destructors are called before base class destructors.
Function Overriding
Derived classes can override base class functions to provide specialized behavior:
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;
}
};
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
Polymorphism
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.
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:
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 doubles: 5.85
Adding three integers: 6
Concatenating strings: Hello, World!
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++:
#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;
}
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
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:
#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.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;
}
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
- Use
overridekeyword to explicitly indicate function overriding - Use
finalkeyword 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
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 {}
};
Abstraction
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.
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:
#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.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;
}
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:
#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;
}
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:
private:
string accountNumber;
double balance;
vector
public:
// Simple interface hides complex data management
bool deposit(double amount);
bool withdraw(double amount);
double getBalance();
vector
};
2. Control Abstraction
Hiding complex control flow and algorithms:
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
- 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
- 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
class BadDatabase {
public:
void connectUsingMySQLProtocol(); // Too specific!
void saveToBinaryFile(); // Implementation detail!
};
// CORRECT: Proper abstraction
class GoodDatabase {
public:
bool connect();
bool save(const Data& data);
};
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
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.
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
#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;
}
Integer: 42
Double: 3.14159
String: Hello
Character: 'A'
Boolean: true
2. Overloading by Number of Parameters
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;
}
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
#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;
}
Displaying string 3 times:
Hello
Hello
Hello
Answer: 42
Value: 98.6 F
Value: 98.6
2. Overloading Constructors
#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;
}
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
conston value parameters - Functions where one has a parameter with a default argument that makes it ambiguous
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:
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: 42Double: 3.14
Hi Hi Hi
Hello Hello [end of output]
Overloading and const Qualifiers
#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 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
- 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
/*
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().
Advanced: Template vs Overloading
Sometimes function templates can be an alternative to 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
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
Operator Overloading Syntax
Operator overloading can be implemented in two ways:
1. Member Function Syntax
public:
// Overload + operator as member function
ClassName operator+(const ClassName& obj) {
// implementation
}
};
2. Friend Function Syntax
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:
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;
}
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.
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;
}
};
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
Friend Function & Class
friend keyword inside the class.
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
Friend Function Syntax and Characteristics
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
thisPointer: They don't have access to thethispointer - 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:
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;
}
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
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:
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;
}
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
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
Static Members
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
Static Data Members
Declaration and Definition
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:
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;
}
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
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
thisPointer: 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:
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;
}
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.
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;
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
Virtual Functions
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
Virtual Function Syntax and Mechanism
Basic Syntax
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:
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;
}
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
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:
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;
}
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:
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()
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
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
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
Abstract Class Syntax and Rules
Basic Syntax
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:
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;
}
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:
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.
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
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
= 0syntax for pure virtual functions
Interfaces
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
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
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:
#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;
}
=== 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:
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:
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)
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
public:
virtual Product* createProduct() = 0;
static IFactory* getDefaultFactory();
virtual ~IFactory() = default;
};
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
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.
Exception Handling
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
Exception Handling Syntax and Mechanism
Basic Syntax
// 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
- Throw: An exception is thrown using the
throwkeyword - Unwind: The call stack is unwound until a matching catch block is found
- Catch: The exception is caught and handled by the appropriate catch block
- Continue: Program continues after the try-catch block
Example: Basic Exception Handling
Let's demonstrate basic exception handling with different scenarios:
#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;
}
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:
#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:
#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 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)
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)
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
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.
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");
}
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
Templates
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
Function Templates
Basic Function Template Syntax
returnType functionName(T parameter) {
// Function body using type T
}
Example: Simple Function Template
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 intMax = getMax(10, 20);
cout << "Max of 10 and 20: " << intMax << endl;
// Using with doubles
// Explanation: Compiler generates getMax
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 3.14 and 2.71: 3.14
Max of 'x' and 'y': y
Class Templates
Basic Class Template Syntax
class ClassName {
private:
T data;
public:
ClassName(T value);
T getData();
void setData(T value);
};
// Member function definition outside class
template <typename T>
ClassName
template <typename T>
T ClassName
Example: Simple Class Template
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
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 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
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: 3.14, Second: 1
Pair: (1, Alice)
Pair: (Bob, 95.5)
Template Specialization
Function Template Specialization
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: 3.14
C-string: "Hello"
Generic type: 1
Class Template Specialization
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;
}
Boolean Wrapper: TRUE
Boolean Wrapper: FALSE
Non-Type Template Parameters
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[3]: 1.1 2.2 3.3
Template Type Deduction
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: 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 |
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
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.
Standard Template Library (STL)
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
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
#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;
}
Size: 3
All elements: 10 20 30
2. list - Doubly Linked List
#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;
}
Associative Containers
1. map - Key-Value Pairs
#include
All students:
ID: 101, Name: Alice
ID: 102, Name: Bob
ID: 103, Name: Charlie
2. set - Unique Elements Collection
#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;
}
STL Algorithms
1. Sorting and Searching
#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;
}
Found 8 at position: 3
2. Counting and Accumulation
#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;
}
Sum of all numbers: 22
Iterators
#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;
}
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)
#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;
}
Odd numbers count: 5
Container Adapters
1. stack - LIFO Structure
#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;
}
STL Tutorial
Effective C++
C++ Primer
2. queue - FIFO Structure
#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;
}
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
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 |
Common STL Headers
- Dynamic array - - Key-value map
- Unique elements collection - Algorithms like sort, find, transform - Iterator utilities - Function objects and operations - Stack adapter - Queue and priority_queue
File Handling
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
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
#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;
}
Appending to Files
#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;
}
Reading from Files
Reading Line by Line
#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;
}
=====================
Line 1: Hello, File Handling!
Line 2: This is line 2.
Line 3: Number: 42
Line 4: PI: 3.14159
Reading Word by Word
#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;
}
==============
Hello,
File
Handling!
This
is
line
2.
Number:
42
PI:
3.14159
Total words: 11
Checking File States
#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;
}
- File is open successfully
- File stream is in good state
Checking file: nonexistent.txt
- Failed to open file
Binary File Handling
#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;
}
Reading student data:
ID: 101, Name: Alice, GPA: 3.8
ID: 102, Name: Bob, GPA: 3.5
File Position Pointers
#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;
}
Moved to beginning
First 5 chars: 12345
New position: 15
Error Handling in File Operations
#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;
}
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
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); |
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.
Advanced OOP Concepts
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
Multiple Inheritance
Basic Multiple Inheritance
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;
}
Drawing shape: Circle
Printing shape: Circle
Drawing shape: Circle
Diamond Problem and Virtual Inheritance
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;
}
Animal constructor: age=5
Mammal constructor
WingedAnimal constructor
Bat constructor
Bat age: 5
Bat object destruction:
Animal destructor
Factory Pattern
#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
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;
}
Saving Text Document
Opening Spreadsheet Document
Saving Spreadsheet Document
Singleton Pattern
#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;
}
LOG [app.log]: Application started
LOG [new_app.log]: Configuration updated
Are loggers the same? Yes
Observer Pattern
#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
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;
}
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
#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
double totalAmount;
public:
ShoppingCart() : totalAmount(0) {}
void addItem(double price) {
totalAmount += price;
}
void setPaymentStrategy(unique_ptr
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;
}
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
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 |
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.
Memory Management
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
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
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;
}
Array elements: 0 10 20 30 40
Memory successfully freed!
Object Allocation
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: 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
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;
}
Function returned, but memory is still allocated
This is a memory leak!
2. Dangling Pointers
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;
}
Memory freed
WARNING: Using dangling pointer - UNDEFINED BEHAVIOR!
Pointer set to nullptr - safe now
3. Double Deletion
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;
}
First delete - OK
Attempting second delete - CRASH RISK!
Deleting nullptr is safe
RAII (Resource Acquisition Is Initialization)
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;
}
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
#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;
}
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.
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
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
Smart Pointers
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
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
#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->use();
// unique_ptr cannot be copied (exclusive ownership)
// unique_ptr
// But it can be moved (transfer ownership)
unique_ptr
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;
}
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
#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;
}
Array elements: 10 20 30 40 50
No manual delete needed!
std::shared_ptr - Shared Ownership
Basic shared_ptr Usage
#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
// Check reference count
cout << "Reference count: " << ptr1.use_count() << endl;
// Create additional shared_ptrs that share ownership
shared_ptr
shared_ptr
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;
}
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
#include
using namespace std;
class Controller; // Forward declaration
class Display {
public:
weak_ptr
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
string name;
Controller(const string& n) : name(n) {
cout << "Controller created: " << name << endl;
}
~Controller() {
cout << "Controller destroyed: " << name << endl;
}
void connectDisplay(shared_ptr
display = disp;
disp->controller = shared_ptr
}
};
int main() {
cout << "=== weak_ptr Demonstration ===" << endl;
// Create objects
auto display = make_shared
auto controller = make_shared
// 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;
}
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 |
| 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
#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;
}
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
#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
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
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;
}
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
- 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
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 |
Move Semantics
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
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
#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;
}
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
#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;
}
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
#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;
}
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
#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;
}
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
- 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
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 |
Lambda Expressions
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:
// [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:
#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;
}
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
#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;
}
Divide: 5
Processing: Hello Lambda
String length: 11
Multiply: 35
Practical Uses of Lambda Expressions
1. With STL Algorithms
#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;
}
First even number: 8
Numbers greater than 5: 4
Squared numbers: 81 64 49 36 25 16 9 4 1
2. Mutable Lambdas
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;
}
Inside lambda: counter = 1
Inside lambda: counter = 2
After calls - original counter: 0
Advanced Lambda Features (C++14 and later)
#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;
}
Generated values: 0 1 2 3 4
IILE result: 25
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...