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,
thispointer, 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)
Explanation:
- This exercise shows how objects combine data and behavior.
- The
Rectangleobject holds the data (length,width), and the methods (calculate_area,calculate_perimeter) define the behavior (operations) that can be performed on that data. - The
constkeyword 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)
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)
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
constmust 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)
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)
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)
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)
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)
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)
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:
- Efficiency: It avoids a temporary default construction followed by an assignment.
- Necessity: It is required for initializing
constmembers (likenameabove) and reference members, as these cannot be assigned a value after they are created. - 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)
Explanation:
This exercise fully implements Encapsulation and demonstrates controlled access to the object’s state.
- Data Hiding: The
balanceis private, meaning it can only be changed by the object’s own methods (depositandwithdraw). - State Management: The
withdrawmethod 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)
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 markedconstbecause 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)
Explanation:
This exercise demonstrates how Data Hiding is used to enforce data integrity and create immutable properties.
- Immutability: By making the
yearprivate and providing no public setter, the class guarantees that theyearof aCarobject, once set by the constructor, cannot be changed during the object’s lifetime. - Controlled Mutability: In contrast, the
makeproperty is still mutable because aset_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 beconstbecause 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)
Explanation:
This demonstrates the use of Static Members and Controlled Mutability on a class-level variable.
- Static Member:
next_serial_numberbelongs to the class itself, not to any individual object. All instances ofSerialGeneratorshare 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 toget_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 (g1org2).
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
Dogclass defines its own version ofeat().
+ 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)
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:
Doginherits from one base class,Animal. - Code Reusability: The
Dogclass automatically gets thesleep()method without needing to redefine it, reducing code duplication. - Method Overriding: By defining a new
eat()method inDogwith the exact same signature as the one inAnimal, the derived class replaces the base class’s implementation when called on aDogobject.
Exercise 15: Multilevel Inheritance
Problem Statement: Implement multilevel inheritance using three classes: Vehicle –> Car –> SportsCar.
Vehicleshould have a methodstart_transport().Carshould inherit fromVehicleand add a private attributeint number_of_doorsand a methodopen_door().SportsCarshould inherit fromCarand add a private attributeint max_speedand a methodactivate_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)
Explanation:
- Multilevel Inheritance creates a deep hierarchy where a class inherits from a class that, in turn, inherits from another class.
- Hierarchy:
SportsCaris aCar, and aCaris aVehicle.SportsCarinherits the methods and properties from both its ancestors. - Constructor Chaining: Crucially, the constructor of
SportsCarmust first call the constructor ofCar, which then calls the constructor ofVehicle, 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
protectedaccess specifier forage. protectedmembers behave likeprivateoutside the inheritance chain, but likepublicwithin 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)
Explanation:
The protected access specifier offers a balance between private and public access.
- Internal Access:
protectedmembers are directly accessible by methods of the class itself and by methods of any classes derived from it (StudentandTeacher). - External Restriction: Like
privatemembers,protectedmembers are inaccessible from outside the class hierarchy (e.g., in themainfunction). - 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)
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:
Vehicleconstructor runs.Carconstructor runs.SportsCarconstructor 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)
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 declaredvirtual, 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)
Explanation:
By declaring area() as a pure virtual function, we create an Abstract Base Class (Shape).
- Abstraction: The
Shapeclass 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
Shapeclass is incomplete (it has a function without a definition), the compiler prevents direct creation ofShapeobjects.
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)
Explanation:
This is the canonical example of Runtime Polymorphism, achieved through virtual functions and pointers (or references) to the base class.
- Mechanism: The
Shapepointer (orstd::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()orTriangle::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)
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 callsBase::~Base(), skippingDerived::~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
virtualenables 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)
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)
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
Pointobject, mimicking the mathematical behavior of addition where the operands are unchanged (maintained by theconstonotherand 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)
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)
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
Engineobject is created and destroyed along with theCarobject, demonstrating the tight lifecycle binding. - Ownership: The
Carobject has sole responsibility for the lifetime of itsEnginecomponent, 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 internalEnginecomponent, 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
Departmentclass should store pointers (Employee*) to its members. TheEmployeeobjects should be created outside and passed into the department. - The department should not manage the memory of the employees (i.e., no
deletein 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)
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:
Employeeobjects can exist and be destroyed regardless of whether theDepartmentobject 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)
Explanation:
This exercise clearly illustrates the memory management implications of Aggregation in the context of a library system.
- External Ownership: The
Bookobjects are created usingnewinmain, meaning they exist on the heap, and their memory is controlled by themainfunction’s scope. - Container Role: The
Libraryonly 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
Librarywould create the books itself (e.g., usingstd::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)
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>, theTeamobject directly owns thePlayerobjects. ThePlayerobjects are created/copied when theTeamobject is constructed and are automatically destroyed when theTeamobject is destroyed. - Memory Management: The use of
std::vectorautomates memory management, ensuring no memory leaks and demonstrating a clean composition lifecycle, as theTeamis 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)
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_accountsis shared by allBankAccountobjects 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-
constmembers must be defined and initialized outside the class body, as shown byint 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)
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_volumeis not a member of theBoxclass. It is a regular external function, but thefrienddeclaration insideBoxgrants 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)
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.

Leave a Reply