PYnative

Python Programming

  • Learn Python
    • Python Tutorials
    • Python Basics
    • Python Interview Q&As
  • Exercises
  • Quizzes
  • Code Editor
Home » CPP Exercises » C++ OOP Exercises

C++ OOP Exercises

Updated on: December 10, 2025 | Leave a Comment

This article provides 30+ C++ OOP practical exercises designed to help you master Object-Oriented Programming (OOP). The challenges focus on the four core pillars: Encapsulation, Inheritance, Polymorphism, and Abstraction

Each exercise includes a clear problem statement, a helpful hint, a complete C++ solution, and a detailed explanation. This ensures you not only solve the problem but deeply understand why the solution works.

What You Will Practice

The exercises includes practical coding challenges covering below major topics:

  • Classes & Objects: Definitions, Constructors (Default, Parameterized), Destructors, Initialization Lists, this pointer, and Shallow/Deep Copy.
  • Encapsulation: Using Getters and Setters and Const Correctness.
  • Inheritance: Single Inheritance, Method Overriding, and Protected access.
  • Runtime Polymorphism: Virtual Functions, Abstract Base Classes (ABCs), and Virtual Destructors.
  • Static Polymorphism: Function Overloading.
  • Operator Overloading: Customizing Binary, Unary, Assignment (operator=), Stream Insertion (operator<<), and Subscript (operator[]) operators.
  • Association: Composition (Strong Has-A) and Aggregation (Weak Has-A).
  • Special Members: Static Members (Variables/Functions), Friend Functions, and Friend Classes.
+ Table Of Contents

Table of contents

  • Exercise 1: Rectangle Class With Methods and Properties
  • Exercise 2: Car Class With Attributes and Simple Behavior
  • Exercise 3: Book Class For Data Retrieval
  • Exercise 4: Circle Class With Constant Methods
  • Exercise 5: Default/Parameterized Constructor
  • Exercise 6: Destructor Demonstration
  • Exercise 7: Copy Constructor
  • Exercise 8: Date Class with Validation
  • Exercise 9: Initialization List
  • Exercise 10: Bank Account (Encapsulation)
  • Exercise 11: Temperature Converter With Getters and Setters
  • Exercise 12: Data Hiding With Immutable Year
  • Exercise 13: Read-Only Property With Static Counter
  • Exercise 14: Basic Single Inheritance
  • Exercise 15: Multilevel Inheritance
  • Exercise 16: Protected Members
  • Exercise 17: Constructor Chaining
  • Exercise 18: Shape Hierarchy With Virtual Area
  • Exercise 19: Abstract Base Class (Interface)
  • Exercise 20: Runtime Polymorphism
  • Exercise 21: Virtual Destructor
  • Exercise 22: Function Overloading (Static Polymorphism)
  • Exercise 23: Operator Overloading (Binary Addition)
  • Exercise 24: Subscript Operator
  • Exercise 25: Composition (Has-A relationship)
  • Exercise 26: Aggregation (Has-A relationship)
  • Exercise 27: Library System as a Aggregation Example
  • Exercise 28: One-to-Many Relationship (Composition Choice)
  • Exercise 29: Static Member Variable
  • Exercise 30: Friend Function
  • Exercise 31: Friend Class

Exercise 1: Rectangle Class With Methods and Properties

Problem Statement: Define a class Rectangle with private members int length and int width. Implement a constructor to set the dimensions. Implement two public methods: calculate_area() which returns the product of length and width, and calculate_perimeter() which returns 2 * (length + width).

Expected Output:

Dimensions: 10x5
Area: 50
Perimeter: 30
+ Hint
  • The methods should be simple mathematical calculations involving the private member variables.
  • Ensure the constructor handles setting both dimension values when the object is created.
+ Show Solution
#include <iostream>

class Rectangle {
private:
    int length;
    int width;

public:
    // Constructor
    Rectangle(int l, int w) : length(l), width(w) {}

    // Method to calculate and return the area
    int calculate_area() const {
        return length * width;
    }

    // Method to calculate and return the perimeter
    int calculate_perimeter() const {
        return 2 * (length + width);
    }
};

int main() {
    Rectangle rect(10, 5); // Create a rectangle object
    
    int area = rect.calculate_area();
    int perimeter = rect.calculate_perimeter();
    
    std::cout << "Rectangle Dimensions: " << rect.getLength() << "x" << rect.getWidth() << std::endl;
    std::cout << "Area: " << area << std::endl;
    std::cout << "Perimeter: " << perimeter << std::endl;

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

  • This exercise shows how objects combine data and behavior.
  • The Rectangle object holds the data (length, width), and the methods (calculate_area, calculate_perimeter) define the behavior (operations) that can be performed on that data.
  • The const keyword after the method signature is good practice; it assures the caller that the method does not modify the object’s state.

Exercise 2: Car Class With Attributes and Simple Behavior

Problem Statement: Create a Car class with public attributes std::string make, std::string model, and int year. Implement a public method start_engine() that simply prints the message: “[Year] [Make] [Model] engine started!”.

Expected Output:

2020 Toyota Corolla engine started!
+ Hint

For simplicity, use public attributes. The method should utilize the object’s own attributes within the print statement

+ Show Solution
#include <iostream>
#include <string>

class Car {
public:
    std::string make;
    std::string model;
    int year;

    // Constructor for easy initialization
    Car(std::string mk, std::string md, int yr) 
        : make(mk), model(md), year(yr) {}

    // Method demonstrating a simple action/behavior
    void start_engine() {
        std::cout << year << " " << make << " " << model << " engine started!" << std::endl;
    }
};

int main() {
    Car my_car("Toyota", "Corolla", 2020);
    
    // Demonstrate interaction with attributes and method
    std::cout << "My car is a " << my_car.year << " " << my_car.make << "." << std::endl;
    my_car.start_engine();

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This exercise shows a basic OOP concept where an object models a real-world entity.

  • The attributes (make, model, year) describe the car’s state, and the method (start_engine) describes its capability or action.
  • Since the attributes are public in this case, they can be directly accessed and modified outside the class (though this is often discouraged in favor of encapsulation).

Exercise 3: Book Class For Data Retrieval

Problem Statement: Design a Book class with private members std::string title, std::string author, and std::string isbn. Include a constructor to initialize them. Implement a public method get_details() that returns a single formatted std::string containing all the book’s information.

Expected Output:

Book Record:
Title: The C++ Programming Language | Author: Bjarne Stroustrup | ISBN: 0321563840
+ Hint

The get_details() method will use string concatenation (or a string stream) to combine all the private members into one return string.

+ Show Solution
#include <iostream>
#include <string>

class Book {
private:
    std::string title;
    std::string author;
    std::string isbn;

public:
    // Constructor
    Book(std::string t, std::string a, std::string i) 
        : title(t), author(a), isbn(i) {}

    // Method to return formatted details
    std::string get_details() const {
        return "Title: " + title + " | Author: " + author + " | ISBN: " + isbn;
    }
};

int main() {
    Book b1("The C++ Programming Language", "Bjarne Stroustrup", "0321563840");
    
    std::string book_info = b1.get_details();
    
    std::cout << "Book Record:" << std::endl;
    std::cout << book_info << std::endl;

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This exercise focuses on Encapsulation combined with controlled retrieval. By making the data private and providing the get_details() method, the class controls how its internal data is presented to the outside world.

This is superior to accessing each member individually, as it provides a single, consistent way to represent the entire object’s state.

Exercise 4: Circle Class With Constant Methods

Problem Statement: Implement a Circle class with a private member double radius. Add a constructor to initialize the radius. Include a public constant member function get_area() that calculates and returns the area of the circle (pir, use pi = 3.14159)

Expected Output:

Circle with radius 5.0 has an area of: 78.5397
+ Hint
  • A constant member function is one that is guaranteed not to modify the object’s data members.
  • The keyword const must follow the function signature in the declaration and definition.
+ Show Solution
#include <iostream>
#include <cmath> // For M_PI, if available, otherwise use 3.14159

class Circle {
private:
    double radius;
    const double PI = 3.14159; // Define PI inside the class

public:
    // Constructor
    Circle(double r) : radius(r) {}

    // Constant member function
    double get_area() const {
        // The 'const' here ensures this method does not modify 'radius' or 'PI'
        return PI * radius * radius;
    }
};

int main() {
    Circle c1(5.0);
    double area = c1.get_area();
    
    std::cout << "Circle with radius 5.0 has an area of: " << area << std::endl;

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

The use of the const keyword on the get_area() method is the key concept here. It signifies that the method is an inspector (it only reads data) and not a mutator (it doesn’t change data).

This improves code safety and allows the method to be called on const objects (objects whose state cannot be changed).

Exercise 5: Default/Parameterized Constructor

Problem Statement: Implement the Rectangle class (from Exercise 1). This time, include two public constructors: a default constructor that sets both length and width to 1, and a parameterized constructor that allows setting custom values for length and width.

Expected Output:

Custom Rectangle created (12x4).
R1 Area: 48
Default Rectangle created (1x1).
R2 Area: 1
+ Hint
  • Define two public constructors. It called a constructor overloading (the concept of having multiple constructors).
  • The default constructor takes no arguments.
  • The parameterized constructor takes two arguments.
+ Show Solution
#include <iostream>

class Rectangle {
private:
    int length;
    int width;

public:
    // 1. Default Constructor (no arguments)
    Rectangle() : length(1), width(1) {
        std::cout << "Default Rectangle created (1x1)." << std::endl;
    }

    // 2. Parameterized Constructor
    Rectangle(int l, int w) : length(l), width(w) {
        std::cout << "Custom Rectangle created (" << length << "x" << width << ")." << std::endl;
    }

    int get_area() const {
        return length * width;
    }
};

int main() {
    // Uses the Parameterized Constructor
    Rectangle r1(12, 4); 
    std::cout << "R1 Area: " << r1.get_area() << std::endl;

    // Uses the Default Constructor
    Rectangle r2;
    std::cout << "R2 Area: " << r2.get_area() << std::endl;

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

  • This demonstrates Constructor Overloading, a form of Static Polymorphism.
  • The compiler selects the correct constructor based on the number and type of arguments provided during object creation.
  • A default constructor is essential for creating objects when no initial values are specified (e.g., in arrays or STL containers).

Exercise 6: Destructor Demonstration

Problem Statement: Create a simple class, e.g., Resource, with a private member std::string id which models acquiring and releasing a resource. Implement a constructor that prints “Resource [ID] acquired” and a destructor that prints “Resource [ID] released”. Use a local object inside a function to clearly show when the destructor is called.

+ Hint

The destructor is defined using the tilde (~) character followed by the class name (e.g., ~Resource()). It is automatically called when the object goes out of scope.

+ Show Solution
#include <iostream>
#include <string>

class Resource {
private:
    std::string id;

public:
    // Constructor
    Resource(std::string name) : id(name) {
        std::cout << ">> Resource '" << id << "' ACQUIRED (Constructed)" << std::endl;
    }

    // Destructor
    ~Resource() {
        std::cout << "<< Resource '" << id << "' RELEASED (Destructed)" << std::endl;
    }
};

void scope_test() {
    std::cout << "\n--- Entering scope_test() ---" << std::endl;
    Resource local_res("Local A"); // Object created
    std::cout << "--- Exiting scope_test() ---" << std::endl;
    // local_res is destroyed here
}

int main() {
    std::cout << "--- Starting main() ---" << std::endl;
    Resource main_res("Main B"); // Object created

    scope_test();

    std::cout << "--- Ending main() ---" << std::endl;
    // main_res is destroyed here
    return 0;
}Code language: C++ (cpp)
Run

Explanation:

The Destructor (~Classname) is a special member function that is automatically called when an object’s lifetime ends, which occurs when it goes out of scope (as seen in scope_test() and main()):

  • Its primary purpose is cleanup. It ensures that any dynamically allocated memory or external resources (files, network connections, etc.) are properly cleaned up when the object is no longer needed.
  • This is fundamental to C++’s powerful RAII (Resource Acquisition Is Initialization) paradigm, which prevents memory leaks and resource exhaustion by tying resource lifetime to object lifetime.

Exercise 7: Copy Constructor

Problem Statement: Create a class Point with a private member int x and int y as a coordinates. Implement a copy constructor for this class. Write a display() method. Demonstrate its use by creating a new Point object as a copy of an existing one.

Given:

class Point {
private:
    int x;
    int y;
};Code language: C++ (cpp)

Expected Output:

Point(10, 20)
Copy Constructor called.
Point(10, 20)

After moving p2:
Point(10, 20)
Point(15, 25)
+ Hint

The copy constructor takes a const reference to an object of the same class, defined as Point(const Point& other).

For this simple case, member-wise copy is sufficient, but writing it explicitly is good practice.

+ Show Solution
#include <iostream>

class Point {
private:
    int x;
    int y;

public:
    // Parameterized Constructor
    Point(int px, int py) : x(px), y(py) {}

    // Copy Constructor
    Point(const Point& other) : x(other.x), y(other.y) {
        std::cout << "Copy Constructor called." << std::endl;
    }

    void display() const {
        std::cout << "Point(" << x << ", " << y << ")" << std::endl;
    }
    
    void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
};

int main() {
    Point p1(10, 20);
    p1.display();

    // The Copy Constructor is implicitly called here
    Point p2 = p1; 
    p2.display();

    // Modify p2 and show p1 is unaffected
    p2.move(5, 5);
    std::cout << "\nAfter moving p2:" << std::endl;
    p1.display(); // Still (10, 20)
    p2.display(); // Now (15, 25)

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

The Copy Constructor (Point(const Point& other)) is automatically invoked whenever a new object is initialized from an existing object of the same type (e.g., Point p2 = p1; or passing an object by value to a function):

  • It ensures the new object is an independent copy.
  • For simple classes like Point, the default, compiler-generated copy constructor performs a member-wise copy (shallow copy), which is often sufficient.
  • However, when a class manages pointers or dynamic memory, a custom copy constructor is vital to perform a deep copy. This prevents two objects from sharing and modifying the same underlying memory block, avoiding double-free errors upon destruction.

Exercise 8: Date Class with Validation

Problem Statement: Create a Date class with private members int day, int month, and int year. Implement a constructor that performs basic validation: if the month is not between 1 and 12, it should set the month to a default value (e.g., 1) and print an error message.

Expected Output:

Date 1: 2025-10-28
Valid Date: 2025-10-28
Date 2: 2025-13-1
Warning: Invalid month (13) provided. Setting to 1.
Valid Date: 2025-1-1
+ Hint

The validation logic should be placed inside the constructor’s body after the members are initialized via the initialization list.

+ Show Solution
#include <iostream>

class Date {
private:
    int day;
    int month;
    int year;

public:
    // Parameterized Constructor
    Date(int d, int m, int y) 
        : day(d), month(m), year(y) 
    {
        // Validation Logic
        std::cout<< year << "-" << month << "-" << day << std::endl;
        if (month < 1 || month > 12) {
            std::cerr << "Warning: Invalid month (" << month << ") provided. Setting to 1." << std::endl;
            this->month = 1; // 'this->' is optional here but highlights the member being modified
        }
        // Basic validation on day could also be added (e.g., if day < 1 || day > 31)
    }

    void display() const {
        std::cout<<"Valid Date: "<<year << "-" << month << "-" << day << std::endl;
    }
};

int main() {
    std::cout<<"Date 1: ";
    Date valid_date(28, 10, 2025);
    valid_date.display();

    std::cout<<"Date 2: ";
    Date invalid_month_date(1, 13, 2025);
    invalid_month_date.display(); // Should show 2025-1-1

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This exercise highlights that constructors are not just for initialization but are also the ideal place for object validation.

By placing validation logic inside the constructor, we ensure that an object is never created in an invalid or inconsistent state. This principle contributes significantly to robust Encapsulation and object integrity.

Exercise 9: Initialization List

Problem Statement: Create a C++ class named Student with the following private members: const std::string name, int roll_number, and double score. Implement a constructor that takes parameters for these three members and initializes them exclusively using the constructor initialization list. Include a display_data() method to show the initialized values.

Given:

class Student {
private:
    const std::string name; // Note: making 'name' const requires initialization list
    int roll_number;
    double score;
};Code language: C++ (cpp)

Expected Output:

Student: Clara B. | Roll: 101 | Score: 78.9
+ Hint

The initialization list is placed after the constructor’s parameter list, separated by a colon (:). It is the only way to initialize const members like name.

+ Show Solution
#include <iostream>
#include <string>

class Student {
private:
    const std::string name; 
    int roll_number;
    double score;

public:
    // Constructor using Initialization List
    Student(std::string n, int roll, double s) 
        : name(n),                 // Initialize const member 'name'
          roll_number(roll),       // Initialize 'roll_number'
          score(s)                 // Initialize 'score'
    {
        // The constructor body is intentionally empty.
    }

    void display_data() const {
        std::cout << "Student: " << name << " | Roll: " << roll_number 
                  << " | Score: " << score << std::endl;
    }
};

int main() {
    Student s1("Clara B.", 101, 78.9);
    s1.display_data();

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

The Initialization List is the preferred and often necessary way to initialize members in C++. Unlike assignments inside the constructor body, initialization lists directly initialize the members when the object is created. This is crucial for:

  1. Efficiency: It avoids a temporary default construction followed by an assignment.
  2. Necessity: It is required for initializing const members (like name above) and reference members, as these cannot be assigned a value after they are created.
  3. Base Classes: It’s also used to call constructors of base classes in inheritance.

Exercise 10: Bank Account (Encapsulation)

Problem Statement: Create a BankAccount class with private members double balance and std::string account_number. Provide public methods deposit(double amount) and withdraw(double amount). The deposit method should add the amount. The withdraw method must include validation: it should only proceed if the amount is positive and less than or equal to the current balance. Both methods should return a boolean indicating success or failure.

Expected Output:

Deposit successful. New balance: $650
Withdrawal failed: Insufficient balance.
Withdrawal successful. New balance: $600
Withdrawal failed: Amount must be positive
+ Hint

Use conditional statements (if) inside the withdraw method for validation. If the withdrawal condition fails, print a message and return false.

+ Show Solution
#include <iostream>
#include <string>

class BankAccount {
private:
    double balance;
    std::string account_number;

public:
    // Constructor
    BankAccount(std::string accNum, double initialBalance = 0.0) 
        : account_number(accNum), balance(initialBalance) {}

    // Method to deposit money
    bool deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            std::cout << "Deposit successful. New balance: $" << balance << std::endl;
            return true;
        }
        std::cout << "Deposit failed: Amount must be positive." << std::endl;
        return false;
    }

    // Method to withdraw money with validation
    bool withdraw(double amount) {
        if (amount <= 0) {
            std::cout << "Withdrawal failed: Amount must be positive." << std::endl;
            return false;
        }
        if (amount <= balance) {
            balance -= amount;
            std::cout << "Withdrawal successful. New balance: $" << balance << std::endl;
            return true;
        } else {
            std::cout << "Withdrawal failed: Insufficient balance." << std::endl;
            return false;
        }
    }
    
    // Getter for balance (to display state)
    double get_balance() const {
        return balance;
    }
};

int main() {
    BankAccount account("123456", 500.00);
    
    account.deposit(150.00);
    account.withdraw(700.00); // Fail: Insufficient balance
    account.withdraw(50.00);  // Success
    account.withdraw(-10.00); // Fail: Invalid amount

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This exercise fully implements Encapsulation and demonstrates controlled access to the object’s state.

  • Data Hiding: The balance is private, meaning it can only be changed by the object’s own methods (deposit and withdraw).
  • State Management: The withdraw method is responsible for managing the state change. It acts as a gatekeeper, ensuring that the object’s state remains valid (i.e., the balance is never reduced below the withdrawal amount).
  • By returning a boolean, the methods inform the external caller of the operation’s outcome, which is a common practice in controlled transaction systems.

Exercise 11: Temperature Converter With Getters and Setters

Problem Statement: Implement a class Temperature with a private member double celsius. Provide a public setter method set_celsius(double c) to assign a value to the private member. Also, provide a public getter method get_fahrenheit() that calculates and returns the temperature in Fahrenheit using the formula: F = C (9/5) + 32.

Given:

class Temperature {
private:
    double celsius;
};Code language: C++ (cpp)

Expected Output:

Celsius set to: 25
Fahrenheit: 77
Celsius set to: 100
Fahrenheit: 212
+ Hint

The setter should assign the input parameter to the private member. The getter performs a calculation but does not modify the internal state, so it should be a const function.

+ Show Solution
#include <iostream>

class Temperature {
private:
    double celsius;

public:
    // Constructor
    Temperature(double c = 0.0) : celsius(c) {}

    // Public Setter (Mutator)
    void set_celsius(double c) {
        if (c >= -273.15) { // Basic check against Absolute Zero
            celsius = c;
            std::cout << "Celsius set to: " << celsius << std::endl;
        } else {
            std::cout << "Error: Temperature below absolute zero ignored." << std::endl;
        }
    }

    // Public Getter (Accessor) - Const function
    double get_fahrenheit() const {
        return (celsius * 9.0 / 5.0) + 32.0;
    }
};

int main() {
    Temperature temp;
    
    temp.set_celsius(25.0);
    std::cout << "Fahrenheit: " << temp.get_fahrenheit() << std::endl; // Expected: 77.0
    
    temp.set_celsius(100.0);
    std::cout << "Fahrenheit: " << temp.get_fahrenheit() << std::endl; // Expected: 212.0

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This demonstrates the common OOP pattern of using Getter and Setter methods to manage private data, providing controlled access without exposing the raw data member.

  • Setter (set_celsius): This is a Mutator method. It allows external code to change the private member, but the setter itself can enforce constraints (like the absolute zero check), controlling how the data is modified.
  • Getter (get_fahrenheit): This is an Accessor method. It allows external code to retrieve a derived value without exposing the underlying internal unit (celsius). It is marked const because it only calculates a value and does not change the object’s state.

Exercise 12: Data Hiding With Immutable Year

Problem Statement: Modify the Car class (from Ex. 3) to have private members make, model, and year. Ensure the year is immutable after the object is constructed by only providing a constructor and a public getter method for year, but no setter method for year.

Given:

class Car {
private:
    std::string make;
    std::string model;
    int year;
};Code language: C++ (cpp)

Expected Output:

2018 Honda Civic
Car was manufactured in: 2018
Changing car details
2018 Acura Civic
+ Hint

Use the initialization list to set all values in the constructor. Only define the get_year() accessor and keep all members private.

+ Show Solution
#include <iostream>
#include <string>

class Car {
private:
    std::string make;
    std::string model;
    int year;

public:
    // Constructor: Sets all private members
    Car(std::string mk, std::string md, int yr) 
        : make(mk), model(md), year(yr) {}

    // Getter for Year (Allows reading)
    int get_year() const {
        return year;
    }
    
    // No setter for year means it's immutable after construction

    // Example Setter for Make (mutable attribute)
    void set_make(std::string newMake) {
        make = newMake;
    }

    void display_info() const {
        std::cout << year << " " << make << " " << model << std::endl;
    }
};

int main() {
    Car my_car("Honda", "Civic", 2018);
    my_car.display_info();
    
    // Attempting to change the year is impossible without a setter:
    // my_car.year = 2020; // Compiler Error: 'int Car::year' is private
    // my_car.set_year(2020); // Not defined
    
    std::cout << "Car was manufactured in: " << my_car.get_year() << std::endl; 
    
    my_car.set_make("Acura"); // Allowed
    my_car.display_info();

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This exercise demonstrates how Data Hiding is used to enforce data integrity and create immutable properties.

  • Immutability: By making the year private and providing no public setter, the class guarantees that the year of a Car object, once set by the constructor, cannot be changed during the object’s lifetime.
  • Controlled Mutability: In contrast, the make property is still mutable because a set_make() method was provided.
  • This fine-grained control over which attributes are modifiable is a core benefit of strong Encapsulation.

Exercise 13: Read-Only Property With Static Counter

Problem Statement: Create a class SerialGenerator with a private static member next_serial_number initialized to 1000. Implement a public method get_serial() that returns the current value of next_serial_number and then increments it. The counter itself should only be readable via this method.

Expected Output:

Initial next serial: 1000
Serial 1: 1000
Serial 2: 1001
Serial 3: 1002
Final next serial: 1003
+ Hint
  • The static member must be initialized outside the class definition.
  • The get_serial() method must not be const because it changes the static member.
+ Show Solution
#include <iostream>

class SerialGenerator {
private:
    // Private static member shared by all objects
    static int next_serial_number; 

public:
    // Public method to retrieve and increment the serial number
    int get_serial() {
        return next_serial_number++; // Returns current value, then increments
    }

    // Static method to access the current number without needing an instance
    static int peek_next_serial() {
        return next_serial_number;
    }
};

// Initialization of the static member outside the class definition
int SerialGenerator::next_serial_number = 1000; 

int main() {
    SerialGenerator g1, g2;
    
    std::cout << "Initial next serial: " << SerialGenerator::peek_next_serial() << std::endl;

    int s1 = g1.get_serial(); // 1000, increments to 1001
    int s2 = g2.get_serial(); // 1001, increments to 1002
    int s3 = g1.get_serial(); // 1002, increments to 1003
    
    std::cout << "Serial 1: " << s1 << std::endl;
    std::cout << "Serial 2: " << s2 << std::endl;
    std::cout << "Serial 3: " << s3 << std::endl;
    
    std::cout << "Final next serial: " << SerialGenerator::peek_next_serial() << std::endl;

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This demonstrates the use of Static Members and Controlled Mutability on a class-level variable.

  • Static Member: next_serial_number belongs to the class itself, not to any individual object. All instances of SerialGenerator share the same counter.
  • Data Control: By making the counter private, the only way to read and advance the number is through the get_serial() method. This ensures that every call to get_serial() gets a unique number and maintains the correct sequence.
  • The peek_next_serial() static method shows how to access a static member without needing an object instance (g1 or g2).

Exercise 14: Basic Single Inheritance

Problem Statement: Create a base class Animal with a public method eat() that prints “Animal is eating.” Derive a class Dog from Animal. The Dog class must override the eat() method to print “Dog is eating kibble.” Additionally, add a new public method bark() to the Dog class that prints “Woof! Woof!”.

Expected Output:

--- Generic Animal Behavior ---
Animal is eating generic food.
Animal is sleeping.

--- Dog Behavior ---
Dog is eating kibble.
Animal is sleeping.
Woof! Woof!
+ Hint
  • Use the colon (:) syntax to denote inheritance (class Dog : public Animal).
  • Ensure the Dog class defines its own version of eat().
+ Show Solution
#include <iostream>
#include <string>

// Base Class
class Animal {
public:
    void eat() const {
        std::cout << "Animal is eating generic food." << std::endl;
    }
    
    void sleep() const {
        std::cout << "Animal is sleeping." << std::endl;
    }
};

// Derived Class
class Dog : public Animal {
public:
    // Override the base class method
    void eat() const {
        std::cout << "Dog is eating kibble." << std::endl;
    }

    // New method specific to Dog
    void bark() const {
        std::cout << "Woof! Woof!" << std::endl;
    }
};

int main() {
    Animal generic_animal;
    Dog my_dog;
    
    std::cout << "--- Generic Animal Behavior ---" << std::endl;
    generic_animal.eat();
    generic_animal.sleep();
    
    std::cout << "\n--- Dog Behavior ---" << std::endl;
    my_dog.eat(); // Dog's overridden method
    my_dog.sleep(); // Inherited from Animal
    my_dog.bark(); // Dog's unique method

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This introduces Inheritance and Method Overriding. Inheritance is the mechanism that allows one class (Dog) to acquire the properties and methods of another class (Animal).

  • Single Inheritance: Dog inherits from one base class, Animal.
  • Code Reusability: The Dog class automatically gets the sleep() method without needing to redefine it, reducing code duplication.
  • Method Overriding: By defining a new eat() method in Dog with the exact same signature as the one in Animal, the derived class replaces the base class’s implementation when called on a Dog object.

Exercise 15: Multilevel Inheritance

Problem Statement: Implement multilevel inheritance using three classes: Vehicle –> Car –> SportsCar.

  • Vehicle should have a method start_transport().
  • Car should inherit from Vehicle and add a private attribute int number_of_doors and a method open_door().
  • SportsCar should inherit from Car and add a private attribute int max_speed and a method activate_turbo().

Expected Output:

Vehicle (2023) is moving.
Car with 2 doors opened.
Sports Car turbo activated! Max speed: 210 mph.
+ Hint

The inheritance chain uses nested : public declarations. Ensure each constructor initializes its own members and the members inherited from its immediate parent.

+ Show Solution
#include <iostream>

// Level 1: Base Class
class Vehicle {
public:
    Vehicle(int y) : year(y) {}
    
    void start_transport() const {
        std::cout << "Vehicle (" << year << ") is moving." << std::endl;
    }
private:
    int year;
};

// Level 2: Derived from Vehicle
class Car : public Vehicle {
private:
    int number_of_doors;
public:
    Car(int y, int doors) : Vehicle(y), number_of_doors(doors) {} // Call base constructor

    void open_door() const {
        std::cout << "Car with " << number_of_doors << " doors opened." << std::endl;
    }
};

// Level 3: Derived from Car
class SportsCar : public Car {
private:
    int max_speed;
public:
    SportsCar(int y, int doors, int speed) 
        : Car(y, doors), max_speed(speed) {} // Call intermediate constructor

    void activate_turbo() const {
        std::cout << "Sports Car turbo activated! Max speed: " << max_speed << " mph." << std::endl;
    }
};

int main() {
    SportsCar ferrari(2023, 2, 210);
    
    // Access methods from all three levels
    ferrari.start_transport(); // From Vehicle
    ferrari.open_door();       // From Car
    ferrari.activate_turbo();  // From SportsCar

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

  • Multilevel Inheritance creates a deep hierarchy where a class inherits from a class that, in turn, inherits from another class.
  • Hierarchy: SportsCar is a Car, and a Car is a Vehicle. SportsCar inherits the methods and properties from both its ancestors.
  • Constructor Chaining: Crucially, the constructor of SportsCar must first call the constructor of Car, which then calls the constructor of Vehicle, ensuring that all inherited private data is properly initialized.

Exercise 16: Protected Members

Problem Statement: Create a base class Person with a protected member int age. Derive two classes, Student and Teacher, from Person. Demonstrate that both derived classes can directly access and modify the protected age member, but an external function (like main) cannot.

+ Hint
  • Use the protected access specifier for age.
  • protected members behave like private outside the inheritance chain, but like public within it.
+ Show Solution
#include <iostream>

// Base Class
class Person {
protected:
    // Accessible by derived classes, but private to the outside world
    int age; 

public:
    Person(int a) : age(a) {}
    
    void display_age() const {
        std::cout << "Person's age is: " << age << std::endl;
    }
};

// Derived Class 1
class Student : public Person {
public:
    Student(int a) : Person(a) {}

    // Directly accessing and modifying the protected member 'age'
    void celebrate_birthday() {
        age++; 
        std::cout << "Student celebrated birthday. New age: " << age << std::endl;
    }
};

// Derived Class 2
class Teacher : public Person {
public:
    Teacher(int a) : Person(a) {}

    void set_age(int new_age) {
        age = new_age; // Direct modification is allowed
        std::cout << "Teacher age set to: " << age << std::endl;
    }
};

int main() {
    Student s(19);
    Teacher t(40);
    
    s.display_age();
    s.celebrate_birthday();

    t.display_age();
    t.set_age(41);
    
    // Compilation Error Examples:
    // s.age = 20; // ERROR: 'int Person::age' is protected
    // std::cout << t.age; // ERROR: 'int Person::age' is protected

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

The protected access specifier offers a balance between private and public access.

  • Internal Access: protected members are directly accessible by methods of the class itself and by methods of any classes derived from it (Student and Teacher).
  • External Restriction: Like private members, protected members are inaccessible from outside the class hierarchy (e.g., in the main function).
  • This is useful for defining attributes or helper methods that should be shared among an inheritance family but hidden from the general program logic.

Exercise 17: Constructor Chaining

Problem Statement: Using the Vehicle –> Car –> SportsCar hierarchy from Exercise 15, rewrite all constructors to explicitly demonstrate constructor chaining using initialization lists, ensuring that when SportsCar is created, its constructor correctly calls the Car constructor, which in turn calls the Vehicle constructor.

Expected Output:

Creating Ferrari...
--> Vehicle constructor called with year 2024
----> Car constructor called with 2 doors
------> SportsCar constructor called with speed 220
Ferrari created, year: 2024
+ Hint

In the derived class constructor, use the initialization list syntax (: BaseClass(arguments)) to explicitly call the base class’s constructor.

+ Show Solution
#include <iostream>

// Level 1: Base Class
class Vehicle {
private:
    int year;
public:
    // 1. Vehicle Constructor
    Vehicle(int y) : year(y) {
        std::cout << "--> Vehicle constructor called with year " << year << std::endl;
    }
    int get_year() const { return year; }
};

// Level 2: Derived from Vehicle
class Car : public Vehicle {
private:
    int number_of_doors;
public:
    // 2. Car Constructor calls Vehicle constructor in the initialization list
    Car(int y, int doors) 
        : Vehicle(y), // Calls Vehicle's constructor
          number_of_doors(doors) 
    {
        std::cout << "----> Car constructor called with " << doors << " doors" << std::endl;
    }
};

// Level 3: Derived from Car
class SportsCar : public Car {
private:
    int max_speed;
public:
    // 3. SportsCar Constructor calls Car constructor in the initialization list
    SportsCar(int y, int doors, int speed) 
        : Car(y, doors), // Calls Car's constructor
          max_speed(speed) 
    {
        std::cout << "------> SportsCar constructor called with speed " << speed << std::endl;
    }
};

int main() {
    std::cout << "Creating Ferrari..." << std::endl;
    SportsCar ferrari(2024, 2, 220);
    std::cout << "Ferrari created, year: " << ferrari.get_year() << std::endl;

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

Constructor Chaining is the process by which a derived class constructor ensures that all its base class constructors are executed before its own body runs.

Initialization List is Key: By placing Car(y, doors) in SportsCar‘s initialization list, we explicitly pass arguments up the hierarchy. This ensures the correct, parameterized constructors are called at every level.

Execution Order: When SportsCar is instantiated:

  1. Vehicle constructor runs.
  2. Car constructor runs.
  3. SportsCar constructor runs.

This guarantees that all inherited members are properly initialized before the derived class uses them

Exercise 18: Shape Hierarchy With Virtual Area

Problem Statement: Create a base class Shape with a method area() that returns 0.0. Derive two classes, Square (with private side member) and Triangle (with private base and height members). Both derived classes must override area() to perform the correct area calculation. For Square, return side *side For Triangle, return (base * side)/2.

Expected Output:

Square Area (5x5): 25
Triangle Area (4x6): 12
Generic Shape Area: 0
+ Hint

Define area() in the base class to be overridden in the derived classes.

+ Show Solution
#include <iostream>

// Base Class
class Shape {
public:
    Shape() {}
    
    // Base implementation (will be overridden)
    double area() const {
        return 0.0;
    }
    
    virtual ~Shape() {} // Good practice
};

// Derived Class 1
class Square : public Shape {
private:
    double side;
public:
    Square(double s) : side(s) {}
    
    // Override area()
    double area() const {
        return side * side;
    }
};

// Derived Class 2
class Triangle : public Shape {
private:
    double base;
    double height;
public:
    Triangle(double b, double h) : base(b), height(h) {}
    
    // Override area()
    double area() const {
        return (base * height) / 2.0;
    }
};

int main() {
    Square s(5.0);
    Triangle t(4.0, 6.0);
    
    // Demonstrate direct calls
    std::cout << "Square Area (5x5): " << s.area() << std::endl; // 25.0
    std::cout << "Triangle Area (4x6): " << t.area() << std::endl; // 12.0
    
    // Base class behavior
    Shape generic;
    std::cout << "Generic Shape Area: " << generic.area() << std::endl; // 0.0

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This exercise establishes the structure for Inheritance-based Polymorphism, even though the area() method is not yet declared as virtual.

  • Common Interface: All derived classes share the same public method signature (area()), creating a common interface for area calculation.
  • Overriding: Each derived class provides its specific implementation of area() based on its unique geometric properties.
  • This is the essential setup for Static Polymorphism (function overloading/hiding). To achieve Runtime Polymorphism (calling the correct derived method via a base class pointer/reference), the area() method in the base class must be declared virtual, which is the focus of the next set of exercises.

Exercise 19: Abstract Base Class (Interface)

Problem Statement: Modify the Shape class (from Exercise 18) to make it an Abstract Base Class (ABC). Achieve this by declaring the area() method as a pure virtual function (= 0). Demonstrate that you cannot create an instance of the Shape class, but you must implement area() in the derived classes (Square and Triangle).

Expected Output:

Square Area (5x5): 25
Triangle Area (4x6): 12
+ Hint

A pure virtual function is declared in the base class using the syntax virtual return_type function_name(...) = 0;. Any class containing at least one pure virtual function is an ABC and cannot be instantiated.

+ Show Solution
#include <iostream>

// Abstract Base Class (ABC)
class Shape {
public:
    // Pure virtual function: makes Shape an ABC
    virtual double area() const = 0;
    
    // Virtual destructor is good practice for base classes
    virtual ~Shape() {} 
};

// Derived Class 1
class Square : public Shape {
private:
    double side;
public:
    Square(double s) : side(s) {}
    
    // Must implement area() to be concrete
    double area() const override {
        return side * side;
    }
};

// Derived Class 2
class Triangle : public Shape {
private:
    double base;
    double height;
public:
    Triangle(double b, double h) : base(b), height(h) {}
    
    // Must implement area() to be concrete
    double area() const override {
        return (base * height) / 2.0;
    }
};

int main() {
    // Shape s; // Compiler Error: cannot instantiate abstract class 'Shape'

    // Must use derived classes
    Square s(5.0);
    Triangle t(4.0, 6.0);
    
    std::cout << "Square Area (5x5): " << s.area() << std::endl;
    std::cout << "Triangle Area (4x6): " << t.area() << std::endl;

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

By declaring area() as a pure virtual function, we create an Abstract Base Class (Shape).

  • Abstraction: The Shape class now acts as an interface or a blueprint. It declares what derived classes must do (area()) but provides no implementation details.
  • Enforcement: Any derived class that intends to be instantiated (a concrete class) must provide an implementation for all inherited pure virtual functions.
  • Inability to Instantiate: Because the Shape class is incomplete (it has a function without a definition), the compiler prevents direct creation of Shape objects.

Exercise 20: Runtime Polymorphism

Problem Statement: Using the Abstract Base Class Shape and its derived classes (Square, Triangle) from exercise 19, demonstrate Runtime Polymorphism. Create a std::vector of Shape* (pointers to Shape). Store instances of both Square and Triangle in this vector. Iterate through the vector and call the area() method on each pointer.

Expected Output:

--- Calculating Areas (Runtime Polymorphism) ---
Area: 64
Area: 25
Area: 9
+ Hint

When calling a virtual function through a base class pointer, the correct derived function will be executed. Remember to use new to allocate objects dynamically and delete to clean up.

+ Show Solution
#include <iostream>
#include <vector>
#include <memory> // For std::unique_ptr (better resource management)

// Abstract Base Class (ABC)
class Shape {
public:
    // Pure virtual function: makes Shape an ABC
    virtual double area() const = 0;
    
    // Virtual destructor is good practice for base classes
    virtual ~Shape() {} 
};

// Derived Class 1
class Square : public Shape {
private:
    double side;
public:
    Square(double s) : side(s) {}
    
    // Must implement area() to be concrete
    double area() const override {
        return side * side;
    }
};

// Derived Class 2
class Triangle : public Shape {
private:
    double base;
    double height;
public:
    Triangle(double b, double h) : base(b), height(h) {}
    
    // Must implement area() to be concrete
    double area() const override {
        return (base * height) / 2.0;
    }
};

int main() {
    // Use smart pointers (unique_ptr) for automatic cleanup and safety
    std::vector<std::unique_ptr<Shape>> shapes;
    
    // Store derived objects via base class pointers
    shapes.push_back(std::make_unique<Square>(8.0));
    shapes.push_back(std::make_unique<Triangle>(10.0, 5.0));
    shapes.push_back(std::make_unique<Square>(3.0));
    
    std::cout << "--- Calculating Areas (Runtime Polymorphism) ---" << std::endl;
    
    for (const auto& shape_ptr : shapes) {
        // The call to shape_ptr->area() executes the correct 
        // derived method (Square::area or Triangle::area) at RUNTIME.
        std::cout << "Area: " << shape_ptr->area() << std::endl; 
    }
    
    // Smart pointers automatically delete the objects when 'shapes' goes out of scope.

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This is the canonical example of Runtime Polymorphism, achieved through virtual functions and pointers (or references) to the base class.

  • Mechanism: The Shape pointer (or std::unique_ptr<Shape>) doesn’t know the exact object type it points to until runtime. The compiler uses the v-table (virtual table) lookup to correctly dispatch the call to the appropriate derived function (Square::area() or Triangle::area()).
  • Dynamic Binding: The decision of which function to run is made at runtime, known as dynamic binding. This allows us to treat a collection of different object types uniformly via their common base class interface.

Exercise 21: Virtual Destructor

Problem Statement: Create a base class Base and a derived class Derived. The Derived class should manage a simple dynamically allocated resource (e.g., new int). If the Base class does not have a virtual destructor, then the memory leak will occurs when deleting a derived object via a base class pointer. so, add a virtual destructor to Base and show that the leak is fixed.

Expected Output:

2. Derived constructor: Resource allocated.

Deleting Derived object via Base pointer...
3. Derived destructor called: Resource DEALLOCATED.
1. Base destructor called.
+ Hint

When deleting a derived object through a non-virtual base pointer, only the base class destructor is called. A virtual destructor ensures the derived class destructor is called first.

+ Show Solution
#include <iostream>

// Base Class
class Base {
public:
    // Change to 'virtual ~Base() {}' to fix the leak
    virtual ~Base() { std::cout << "1. Base destructor called." << std::endl; }
};
// Derived Class
class Derived : public Base {
private:
    int* resource;
public:
    Derived() {
        resource = new int(100);
        std::cout << "2. Derived constructor: Resource allocated." << std::endl;
    }

    ~Derived() {
        delete resource; // Cleanup step
        std::cout << "3. Derived destructor called: Resource DEALLOCATED." << std::endl;
    }
};

void run_test() {
    Base* ptr = new Derived(); // Base pointer pointing to Derived object
    std::cout << "\nDeleting Derived object via Base pointer..." << std::endl;
    delete ptr; 
}

int main() {
    run_test();
    
    return 0;
}Code language: C++ (cpp)
Run

Explanation:

The Virtual Destructor is essential in an inheritance hierarchy involving polymorphism and dynamic memory.

  • The Problem (Non-Virtual): When delete ptr; is executed on a base class pointer (Base*) pointing to a derived object (Derived), the compiler performs static binding for the destructor unless it’s virtual. It only calls Base::~Base(), skipping Derived::~Derived().
  • The Result: The resource (new int) allocated in the derived class is never freed, leading to a memory leak.
  • The Fix: Declaring the base class destructor as virtual enables dynamic binding for the destructor call, ensuring that the most derived destructor (Derived::~Derived()) is called first, which then correctly calls the base destructors up the chain, preventing the leak.

Exercise 22: Function Overloading (Static Polymorphism)

Problem Statement: In a class called Printer, create two public methods named print_info(). The first version should take no arguments and print a default message. The second version should take a single std::string argument, interpret it as a message, and print that message prefixed with “Custom: “. This demonstrates Function Overloading.

Expected Output:

Default: No specific message provided.
Custom: Hello OOP!
+ Hint

Function overloading requires functions with the same name but different signatures (different number or types of parameters). The return type does not matter for overloading.

+ Show Solution
#include <iostream>
#include <string>

class Printer {
public:
    // 1. Overloaded version: No arguments (Default Behavior)
    void print_info() const {
        std::cout << "Default: No specific message provided." << std::endl;
    }

    // 2. Overloaded version: One string argument (Custom Behavior)
    void print_info(const std::string& message) const {
        std::cout << "Custom: " << message << std::endl;
    }
};

int main() {
    Printer p;
    
    p.print_info();                 // Calls version 1
    p.print_info("Hello OOP!");     // Calls version 2

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

Function Overloading (also known as Static Polymorphism or Compile-time Polymorphism) allows multiple functions within the same scope to share the same name.

  • Resolution: The C++ compiler resolves which function to call based on the function’s signature (the name and the argument types/number) at compile time.
  • Clarity: It improves code readability by allowing a single, logical name (print_info) to be used for methods that perform the same general task but handle different types of input data.

Exercise 23: Operator Overloading (Binary Addition)

Problem Statement: Overload the binary addition operator (+) for the Point class (with private members x and y coordinates). The overloaded operator should take two Point objects as operands and return a new Point object that represents the component-wise sum: (x1, y1) + (x2, y2) = (x1+x2, y1+y2).

Given:

class Point {
private:
    int x;
    int y;
};Code language: C++ (cpp)

Expected Output:

P1: (10, 5)
P2: (3, 7)
P3 (P1 + P2): (13, 12)
+ Hint
  • The operator can be overloaded as a non-member function or a member function.
  • As a member function, it takes one argument (the right-hand operand).
  • The return type must be Point.
+ Show Solution
#include <iostream>

class Point {
private:
    int x;
    int y;

public:
    Point(int px = 0, int py = 0) : x(px), y(py) {}

    // Overload the binary + operator as a member function
    Point operator+(const Point& other) const {
        // Create a new Point object with the summed coordinates
        return Point(this->x + other.x, this->y + other.y);
    }

    void display() const {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
};

int main() {
    Point p1(10, 5);
    Point p2(3, 7);
    
    // Uses the overloaded operator: p1.operator+(p2)
    Point p3 = p1 + p2; 
    
    std::cout << "P1: "; p1.display();
    std::cout << "P2: "; p2.display();
    std::cout << "P3 (P1 + P2): "; p3.display(); // Expected: (13, 12)

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

Operator Overloading allows standard C++ operators (like +, -, *, etc.) to be redefined for use with user-defined classes. This enhances code intuition and readability.

  • Syntax: When overloaded as a member function, the left-hand operand is implicitly the object (p1) and the right-hand operand is passed as the argument (p2).
  • Return Value: The operator returns a new Point object, mimicking the mathematical behavior of addition where the operands are unchanged (maintained by the const on other and the function itself).

Exercise 24: Subscript Operator

Problem Statement: Create a simple ArrayWrapper class that holds a private array of 5 integers. Overload the subscript operator ([]) to allow access to the array elements using standard bracket notation. Implement basic boundary checking: if the index is out of bounds (0 to 4), throw an appropriate exception or print an error and exit.

Expected Output:

Initial arr[2]: 20
New arr[2]: 99
Boundary check for arr[5]
Error: Index out of bounds (0-4).
+ Hint

The subscript operator is a member function: int& operator[](int index). It should return a reference (int&) to allow both reading and writing (e.g., arr[1] = 99;).

+ Show Solution
#include <iostream>
#include <stdexcept>

class ArrayWrapper {
private:
    int data[5];
    const int SIZE = 5;

public:
    ArrayWrapper() {
        for (int i = 0; i < SIZE; ++i) {
            data[i] = i * 10;
        }
    }

    // Overload the subscript operator for R/W access
    int& operator[](int index) {
        if (index < 0 || index >= SIZE) {
            throw std::out_of_range("Error: Index out of bounds (0-4).");
        }
        return data[index];
    }
    
    // Optional: Overload for read-only access on const objects
    const int& operator[](int index) const {
        if (index < 0 || index >= SIZE) {
             throw std::out_of_range("Error: Index out of bounds (0-4).");
        }
        return data[index];
    }
};

int main() {
    ArrayWrapper arr;
    
    std::cout << "Initial arr[2]: " << arr[2] << std::endl; // 20
    
    // Write access
    arr[2] = 99;
    std::cout << "New arr[2]: " << arr[2] << std::endl; // 99

    try {
        // Test boundary check
        std::cout <<"Boundary check for arr[5]"<< std::endl; // 99
        std::cout << arr[5] << std::endl; 
    } catch (const std::out_of_range& e) {
        std::cerr << e.what() << std::endl; 
    }

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

Overloading the Subscript Operator ([]) allows a class object to be accessed using array notation, making user-defined containers behave like built-in arrays.

  • Reference Return: Returning an int& allows the operator to be used on the left side of an assignment (e.g., arr[2] = 99;), which modifies the original element.
  • Data Integrity: The implementation adds crucial boundary checking. This improves safety by catching common programming errors (accessing memory outside the array limits) and is a key feature that distinguishes a safe container class from a raw C-style array.

Exercise 25: Composition (Has-A relationship)

Problem Statement: Demonstrate Composition by creating a class Engine and a class Car. The Car class should contain an object of the Engine class (composition). The Engine class should have a method start() that prints “Engine started.” The Car class should have a method drive() that calls the Engine::start() method internally. The Engine should be initialized when the Car is constructed.

Expected Output:

EntEngine (V6) constructed.
Car (SedanX) assembled.

Starting SedanX drive.
--> V6 engine starting up...

End of main scope.
Car (SedanX) destroyed.
+ Hint

Declare an Engine object as a private member inside the Car class. Initialize the Engine member using the Car constructor’s initialization list.

+ Show Solution
#include <iostream>
#include <string>

// Component Class
class Engine {
private:
    std::string type;
public:
    Engine(std::string t) : type(t) {
        std::cout << "Engine (" << type << ") constructed." << std::endl;
    }
    
    void start() const {
        std::cout << "--> " << type << " engine starting up..." << std::endl;
    }
};

// Container Class (using Composition)
class Car {
private:
    std::string model;
    // Composition: Engine object is fully owned by Car. 
    // It cannot exist without the Car.
    Engine engine; 
    
public:
    // Car constructor initializes its own members and the Engine component.
    Car(std::string m, std::string engineType) 
        : model(m), 
          engine(engineType) // Initialize the Engine component
    {
        std::cout << "Car (" << model << ") assembled." << std::endl;
    }

    void drive() const {
        std::cout << "\nStarting " << model << " drive." << std::endl;
        engine.start(); // Car delegates the action to its component
    }
    
    // Destructor (implicitly called, but shows relationship)
    ~Car() {
        std::cout << "Car (" << model << ") destroyed." << std::endl;
        // Engine's destructor is automatically called here.
    }
};

int main() {
    Car my_car("SedanX", "V6");
    my_car.drive();
    
    std::cout << "\nEnd of main scope." << std::endl;
    return 0;
}Code language: C++ (cpp)
Run

Explanation:

Composition models a strong “Has-A” relationship where the component object (Engine) is an integral part of the container object (Car) and generally cannot exist independently of it.

  • Dependence: The Engine object is created and destroyed along with the Car object, demonstrating the tight lifecycle binding.
  • Ownership: The Car object has sole responsibility for the lifetime of its Engine component, making it the owner. This is typically implemented by including the component as a regular value member.
  • Delegation: The Car::drive() method delegates the core starting behavior to its internal Engine component, simplifying the car’s implementation.

Exercise 26: Aggregation (Has-A relationship)

Problem Statement: Demonstrate Aggregation by creating two classes: Employee and Department. The Department class should manage a group of Employee objects using a std::vector of pointers (Employee*). Implement methods to add an employee to the department. Ensure that the Employee objects can be created and destroyed independently of the Department object.

Expected Output:

Employee Alice hired.
Employee Bob hired.
[Dept] Employee added to HR.
[Dept] Employee added to HR.

--- HR Staff ---
Alice is performing tasks.
Bob is performing tasks.
Employee Alice retired/left.
Employee Bob retired/left.

Department HR shut down.
+ Hint
  • The Department class should store pointers (Employee*) to its members. The Employee objects should be created outside and passed into the department.
  • The department should not manage the memory of the employees (i.e., no delete in the department’s destructor).
+ Show Solution
#include <iostream>
#include <string>
#include <vector>

// Part Class (Independent existence)
class Employee {
private:
    std::string name;
public:
    Employee(std::string n) : name(n) {
        std::cout << "Employee " << name << " hired." << std::endl;
    }
    void work() const {
        std::cout << name << " is performing tasks." << std::endl;
    }
    ~Employee() {
        std::cout << "Employee " << name << " retired/left." << std::endl;
    }
};

// Whole Class (using Aggregation)
class Department {
private:
    std::string dept_name;
    // Aggregation: Stores pointers/references to objects owned elsewhere.
    std::vector<Employee*> staff; 
    
public:
    Department(std::string name) : dept_name(name) {}

    // Adds an Employee pointer owned outside the department
    void add_employee(Employee* emp) {
        staff.push_back(emp);
        std::cout << "[Dept] Employee added to " << dept_name << "." << std::endl;
    }

    void list_staff() const {
        std::cout << "\n--- " << dept_name << " Staff ---" << std::endl;
        for (const auto* emp : staff) {
            emp->work();
        }
    }
    
    // IMPORTANT: No 'delete' for Employee pointers in the Department destructor.
    ~Department() {
        std::cout << "\nDepartment " << dept_name << " shut down." << std::endl;
    }
};

int main() {
    // 1. Employees (parts) are created independently (on the heap)
    Employee* alice = new Employee("Alice");
    Employee* bob = new Employee("Bob");

    // 2. Department (whole) is created
    Department hr_dept("HR");
    
    // 3. Department uses the employees
    hr_dept.add_employee(alice);
    hr_dept.add_employee(bob);
    hr_dept.list_staff();

    // 4. Department goes out of scope and is destroyed, 
    // but Alice and Bob still exist until manually deleted.
    
    // Cleanup must be done externally (memory management not tied to Department)
    delete alice; 
    delete bob;
    
    return 0;
}Code language: C++ (cpp)
Run

Explanation:

Aggregation also models a “Has-A” relationship, but it’s weaker than composition. The key difference is the independent lifecycle of the parts (Employee).

  • Independent Lifecycle: Employee objects can exist and be destroyed regardless of whether the Department object exists.
  • Implementation: This is typically implemented by storing pointers or references to the components. The container (Department) only knows about the parts; it does not own their memory.
  • Flexibility: Aggregation is used when components are shared among multiple containers or when components’ lifetimes are managed externally (e.g., via a garbage collector or specific memory allocation logic).

Exercise 27: Library System as a Aggregation Example

Problem Statement: Model a Library class that uses aggregation to manage a collection of Book objects (pointers). Implement a method add_book(Book* book) to accept externally created books and a method list_books(). Assume the Book class exists (from exercise 3).

Given:

class Book {
private:
    std::string title;
public:
    Book(std::string t) : title(t) {}
    
    // CORRECTION: Return type changed from void to std::string
    std::string get_details() const { 
        return "  - Book Title: " + title;
    }
};Code language: C++ (cpp)

Expected Output:

[Library] Added:   - Book Title: C++ Primer
[Library] Added: - Book Title: Effective Modern C++

--- Central Library Catalog ---
- Book Title: C++ Primer
- Book Title: Effective Modern C++

Library closed. (Books remain in memory until explicitly deleted.)
+ Hint

Use a std::vector<Book*> to store the books. The Library destructor should not delete the books it holds, as that violates the principle of aggregation.

+ Show Solution
#include <iostream>
#include <string>
#include <vector>

// Part Class (Independent existence)
class Book {
private:
    std::string title;
public:
    Book(std::string t) : title(t) {}
    
    // CORRECTION: Return type changed from void to std::string
    std::string get_details() const { 
        return "  - Book Title: " + title;
    }
};

// Whole Class (using Aggregation)
class Library {
private:
    std::string name;
    // Aggregation: Stores pointers to books owned by the main program/user
    std::vector<Book*> catalog; 

public:
    Library(std::string n) : name(n) {}

    void add_book(Book* book) {
        catalog.push_back(book);
        // Works now because get_details() returns std::string
        std::cout << "[Library] Added: " << book->get_details() << std::endl;
    }

    void list_books() const {
        std::cout << "\n--- " << name << " Catalog ---" << std::endl;
        if (catalog.empty()) {
            std::cout << "Catalog is empty." << std::endl;
        } else {
            for (const auto* book : catalog) {
                // Works now because get_details() returns std::string
                std::cout << book->get_details() << std::endl; 
            }
        }
    }
    
    // Destructor: No memory cleanup for the Book pointers
    ~Library() {
        std::cout << "\nLibrary closed. (Books remain in memory until explicitly deleted.)" << std::endl;
    }
};

int main() {
    // Books are owned by main scope
    Book* b1 = new Book("C++ Primer");
    Book* b2 = new Book("Effective Modern C++");

    Library central("Central Library");
    central.add_book(b1);
    central.add_book(b2);
    
    central.list_books();
    
    // Explicit external cleanup is necessary in aggregation
    delete b1;
    delete b2;
    
    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This exercise clearly illustrates the memory management implications of Aggregation in the context of a library system.

  • External Ownership: The Book objects are created using new in main, meaning they exist on the heap, and their memory is controlled by the main function’s scope.
  • Container Role: The Library only stores the addresses (pointers) to these external objects. It acts as a logical container but does not assume memory responsibility.
  • Key Difference: If this were Composition, the Library would create the books itself (e.g., using std::vector<Book>) and its destructor would automatically destroy them.

Exercise 28: One-to-Many Relationship (Composition Choice)

Problem Statement: Implement a Team class that holds multiple Player objects (using composition). The Player class should be simple (name, number). Use a std::vector<Player> inside the Team class. Implement a Team constructor that initializes a list of players.

Expected Output:

Player Mike created.
Player Sara created.
Player Sara destroyed.
Player Mike destroyed.

Team The Mavericks formed.

--- The Mavericks Roster ---
Mike (#10)
Sara (#7)

Team The Mavericks disbanded.
Player Mike destroyed.
Player Sara destroyed.
Player Mike destroyed.
Player Sara destroyed.
+ Hint

Use a value-based std::vector<Player>. This ensures the Player objects are automatically constructed and destroyed when the Team object is.

+ Show Solution
#include <iostream>
#include <string>
#include <vector>

// Component Class
class Player {
private:
    std::string name;
    int number;
public:
    Player(std::string n, int num) : name(n), number(num) {
        std::cout << "Player " << name << " created." << std::endl;
    }
    std::string get_info() const {
        return name + " (#" + std::to_string(number) + ")";
    }
    ~Player() {
        std::cout << "Player " << name << " destroyed." << std::endl;
    }
};

// Container Class (using Composition)
class Team {
private:
    std::string team_name;
    // Composition: Players are value members, owned by the Team.
    std::vector<Player> roster; 
    
public:
    Team(std::string name, const std::vector<Player>& initial_roster) 
        : team_name(name), 
          roster(initial_roster) // Players are deep-copied into the vector
    {
        std::cout << "\nTeam " << team_name << " formed." << std::endl;
    }

    void list_roster() const {
        std::cout << "\n--- " << team_name << " Roster ---" << std::endl;
        for (const auto& player : roster) {
            std::cout << player.get_info() << std::endl;
        }
    }
    
    ~Team() {
        std::cout << "\nTeam " << team_name << " disbanded." << std::endl;
        // All Player destructors are automatically called by vector's destructor
    }
};

int main() {
    // Temporary list of players (these will be copied)
    std::vector<Player> initial = {
        Player("Mike", 10), 
        Player("Sara", 7)
    };

    Team champions("The Mavericks", initial);
    champions.list_roster();
    
    // When 'champions' is destroyed, the players within its roster are also destroyed.
    return 0;
}Code language: C++ (cpp)
Run

Explanation:

This models a One-to-Many relationship using Composition, which is often the best choice for modeling groups where the existence of the members is entirely dependent on the group.

  • Strong Ownership: By using std::vector<Player>, the Team object directly owns the Player objects. The Player objects are created/copied when the Team object is constructed and are automatically destroyed when the Team object is destroyed.
  • Memory Management: The use of std::vector automates memory management, ensuring no memory leaks and demonstrating a clean composition lifecycle, as the Team is solely responsible for its players.

Exercise 29: Static Member Variable

Problem Statement: In the BankAccount class (from Exercise. 10), add a private static member int total_accounts initialized to 0. Increment this counter in the constructor and decrement it in the destructor. Print the value in the constructor and destructor to demonstrate tracking.

Expected Output:

Start (Initial count: 0)
Account A101 opened. Total accounts: 1
Account A102 opened. Total accounts: 2
Account A103 opened. Total accounts: 3
Current total inside scope: 3
Account A103 closed. Total accounts remaining: 2
Account A102 closed. Total accounts remaining: 1
End of scope total: 1
Account A101 closed. Total accounts remaining: 0
+ Hint

The static member must be declared inside the class and defined (initialized) outside the class definition, typically in the accompanying .cpp file (or below main for a single file).

+ Show Solution
#include <iostream>
#include <string>

class BankAccount {
private:
    std::string account_number;
    // Static member: tracks class-level data
    static int total_accounts; 

public:
    // Constructor increments the shared counter
    BankAccount(std::string accNum) : account_number(accNum) {
        total_accounts++;
        std::cout << "Account " << account_number << " opened. Total accounts: " << total_accounts << std::endl;
    }
    
    // Destructor decrements the shared counter
    ~BankAccount() {
        total_accounts--;
        std::cout << "Account " << account_number << " closed. Total accounts remaining: " << total_accounts << std::endl;
    }
    
    // Static method to access the static variable
    static int get_total_accounts() {
        return total_accounts;
    }
};

// Definition and Initialization of the static member
int BankAccount::total_accounts = 0; 

int main() {
    std::cout << "Start (Initial count: " << BankAccount::get_total_accounts() << ")" << std::endl;
    
    BankAccount a1("A101"); // Count: 1
    {
        BankAccount a2("A102"); // Count: 2
        BankAccount a3("A103"); // Count: 3
        std::cout << "Current total inside scope: " << BankAccount::get_total_accounts() << std::endl;
    } // a2 and a3 destructors called here (Count: 1)
    
    std::cout << "End of scope total: " << BankAccount::get_total_accounts() << std::endl;
    // a1 destructor called at end of main (Count: 0)

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

A Static Member Variable (or class variable) is a single, shared instance of a variable that exists regardless of how many objects of the class are created.

  • Shared State: total_accounts is shared by all BankAccount objects and is stored separately from the individual object data.
  • Lifecycle Tracking: By incrementing in the constructor and decrementing in the destructor, the static variable accurately tracks the live count of objects currently instantiated, providing crucial class-level statistics.
  • Initialization: Static non-const members must be defined and initialized outside the class body, as shown by int BankAccount::total_accounts = 0;.

Exercise 30: Friend Function

Problem Statement: Create a class Box with private members length and width. Implement a non-member function named calculate_volume(const Box& b, int height) that calculates the volume (l * w * h). To allow this external function to access the private length and width, declare it as a friend function inside the Box class.

Expected Output:

Box dimensions: 10x5x2
Calculated Volume (via Friend Function): 100
+ Hint

Declare friend before the function signature inside the class body. The friend function takes a const reference to a Box object.

+ Show Solution
#include <iostream>

class Box {
private:
    int length;
    int width;

public:
    Box(int l, int w) : length(l), width(w) {}

    // Declaration of the non-member friend function
    friend int calculate_volume(const Box& b, int height); 
};

// Definition of the non-member function (does not use Box:: scope)
int calculate_volume(const Box& b, int height) {
    // The friend function can directly access private members 'length' and 'width'
    return b.length * b.width * height;
}

int main() {
    Box my_box(10, 5); // Length=10, Width=5
    int h = 2;

    // External function call, accessing private data
    int volume = calculate_volume(my_box, h); 
    
    std::cout << "Box dimensions: 10x5x2" << std::endl;
    std::cout << "Calculated Volume (via Friend Function): " << volume << std::endl; // 100

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

A Friend Function is a function defined outside a class’s scope but given special permission to access the private and protected members of that class.

  • Breaking Encapsulation (Controlled): Friendship is a way to intentionally relax strict encapsulation when necessary, often for overloaded operators (like operator<< in Ex. 28) or utility functions closely tied to the class’s data.
  • Non-Member Status: calculate_volume is not a member of the Box class. It is a regular external function, but the friend declaration inside Box grants the necessary access rights.
  • Use Case: Here, it allows a logically related but non-member utility function to perform its task efficiently by avoiding public getters for simple data access.

Exercise 31: Friend Class

Problem Statement: Create two classes, Data (with a private member int value) and Accessor. Make class Accessor a friend class of Data. Implement a method display_value(const Data& d) inside Accessor that accesses and prints the private value of a Data object.

Expected Output:

Friend Accessor read Data value: 1234
+ Hint

Declare friend class Accessor; inside the Data class. The Accessor class can then define methods that access all private members of Data objects passed to it.

+ Show Solution
#include <iostream>

// Forward declaration needed for the friend declaration
class Accessor; 

class Data {
private:
    int value;

public:
    Data(int v) : value(v) {}

    // Declare Accessor as a friend class
    friend class Accessor; 
};

class Accessor {
public:
    // This method belongs to Accessor, but can access Data's private members
    void display_value(const Data& d) const {
        // Direct access to d.value is allowed because Accessor is a friend
        std::cout << "Friend Accessor read Data value: " << d.value << std::endl;
        // We could also modify it if 'd' were not const
    }
};

int main() {
    Data private_data(1234);
    Accessor friend_accessor;

    // Accessor object uses its method to read Data's private member
    friend_accessor.display_value(private_data);
    
    // std::cout << private_data.value; // Compiler Error (Private)

    return 0;
}Code language: C++ (cpp)
Run

Explanation:

A Friend Class grants all methods within the accessor class the ability to access the private and protected members of the class that declared the friendship.

  • Broad Access: Unlike friend functions, which grant access only to a single function, friend classes grant blanket access to the entire class of methods.
  • Use Case: This pattern is useful for unit testing or when implementing an internal helper class (like an iterator or a specialized manager) that must be tightly coupled with the private implementation of another class.

Filed Under: CPP Exercises

Did you find this page helpful? Let others know about it. Sharing helps me continue to create free Python resources.

TweetF  sharein  shareP  Pin

About Vishal

Image

I’m Vishal Hule, the Founder of PYnative.com. As a Python developer, I enjoy assisting students, developers, and learners. Follow me on Twitter.

Related Tutorial Topics:

CPP Exercises

All Coding Exercises:

C Exercises
C++ Exercises
Python Exercises

Python Exercises and Quizzes

Free coding exercises and quizzes cover Python basics, data structure, data analytics, and more.

  • 15+ Topic-specific Exercises and Quizzes
  • Each Exercise contains 25+ questions
  • Each Quiz contains 25 MCQ
Exercises
Quizzes

Leave a Reply Cancel reply

your email address will NOT be published. all comments are moderated according to our comment policy.

Use <pre> tag for posting code. E.g. <pre> Your entire code </pre>

In: CPP Exercises
TweetF  sharein  shareP  Pin

  CPP Exercises

  • All C++ Exercises
  • C++ Exercise for Beginners
  • C++ Loops Exercise
  • C++ Functions Exercise
  • C++ Arrays Exercise
  • C++ String Exercise
  • C++ Pointers Exercise
  • C++ OOP Exercise
  • C++ File Handling Exercise
  • C++ Structures and Enums Exercise
  • C++ Templates & Generic Programming Exercise

All Coding Exercises

C Exercises C++ Exercises Python Exercises

About PYnative

PYnative.com is for Python lovers. Here, You can get Tutorials, Exercises, and Quizzes to practice and improve your Python skills.

Explore Python

  • Learn Python
  • Python Basics
  • Python Databases
  • Python Exercises
  • Python Quizzes
  • Online Python Code Editor
  • Python Tricks

Follow Us

To get New Python Tutorials, Exercises, and Quizzes

  • Twitter
  • Facebook
  • Sitemap

Legal Stuff

  • About Us
  • Contact Us

We use cookies to improve your experience. While using PYnative, you agree to have read and accepted our:

  • Terms Of Use
  • Privacy Policy
  • Cookie Policy

Copyright © 2018–2025 pynative.com

Advertisement