Module 5.1

Classes & Objects

Learn the foundation of Object-Oriented Programming in C++. Discover how to define classes, create objects, use access modifiers, and implement constructors and destructors to build well-structured, maintainable code.

45 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Class definition and structure
  • Access modifiers (public, private, protected)
  • Constructors and destructors
  • Member functions and data members
  • Object instantiation and usage
Contents
01

Introduction to OOP

Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects rather than functions and logic. C++ is one of the most powerful OOP languages, allowing you to model real-world entities as classes and objects.

Programming Paradigm

Object-Oriented Programming

Object-Oriented Programming (OOP) is a programming approach that bundles data (attributes) and the functions that operate on that data (methods) into units called objects. This mirrors how we think about real-world entities - a car has properties (color, speed) and behaviors (accelerate, brake).

Four Pillars of OOP: Encapsulation (bundling data and methods), Abstraction (hiding complexity), Inheritance (creating class hierarchies), Polymorphism (same interface, different implementations).

// Procedural approach - data and functions are separate
struct CarData {
    string brand;
    int speed;
};

void accelerate(CarData& car) {
    car.speed += 10;
}

In procedural programming, we define data structures and functions separately. The CarData struct holds the data, and the accelerate function operates on it. While this works, it does not naturally group related functionality together, and anyone can directly access and modify the data without any validation or control.

// Object-oriented approach - data and behavior together
class Car {
private:
    string brand;
    int speed;
    
public:
    void accelerate() {
        speed += 10;
    }
    
    int getSpeed() {
        return speed;
    }
};

With OOP, the Car class encapsulates both data (brand, speed) and behavior (accelerate, getSpeed) in a single unit. The private keyword protects the data from direct external access, while public methods provide controlled ways to interact with the object. This is encapsulation in action.

// Creating and using objects
int main() {
    Car myCar;           // Create an object (instance) of Car
    myCar.accelerate();  // Call method on the object
    cout << "Speed: " << myCar.getSpeed() << endl;
    
    return 0;
}

An object is an instance of a class - when you create myCar, you are creating a specific car with its own data. You interact with the object through its public methods using dot notation. Each object maintains its own state independently, so multiple Car objects can have different speeds.

Why Use OOP?

Encapsulation

Bundle data and methods together, protecting internal state from unauthorized access

Reusability

Create classes once, use them throughout your codebase and in future projects

Inheritance

Create new classes based on existing ones, promoting code reuse and hierarchy

Maintainability

Changes to a class are localized, making code easier to update and debug

Concept Description C++ Keyword
Class Blueprint for creating objects class
Object Instance of a class Variable of class type
Data Members Variables inside a class Declared in class body
Member Functions Functions inside a class Methods defined in class
Access Modifiers Control visibility public, private, protected
02

Defining Classes

A class is a user-defined data type that serves as a blueprint for creating objects. It defines what data an object will hold and what operations can be performed on that data. Classes are the foundation of object-oriented design in C++.

OOP Concept

Class

A class is a template that defines the properties (data members) and behaviors (member functions) that objects of that type will have. Think of a class as a cookie cutter - it defines the shape, but each cookie (object) is a separate entity.

Syntax: class ClassName { access_modifier: members }; - Note the semicolon after the closing brace!

// Basic class definition
class Student {
public:
    string name;
    int age;
    double gpa;
};

This is the simplest form of a class. The class keyword is followed by the class name (conventionally using PascalCase). Inside the curly braces, we declare member variables that every Student object will have. The public: access modifier makes these members accessible from outside the class. Note the required semicolon after the closing brace.

// Creating objects (instances)
int main() {
    Student alice;         // Object created on stack
    alice.name = "Alice";  // Access member with dot operator
    alice.age = 20;
    alice.gpa = 3.8;
    
    cout << alice.name << " has GPA: " << alice.gpa << endl;
    return 0;
}

Creating an object is similar to declaring a variable - the class name is the type. The dot operator (.) accesses members of the object. Each object has its own copy of the data members, so alice.name and bob.name (another Student object) would be independent.

// Class with methods (member functions)
class Rectangle {
public:
    double width;
    double height;
    
    // Member function defined inside class
    double area() {
        return width * height;
    }
    
    // Member function declared inside, defined outside
    double perimeter();
};

// Define member function outside class using scope resolution
double Rectangle::perimeter() {
    return 2 * (width + height);
}

Classes can contain both data and functions. Functions defined inside the class are implicitly inline. For larger functions, declare them inside the class but define them outside using the scope resolution operator (::). The Rectangle:: prefix tells the compiler that perimeter belongs to the Rectangle class.

// Using the Rectangle class
int main() {
    Rectangle room;
    room.width = 10.5;
    room.height = 8.0;
    
    cout << "Area: " << room.area() << endl;           // 84
    cout << "Perimeter: " << room.perimeter() << endl; // 37
    
    return 0;
}

Member functions have direct access to all data members of the object they are called on. When you call room.area(), the function uses room.width and room.height automatically. This is one of the key benefits of OOP - data and the functions that operate on it are bundled together.

Common Mistake: Forgetting the semicolon after the class definition's closing brace is one of the most common C++ errors. The compiler error messages can be confusing, so always check for this!

Practice Questions: Defining Classes

Task: Create a class called Book with public members: title (string) and pages (int). Create an object and print the book info.

Show Solution
#include <iostream>
#include <string>
using namespace std;

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

int main() {
    Book myBook;
    myBook.title = "C++ Primer";
    myBook.pages = 976;
    
    cout << myBook.title << " has " << myBook.pages << " pages" << endl;
    return 0;
}

Task: Create a Student class with name (string), rollNumber (int), and grade (char). Add a display() method to print all details.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Student {
public:
    string name;
    int rollNumber;
    char grade;
    
    void display() {
        cout << "Name: " << name << endl;
        cout << "Roll Number: " << rollNumber << endl;
        cout << "Grade: " << grade << endl;
    }
};

int main() {
    Student s1;
    s1.name = "Alice";
    s1.rollNumber = 101;
    s1.grade = 'A';
    
    s1.display();
    return 0;
}

Task: Create a Rectangle class with length and width. Add methods area() and perimeter() that return the calculated values. Test with a rectangle of 10x5.

Show Solution
#include <iostream>
using namespace std;

class Rectangle {
public:
    double length;
    double width;
    
    double area() {
        return length * width;
    }
    
    double perimeter() {
        return 2 * (length + width);
    }
};

int main() {
    Rectangle rect;
    rect.length = 10;
    rect.width = 5;
    
    cout << "Area: " << rect.area() << endl;           // 50
    cout << "Perimeter: " << rect.perimeter() << endl; // 30
    return 0;
}

Task: Create a BankAccount class with accountNumber, holderName, and balance. Add deposit() and withdraw() methods that update the balance.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class BankAccount {
public:
    string accountNumber;
    string holderName;
    double balance;
    
    void deposit(double amount) {
        balance += amount;
        cout << "Deposited: $" << amount << endl;
    }
    
    void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            cout << "Withdrawn: $" << amount << endl;
        } else {
            cout << "Insufficient funds!" << endl;
        }
    }
    
    void showBalance() {
        cout << "Current Balance: $" << balance << endl;
    }
};

int main() {
    BankAccount acc;
    acc.accountNumber = "ACC001";
    acc.holderName = "John Doe";
    acc.balance = 1000;
    
    acc.showBalance();    // $1000
    acc.deposit(500);     // Deposited: $500
    acc.withdraw(200);    // Withdrawn: $200
    acc.showBalance();    // $1300
    return 0;
}

Task: Create a Circle class with radius. Declare methods area(), circumference(), and diameter() inside the class but define them outside using scope resolution operator.

Show Solution
#include <iostream>
using namespace std;

const double PI = 3.14159;

class Circle {
public:
    double radius;
    
    // Method declarations
    double area();
    double circumference();
    double diameter();
};

// Method definitions outside class
double Circle::area() {
    return PI * radius * radius;
}

double Circle::circumference() {
    return 2 * PI * radius;
}

double Circle::diameter() {
    return 2 * radius;
}

int main() {
    Circle c;
    c.radius = 5;
    
    cout << "Radius: " << c.radius << endl;
    cout << "Diameter: " << c.diameter() << endl;       // 10
    cout << "Area: " << c.area() << endl;               // 78.5398
    cout << "Circumference: " << c.circumference() << endl; // 31.4159
    return 0;
}
03

Access Modifiers

Access modifiers control the visibility of class members. They are fundamental to encapsulation, allowing you to hide implementation details while exposing a clean interface. C++ provides three levels of access: public, private, and protected.

Encapsulation

Access Modifiers

Access modifiers determine which parts of your code can access class members. public members are accessible everywhere, private members only within the class itself, and protected members within the class and its derived classes.

Default Access: In a class, members are private by default. In a struct, they are public by default.

class BankAccount {
private:
    double balance;      // Only accessible within class
    string accountNumber;
    
protected:
    string ownerName;    // Accessible in derived classes too
    
public:
    void deposit(double amount);   // Accessible everywhere
    double getBalance();
};

The private section contains sensitive data like balance that should never be directly modified from outside. The protected section contains data that derived classes (like SavingsAccount) might need. The public section provides the interface - the safe ways to interact with the account.

class BankAccount {
private:
    double balance = 0;
    
public:
    void deposit(double amount) {
        if (amount > 0) {          // Validation!
            balance += amount;
        }
    }
    
    bool withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;              // Insufficient funds
    }
    
    double getBalance() {
        return balance;            // Read-only access
    }
};

This demonstrates why private data with public methods is powerful. The deposit method validates that amounts are positive. The withdraw method ensures you cannot withdraw more than available. If balance were public, anyone could set it to any value, including negative numbers, breaking your program logic.

int main() {
    BankAccount myAccount;
    
    myAccount.deposit(1000);        // OK - public method
    myAccount.withdraw(500);        // OK - public method
    cout << myAccount.getBalance(); // OK - prints 500
    
    // myAccount.balance = 1000000; // ERROR! balance is private
    
    return 0;
}

From outside the class, you can only use public members. Attempting to access balance directly causes a compiler error. This is encapsulation - the internal state is hidden, and all interactions go through controlled methods. This makes your code more robust and easier to maintain.

Modifier Same Class Derived Class Outside
public Yes Yes Yes
protected Yes Yes No
private Yes No No
Best Practice: Make data members private and provide public getter/setter methods. This gives you control over how data is accessed and modified, allowing validation and future changes without breaking existing code.

Practice Questions: Access Modifiers

Task: Create a Person class with private age and public methods setAge() (validates age is 0-150) and getAge().

Show Solution
#include <iostream>
using namespace std;

class Person {
private:
    int age;
    
public:
    void setAge(int a) {
        if (a >= 0 && a <= 150) {
            age = a;
        }
    }
    
    int getAge() {
        return age;
    }
};

int main() {
    Person p;
    p.setAge(25);
    cout << "Age: " << p.getAge() << endl;  // 25
    
    p.setAge(-5);  // Ignored due to validation
    cout << "Age: " << p.getAge() << endl;  // Still 25
    return 0;
}

Task: Create an Employee class with private salary and public name. Add setSalary() that only accepts positive values and getSalary() to read it.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Employee {
private:
    double salary;
    
public:
    string name;
    
    void setSalary(double s) {
        if (s > 0) {
            salary = s;
        } else {
            cout << "Invalid salary!" << endl;
        }
    }
    
    double getSalary() {
        return salary;
    }
};

int main() {
    Employee emp;
    emp.name = "John";
    emp.setSalary(50000);
    
    cout << emp.name << " earns $" << emp.getSalary() << endl;
    
    emp.setSalary(-1000);  // Invalid salary!
    cout << "Salary unchanged: $" << emp.getSalary() << endl;
    return 0;
}

Task: Create a Password class with private password. Add setPassword() that only accepts passwords with at least 8 characters, and checkPassword() that returns true if input matches.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Password {
private:
    string password;
    
public:
    bool setPassword(string pwd) {
        if (pwd.length() >= 8) {
            password = pwd;
            return true;
        }
        cout << "Password must be at least 8 characters!" << endl;
        return false;
    }
    
    bool checkPassword(string input) {
        return input == password;
    }
};

int main() {
    Password user;
    
    user.setPassword("short");      // Too short
    user.setPassword("mySecurePass123");  // OK
    
    if (user.checkPassword("mySecurePass123")) {
        cout << "Access granted!" << endl;
    } else {
        cout << "Access denied!" << endl;
    }
    return 0;
}

Task: Create a Temperature class with private celsius. Add setCelsius(), getCelsius(), setFahrenheit(), and getFahrenheit() methods that convert appropriately.

Show Solution
#include <iostream>
using namespace std;

class Temperature {
private:
    double celsius;
    
public:
    void setCelsius(double c) {
        celsius = c;
    }
    
    double getCelsius() {
        return celsius;
    }
    
    void setFahrenheit(double f) {
        celsius = (f - 32) * 5.0 / 9.0;
    }
    
    double getFahrenheit() {
        return celsius * 9.0 / 5.0 + 32;
    }
};

int main() {
    Temperature temp;
    
    temp.setCelsius(100);
    cout << "100C = " << temp.getFahrenheit() << "F" << endl;  // 212F
    
    temp.setFahrenheit(32);
    cout << "32F = " << temp.getCelsius() << "C" << endl;  // 0C
    return 0;
}

Task: Create a SecureAccount class with private balance and transactionCount. Add deposit(), withdraw() (validates sufficient funds), getBalance(), and getTransactionCount().

Show Solution
#include <iostream>
using namespace std;

class SecureAccount {
private:
    double balance;
    int transactionCount;
    
public:
    SecureAccount() {
        balance = 0;
        transactionCount = 0;
    }
    
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            transactionCount++;
            cout << "Deposited: $" << amount << endl;
        }
    }
    
    bool withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            transactionCount++;
            cout << "Withdrawn: $" << amount << endl;
            return true;
        }
        cout << "Transaction failed!" << endl;
        return false;
    }
    
    double getBalance() { return balance; }
    int getTransactionCount() { return transactionCount; }
};

int main() {
    SecureAccount acc;
    acc.deposit(1000);
    acc.deposit(500);
    acc.withdraw(200);
    acc.withdraw(2000);  // Fails
    
    cout << "Balance: $" << acc.getBalance() << endl;  // 1300
    cout << "Transactions: " << acc.getTransactionCount() << endl;  // 3
    return 0;
}
04

Constructors

Constructors are special member functions that initialize objects when they are created. They ensure that every object starts in a valid state. C++ provides several types of constructors for different initialization scenarios.

Special Member Function

Constructor

A constructor is a special function that is automatically called when an object is created. It has the same name as the class, no return type (not even void), and can be overloaded to accept different parameters.

Key Point: If you do not define any constructor, C++ provides a default constructor that initializes members to default values (zero for numeric types).

class Player {
private:
    string name;
    int health;
    int score;
    
public:
    // Default constructor - no parameters
    Player() {
        name = "Unknown";
        health = 100;
        score = 0;
    }
};

The default constructor takes no arguments and sets initial values for all members. It is called when you create an object without any arguments: Player p;. Without this constructor, members would have undefined values, potentially causing bugs. Always initialize all members.

class Player {
private:
    string name;
    int health;
    int score;
    
public:
    // Parameterized constructor
    Player(string n, int h) {
        name = n;
        health = h;
        score = 0;
    }
};

A parameterized constructor accepts arguments to customize the object at creation time. Call it with: Player hero("Hero", 150);. This is more convenient than creating an object and then setting each property. You can have multiple constructors with different parameters (overloading).

class Player {
private:
    string name;
    int health;
    int score;
    
public:
    // Constructor with initializer list (preferred)
    Player(string n, int h, int s) : name(n), health(h), score(s) {
        // Constructor body - additional logic if needed
    }
};

The initializer list (after the colon) directly initializes members, which is more efficient than assigning in the body. For const members and references, this is the only way to initialize them. The format is member(value) separated by commas. This is the preferred style in modern C++.

class Player {
public:
    string name;
    int health;
    int score;
    
    // Multiple constructors (overloading)
    Player() : name("Unknown"), health(100), score(0) {}
    
    Player(string n) : name(n), health(100), score(0) {}
    
    Player(string n, int h) : name(n), health(h), score(0) {}
    
    Player(string n, int h, int s) : name(n), health(h), score(s) {}
};

Constructor overloading allows multiple ways to create objects. The compiler chooses the appropriate constructor based on the arguments provided. This provides flexibility while keeping the interface clean.

int main() {
    Player p1;                        // Default: Unknown, 100, 0
    Player p2("Alice");               // Just name: Alice, 100, 0
    Player p3("Bob", 150);            // Name and health: Bob, 150, 0
    Player p4("Charlie", 200, 50);    // All values: Charlie, 200, 50
    
    return 0;
}

Each object is created using a different constructor based on the arguments. This is a key advantage of constructor overloading - users of your class can choose the most convenient way to create objects based on what information they have available.

Copy Constructor: C++ automatically provides a copy constructor that creates a new object as a copy of an existing one: Player p5 = p4; or Player p5(p4);. You can override it for custom copying behavior.

Practice Questions: Constructors

Task: Create a Point class with x and y coordinates. Add a default constructor that sets both to 0, and a display() method.

Show Solution
#include <iostream>
using namespace std;

class Point {
private:
    int x, y;
    
public:
    Point() {
        x = 0;
        y = 0;
    }
    
    void display() {
        cout << "(" << x << ", " << y << ")" << endl;
    }
};

int main() {
    Point p;
    p.display();  // (0, 0)
    return 0;
}

Task: Create a Car class with brand and year. Add a parameterized constructor to initialize both values and a showInfo() method.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Car {
private:
    string brand;
    int year;
    
public:
    Car(string b, int y) {
        brand = b;
        year = y;
    }
    
    void showInfo() {
        cout << year << " " << brand << endl;
    }
};

int main() {
    Car car1("Toyota", 2022);
    Car car2("Honda", 2023);
    
    car1.showInfo();  // 2022 Toyota
    car2.showInfo();  // 2023 Honda
    return 0;
}

Task: Create a Product class with name, price, and quantity. Add a default constructor and a parameterized constructor. Use initializer lists for both.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Product {
private:
    string name;
    double price;
    int quantity;
    
public:
    // Default constructor with initializer list
    Product() : name("Unknown"), price(0.0), quantity(0) {}
    
    // Parameterized constructor with initializer list
    Product(string n, double p, int q) : name(n), price(p), quantity(q) {}
    
    void display() {
        cout << name << ": $" << price << " x " << quantity << endl;
    }
};

int main() {
    Product p1;
    Product p2("Laptop", 999.99, 5);
    
    p1.display();  // Unknown: $0 x 0
    p2.display();  // Laptop: $999.99 x 5
    return 0;
}

Task: Create a Time class with hours, minutes, seconds. Add three constructors: default (00:00:00), hours only, and all three parameters.

Show Solution
#include <iostream>
#include <iomanip>
using namespace std;

class Time {
private:
    int hours, minutes, seconds;
    
public:
    // Default constructor
    Time() : hours(0), minutes(0), seconds(0) {}
    
    // Hours only
    Time(int h) : hours(h), minutes(0), seconds(0) {}
    
    // All parameters
    Time(int h, int m, int s) : hours(h), minutes(m), seconds(s) {}
    
    void display() {
        cout << setfill('0') << setw(2) << hours << ":"
             << setw(2) << minutes << ":"
             << setw(2) << seconds << endl;
    }
};

int main() {
    Time t1;           // 00:00:00
    Time t2(14);       // 14:00:00
    Time t3(9, 30, 45); // 09:30:45
    
    t1.display();
    t2.display();
    t3.display();
    return 0;
}

Task: Create a Date class with day, month, year. The constructor should validate that day is 1-31, month is 1-12. If invalid, set to 1/1/2000.

Show Solution
#include <iostream>
using namespace std;

class Date {
private:
    int day, month, year;
    
    bool isValid(int d, int m, int y) {
        return (d >= 1 && d <= 31) && (m >= 1 && m <= 12) && (y > 0);
    }
    
public:
    Date(int d, int m, int y) {
        if (isValid(d, m, y)) {
            day = d;
            month = m;
            year = y;
        } else {
            cout << "Invalid date! Setting to default." << endl;
            day = 1;
            month = 1;
            year = 2000;
        }
    }
    
    void display() {
        cout << day << "/" << month << "/" << year << endl;
    }
};

int main() {
    Date d1(15, 6, 2024);   // Valid: 15/6/2024
    Date d2(32, 13, 2024);  // Invalid: 1/1/2000
    
    d1.display();
    d2.display();
    return 0;
}
05

Destructors

Destructors are the opposite of constructors - they clean up when an object is destroyed. They are crucial for releasing resources like dynamic memory, file handles, or network connections that the object acquired during its lifetime.

Special Member Function

Destructor

A destructor is a special function called automatically when an object goes out of scope or is explicitly deleted. It has the same name as the class prefixed with a tilde (~), takes no parameters, and cannot be overloaded.

Syntax: ~ClassName() { /* cleanup code */ }

class FileHandler {
private:
    string filename;
    bool isOpen;
    
public:
    FileHandler(string name) : filename(name), isOpen(true) {
        cout << "Opening file: " << filename << endl;
    }
    
    ~FileHandler() {
        if (isOpen) {
            cout << "Closing file: " << filename << endl;
            isOpen = false;
        }
    }
};

The destructor ~FileHandler() ensures the file is closed when the object is destroyed. This pattern is called RAII (Resource Acquisition Is Initialization) - you acquire resources in the constructor and release them in the destructor. This prevents resource leaks even if exceptions occur.

int main() {
    cout << "Starting program" << endl;
    
    {
        FileHandler file("data.txt");
        cout << "Working with file" << endl;
    }  // file goes out of scope here - destructor called
    
    cout << "Back to main" << endl;
    return 0;
}

When the inner block ends, file goes out of scope and its destructor is automatically called. This is automatic cleanup - you do not need to remember to close the file. The output shows the sequence: Starting, Opening, Working, Closing, Back to main.

class DynamicArray {
private:
    int* data;
    int size;
    
public:
    DynamicArray(int s) : size(s) {
        data = new int[size];  // Allocate memory
        cout << "Allocated " << size << " integers" << endl;
    }
    
    ~DynamicArray() {
        delete[] data;  // Free memory
        cout << "Freed memory" << endl;
    }
};

When working with dynamic memory (new), destructors are essential. The constructor allocates memory with new, and the destructor frees it with delete[]. Without the destructor, this memory would leak every time an object is destroyed. This is one of the most important uses of destructors.

Memory Rule: If your constructor uses new, your destructor must use delete. Use delete[] for arrays allocated with new[]. Failing to do this causes memory leaks.

Practice Questions: Destructors

Task: Create a Message class that prints "Object created" in constructor and "Object destroyed" in destructor. Create an object and observe the output.

Show Solution
#include <iostream>
using namespace std;

class Message {
public:
    Message() {
        cout << "Object created" << endl;
    }
    
    ~Message() {
        cout << "Object destroyed" << endl;
    }
};

int main() {
    cout << "Before creating object" << endl;
    Message m;
    cout << "After creating object" << endl;
    return 0;
}  // Destructor called here automatically

Task: Create a Counter class with a static count. Increment in constructor, decrement in destructor. Print the count in both.

Show Solution
#include <iostream>
using namespace std;

class Counter {
private:
    static int count;
    
public:
    Counter() {
        count++;
        cout << "Object created. Count: " << count << endl;
    }
    
    ~Counter() {
        count--;
        cout << "Object destroyed. Count: " << count << endl;
    }
};

int Counter::count = 0;

int main() {
    Counter c1;
    {
        Counter c2;
        Counter c3;
    }  // c2 and c3 destroyed here
    cout << "Back in main" << endl;
    return 0;
}  // c1 destroyed here

Task: Create a Logger class with a name. Print "Logger [name] started" in constructor and "Logger [name] stopped" in destructor.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Logger {
private:
    string name;
    
public:
    Logger(string n) : name(n) {
        cout << "Logger [" << name << "] started" << endl;
    }
    
    ~Logger() {
        cout << "Logger [" << name << "] stopped" << endl;
    }
    
    void log(string message) {
        cout << "[" << name << "] " << message << endl;
    }
};

int main() {
    {
        Logger app("AppLog");
        app.log("Application running");
    }  // Destructor called here
    
    cout << "After logger scope" << endl;
    return 0;
}

Task: Create a Timer class that records creation time in constructor and prints elapsed time in destructor using chrono.

Show Solution
#include <iostream>
#include <chrono>
#include <thread>
using namespace std;
using namespace chrono;

class Timer {
private:
    steady_clock::time_point startTime;
    string label;
    
public:
    Timer(string l) : label(l) {
        startTime = steady_clock::now();
        cout << "Timer [" << label << "] started" << endl;
    }
    
    ~Timer() {
        auto endTime = steady_clock::now();
        auto duration = duration_cast<milliseconds>(endTime - startTime);
        cout << "Timer [" << label << "] ended: " << duration.count() << "ms" << endl;
    }
};

int main() {
    {
        Timer t("SleepTest");
        this_thread::sleep_for(milliseconds(500));
    }
    return 0;
}

Task: Create a DynamicArray class that allocates an int array in constructor and properly deallocates it in destructor. Add set() and get() methods.

Show Solution
#include <iostream>
using namespace std;

class DynamicArray {
private:
    int* data;
    int size;
    
public:
    DynamicArray(int s) : size(s) {
        data = new int[size];
        for (int i = 0; i < size; i++) {
            data[i] = 0;
        }
        cout << "Allocated array of size " << size << endl;
    }
    
    ~DynamicArray() {
        delete[] data;
        cout << "Freed array memory" << endl;
    }
    
    void set(int index, int value) {
        if (index >= 0 && index < size) {
            data[index] = value;
        }
    }
    
    int get(int index) {
        if (index >= 0 && index < size) {
            return data[index];
        }
        return -1;
    }
};

int main() {
    {
        DynamicArray arr(5);
        arr.set(0, 10);
        arr.set(2, 30);
        cout << "arr[0] = " << arr.get(0) << endl;
        cout << "arr[2] = " << arr.get(2) << endl;
    }  // Destructor frees memory here
    
    cout << "Memory freed safely" << endl;
    return 0;
}
06

Member Functions

Member functions define the behavior of a class. They can access all class members directly and operate on the object's data. Understanding different types of member functions helps you design clean and efficient classes.

Class Behavior

Member Functions

Member functions (also called methods) are functions declared within a class that operate on the class's data. They have access to all members (public, private, protected) and can be marked as const if they do not modify the object.

Types: Getters (accessors), Setters (mutators), Const member functions, Static member functions

class Circle {
private:
    double radius;
    
public:
    // Setter - modifies data
    void setRadius(double r) {
        if (r > 0) {
            radius = r;
        }
    }
    
    // Getter - reads data (const - does not modify)
    double getRadius() const {
        return radius;
    }
    
    // Calculation method (const - does not modify)
    double area() const {
        return 3.14159 * radius * radius;
    }
};

Getters and setters provide controlled access to private data. The const keyword after the parameter list indicates the function will not modify the object. This allows calling these methods on const objects and communicates intent to other programmers. Always mark functions as const when they do not modify state.

class Counter {
private:
    int count;
    static int totalCounters;  // Shared by all objects
    
public:
    Counter() : count(0) {
        totalCounters++;
    }
    
    void increment() { count++; }
    int getCount() const { return count; }
    
    // Static function - called on class, not object
    static int getTotalCounters() {
        return totalCounters;
    }
};

int Counter::totalCounters = 0;  // Initialize static member

Static member functions belong to the class itself, not to any particular object. They can only access static members. Call them using the class name: Counter::getTotalCounters(). Static members are useful for tracking class-wide data or providing utility functions related to the class.

int main() {
    Counter c1, c2, c3;
    
    c1.increment();
    c1.increment();
    c2.increment();
    
    cout << "c1: " << c1.getCount() << endl;  // 2
    cout << "c2: " << c2.getCount() << endl;  // 1
    cout << "c3: " << c3.getCount() << endl;  // 0
    
    // Static function called on class
    cout << "Total counters: " << Counter::getTotalCounters() << endl;  // 3
    
    return 0;
}

Each object has its own count, but totalCounters is shared across all objects. The static function getTotalCounters() is called using the class name and scope resolution operator, not on an object. This is how you track information across all instances.

class Calculator {
public:
    // Method chaining - return reference to *this
    Calculator& add(int value) {
        result += value;
        return *this;
    }
    
    Calculator& multiply(int value) {
        result *= value;
        return *this;
    }
    
    int getResult() const { return result; }
    
private:
    int result = 0;
};

Returning *this enables method chaining - calling multiple methods in sequence on the same object. The this pointer is an implicit parameter to all non-static member functions that points to the object the method was called on.

int main() {
    Calculator calc;
    
    // Method chaining - fluent interface
    calc.add(5).multiply(3).add(10);
    
    cout << "Result: " << calc.getResult() << endl;  // 25
    
    return 0;
}

Method chaining creates a fluent, readable interface. Each method returns the object itself, so you can immediately call another method on the result. This pattern is common in builder classes and configuration objects.

Practice Questions: Member Functions

Task: Create a Counter class with count value. Add increment(), decrement(), and getCount() const methods.

Show Solution
#include <iostream>
using namespace std;

class Counter {
private:
    int count;
    
public:
    Counter() : count(0) {}
    
    void increment() {
        count++;
    }
    
    void decrement() {
        if (count > 0) count--;
    }
    
    int getCount() const {
        return count;
    }
};

int main() {
    Counter c;
    c.increment();
    c.increment();
    c.increment();
    cout << "Count: " << c.getCount() << endl;  // 3
    
    c.decrement();
    cout << "Count: " << c.getCount() << endl;  // 2
    return 0;
}

Task: Create a Circle class with radius. Add setRadius() and const methods getRadius(), area(), and circumference().

Show Solution
#include <iostream>
using namespace std;

const double PI = 3.14159;

class Circle {
private:
    double radius;
    
public:
    Circle() : radius(0) {}
    
    void setRadius(double r) {
        if (r > 0) radius = r;
    }
    
    double getRadius() const {
        return radius;
    }
    
    double area() const {
        return PI * radius * radius;
    }
    
    double circumference() const {
        return 2 * PI * radius;
    }
};

int main() {
    Circle c;
    c.setRadius(5);
    
    cout << "Radius: " << c.getRadius() << endl;
    cout << "Area: " << c.area() << endl;
    cout << "Circumference: " << c.circumference() << endl;
    return 0;
}

Task: Create an Employee class with a static employeeCount. Add a constructor that increments count, and a static method getTotalEmployees().

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Employee {
private:
    string name;
    static int employeeCount;
    
public:
    Employee(string n) : name(n) {
        employeeCount++;
    }
    
    string getName() const {
        return name;
    }
    
    static int getTotalEmployees() {
        return employeeCount;
    }
};

int Employee::employeeCount = 0;

int main() {
    cout << "Employees: " << Employee::getTotalEmployees() << endl;  // 0
    
    Employee e1("Alice");
    Employee e2("Bob");
    Employee e3("Charlie");
    
    cout << "Employees: " << Employee::getTotalEmployees() << endl;  // 3
    return 0;
}

Task: Create a MyString class with a string. Add methods: length(), toUpper(), toLower(), and reverse() that return new strings.

Show Solution
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>
using namespace std;

class MyString {
private:
    string str;
    
public:
    MyString(string s) : str(s) {}
    
    int length() const {
        return str.length();
    }
    
    string toUpper() const {
        string result = str;
        for (char& c : result) c = toupper(c);
        return result;
    }
    
    string toLower() const {
        string result = str;
        for (char& c : result) c = tolower(c);
        return result;
    }
    
    string reverse() const {
        string result = str;
        std::reverse(result.begin(), result.end());
        return result;
    }
    
    string get() const { return str; }
};

int main() {
    MyString s("Hello World");
    
    cout << "Original: " << s.get() << endl;
    cout << "Length: " << s.length() << endl;
    cout << "Upper: " << s.toUpper() << endl;
    cout << "Lower: " << s.toLower() << endl;
    cout << "Reverse: " << s.reverse() << endl;
    return 0;
}

Task: Create a Calculator class with a result. Add chainable methods: add(), subtract(), multiply(), divide(), clear(), and getResult() const.

Show Solution
#include <iostream>
using namespace std;

class Calculator {
private:
    double result;
    
public:
    Calculator() : result(0) {}
    
    Calculator& add(double value) {
        result += value;
        return *this;
    }
    
    Calculator& subtract(double value) {
        result -= value;
        return *this;
    }
    
    Calculator& multiply(double value) {
        result *= value;
        return *this;
    }
    
    Calculator& divide(double value) {
        if (value != 0) result /= value;
        return *this;
    }
    
    Calculator& clear() {
        result = 0;
        return *this;
    }
    
    double getResult() const {
        return result;
    }
};

int main() {
    Calculator calc;
    
    // Method chaining: ((10 + 5) * 2 - 6) / 4 = 6
    calc.add(10).add(5).multiply(2).subtract(6).divide(4);
    
    cout << "Result: " << calc.getResult() << endl;  // 6
    
    calc.clear().add(100).divide(4);
    cout << "Result: " << calc.getResult() << endl;  // 25
    return 0;
}
07

Object Instantiation

Object instantiation is the process of creating actual objects from a class blueprint. C++ offers multiple ways to create objects - on the stack, on the heap, as arrays, or as pointers. Understanding these options helps you manage memory efficiently and choose the right approach for your needs.

Object Creation

Object Instantiation

Object instantiation is creating a concrete instance of a class. When you instantiate an object, memory is allocated for its data members and its constructor is called to initialize the object.

Two Main Ways: Stack allocation (automatic, scoped lifetime) vs Heap allocation (manual, dynamic lifetime with new/delete).

Stack vs Heap Allocation

class Player {
public:
    string name;
    int health;
    
    Player(string n, int h) : name(n), health(h) {
        cout << name << " created" << endl;
    }
    
    ~Player() {
        cout << name << " destroyed" << endl;
    }
};

This Player class has a constructor that announces creation and a destructor that announces destruction. This helps us visualize when objects are created and destroyed based on how we instantiate them.

// Stack allocation - automatic memory management
int main() {
    Player hero("Hero", 100);  // Created on stack
    cout << "Playing game..." << endl;
    return 0;
}  // hero automatically destroyed here

Stack-allocated objects are created when declared and automatically destroyed when they go out of scope. This is the simplest and safest way to create objects. The compiler handles all memory management for you, and there is no risk of memory leaks.

// Heap allocation - manual memory management
int main() {
    Player* villain = new Player("Villain", 80);  // Created on heap
    cout << "Playing game..." << endl;
    
    delete villain;  // Must manually destroy!
    return 0;
}

Heap-allocated objects use new to create and delete to destroy. The object persists until explicitly deleted, regardless of scope. This gives you control over object lifetime but requires careful memory management to avoid leaks.

Feature Stack Heap
Syntax Player p("A", 100); Player* p = new Player("A", 100);
Access Dot: p.name Arrow: p->name
Destruction Automatic (scope ends) Manual (delete p;)
Size Limit Limited (usually ~1MB) Large (available RAM)
Speed Faster Slower (allocation overhead)

Object Pointers

int main() {
    Player hero("Hero", 100);      // Stack object
    Player* ptr = &hero;           // Pointer to stack object
    
    // Access via pointer using arrow operator
    cout << ptr->name << endl;     // Hero
    cout << ptr->health << endl;   // 100
    
    // Equivalent to:
    cout << (*ptr).name << endl;   // Hero (dereference then dot)
    
    return 0;
}

You can create pointers to stack objects using the address-of operator (&). Access members through pointers using the arrow operator (->), which is shorthand for dereferencing and then using dot ((*ptr).member).

// Dynamic object with pointer
int main() {
    Player* enemy = new Player("Enemy", 50);
    
    enemy->health -= 10;           // Reduce health
    cout << enemy->name << " health: " << enemy->health << endl;
    
    delete enemy;                  // Free memory
    enemy = nullptr;               // Good practice: avoid dangling pointer
    
    return 0;
}

After delete, set the pointer to nullptr to avoid dangling pointers. A dangling pointer points to freed memory and using it causes undefined behavior. Setting to nullptr makes it safe to check before use.

Arrays of Objects

class Enemy {
public:
    string type;
    int damage;
    
    Enemy() : type("Goblin"), damage(10) {}  // Default constructor required!
    Enemy(string t, int d) : type(t), damage(d) {}
};

When creating arrays of objects, a default constructor is required because the array elements must be default-initialized first. If you only have parameterized constructors, you cannot create arrays using simple array syntax.

// Stack array of objects
int main() {
    Enemy enemies[3];  // Creates 3 Enemy objects using default constructor
    
    enemies[0].type = "Orc";
    enemies[0].damage = 25;
    
    for (int i = 0; i < 3; i++) {
        cout << enemies[i].type << " deals " << enemies[i].damage << " damage" << endl;
    }
    return 0;
}

Stack arrays of objects are automatically created and destroyed. Each element is initialized with the default constructor. You can then modify individual objects as needed. The entire array is destroyed when it goes out of scope.

// Heap array of objects
int main() {
    int size = 5;
    Enemy* horde = new Enemy[size];  // Dynamic array on heap
    
    horde[0] = Enemy("Dragon", 100);
    horde[1] = Enemy("Troll", 50);
    
    for (int i = 0; i < size; i++) {
        cout << horde[i].type << ": " << horde[i].damage << endl;
    }
    
    delete[] horde;  // Use delete[] for arrays!
    return 0;
}

For heap arrays, use new Type[size] to allocate and delete[] to deallocate. Using delete instead of delete[] for arrays causes undefined behavior - it only destroys the first element. Always match new[] with delete[].

Passing Objects to Functions

// Pass by value - creates a copy
void levelUp(Player p) {
    p.health += 50;  // Modifies the copy only
    cout << "Inside function: " << p.health << endl;
}

Passing by value creates a copy of the object. Changes inside the function do not affect the original. This is safe but can be slow for large objects because the entire object must be copied.

// Pass by reference - no copy, can modify
void heal(Player& p) {
    p.health += 25;  // Modifies the original!
}

// Pass by const reference - no copy, read-only
void display(const Player& p) {
    cout << p.name << ": " << p.health << " HP" << endl;
    // p.health = 0;  // ERROR! Cannot modify const reference
}

Pass by reference avoids copying and allows modification. Pass by const reference avoids copying but prevents modification - best for read-only access to large objects. This is the preferred way to pass objects you do not need to modify.

// Pass by pointer
void damage(Player* p, int amount) {
    if (p != nullptr) {  // Always check for null!
        p->health -= amount;
    }
}

int main() {
    Player hero("Hero", 100);
    
    levelUp(hero);             // By value: hero unchanged
    cout << "After levelUp: " << hero.health << endl;  // 100
    
    heal(hero);                // By reference: hero modified
    cout << "After heal: " << hero.health << endl;     // 125
    
    display(hero);             // Const ref: read-only
    
    damage(&hero, 30);         // By pointer: hero modified
    cout << "After damage: " << hero.health << endl;   // 95
    
    return 0;
}

Pass by pointer is similar to pass by reference but requires explicit address-taking (&hero) and can be null. Choose the method based on your needs: value for small types or when a copy is needed, const reference for read-only access, reference or pointer when modification is required.

Modern C++ Tip: Prefer smart pointers (std::unique_ptr, std::shared_ptr) over raw pointers for heap allocation. They automatically manage memory and prevent leaks.

Practice Questions: Object Instantiation

Task: Create a Book class with title and pages. Create one object on the stack and one on the heap. Print both and properly clean up.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Book {
public:
    string title;
    int pages;
    
    Book(string t, int p) : title(t), pages(p) {}
    
    void display() {
        cout << title << " (" << pages << " pages)" << endl;
    }
};

int main() {
    // Stack object
    Book stackBook("C++ Primer", 976);
    stackBook.display();
    
    // Heap object
    Book* heapBook = new Book("Clean Code", 464);
    heapBook->display();
    
    delete heapBook;  // Clean up heap object
    return 0;
}

Task: Create a Student class with default constructor. Create an array of 3 students on the stack, set their names, and print all.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Student {
public:
    string name;
    int grade;
    
    Student() : name("Unknown"), grade(0) {}  // Default constructor required
};

int main() {
    Student students[3];  // Array on stack
    
    students[0].name = "Alice";
    students[0].grade = 95;
    students[1].name = "Bob";
    students[1].grade = 87;
    students[2].name = "Charlie";
    students[2].grade = 92;
    
    for (int i = 0; i < 3; i++) {
        cout << students[i].name << ": " << students[i].grade << endl;
    }
    return 0;
}

Task: Create a Counter class with value. Write three functions: incrementByValue(), incrementByRef(), and incrementByPtr(). Test and show which ones modify the original.

Show Solution
#include <iostream>
using namespace std;

class Counter {
public:
    int value;
    Counter(int v) : value(v) {}
};

void incrementByValue(Counter c) {
    c.value++;  // Modifies copy only
}

void incrementByRef(Counter& c) {
    c.value++;  // Modifies original
}

void incrementByPtr(Counter* c) {
    c->value++;  // Modifies original
}

int main() {
    Counter counter(10);
    
    incrementByValue(counter);
    cout << "After byValue: " << counter.value << endl;  // 10
    
    incrementByRef(counter);
    cout << "After byRef: " << counter.value << endl;    // 11
    
    incrementByPtr(&counter);
    cout << "After byPtr: " << counter.value << endl;    // 12
    
    return 0;
}

Task: Create a Product class. Dynamically allocate an array of 5 products on the heap, initialize them, display all, and properly deallocate.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Product {
public:
    string name;
    double price;
    
    Product() : name("Unknown"), price(0) {}
    Product(string n, double p) : name(n), price(p) {}
};

int main() {
    int size = 5;
    Product* products = new Product[size];  // Heap array
    
    products[0] = Product("Laptop", 999.99);
    products[1] = Product("Phone", 699.99);
    products[2] = Product("Tablet", 499.99);
    products[3] = Product("Watch", 299.99);
    products[4] = Product("Earbuds", 149.99);
    
    cout << "Product List:" << endl;
    for (int i = 0; i < size; i++) {
        cout << products[i].name << ": $" << products[i].price << endl;
    }
    
    delete[] products;  // Use delete[] for arrays!
    return 0;
}

Task: Create a Team class that holds a dynamic array of Player pointers. Implement addPlayer(), displayAll(), and proper destructor to clean up all players.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Player {
public:
    string name;
    int score;
    
    Player(string n, int s) : name(n), score(s) {}
};

class Team {
private:
    Player** players;  // Array of Player pointers
    int capacity;
    int count;
    
public:
    Team(int cap) : capacity(cap), count(0) {
        players = new Player*[capacity];
    }
    
    ~Team() {
        for (int i = 0; i < count; i++) {
            delete players[i];  // Delete each Player
        }
        delete[] players;       // Delete the array
        cout << "Team cleaned up" << endl;
    }
    
    bool addPlayer(string name, int score) {
        if (count < capacity) {
            players[count++] = new Player(name, score);
            return true;
        }
        return false;
    }
    
    void displayAll() {
        cout << "Team Roster:" << endl;
        for (int i = 0; i < count; i++) {
            cout << players[i]->name << ": " << players[i]->score << endl;
        }
    }
};

int main() {
    Team team(5);
    team.addPlayer("Alice", 100);
    team.addPlayer("Bob", 85);
    team.addPlayer("Charlie", 92);
    
    team.displayAll();
    return 0;
}  // Destructor cleans up all memory

Key Takeaways

Classes are Blueprints

A class defines what data and behavior objects will have. Objects are instances created from classes.

Encapsulation Protects Data

Use private members with public getters/setters to control access and validate data changes.

Constructors Initialize

Constructors set up objects when created. Use initializer lists for efficient member initialization.

Destructors Clean Up

Destructors release resources when objects are destroyed. Essential for dynamic memory and RAII.

Const for Read-Only

Mark member functions as const when they do not modify the object. This enables use with const objects.

Static for Class-Wide

Static members belong to the class, not objects. Use for shared data or utility functions.

Knowledge Check

Quick Quiz

Test what you've learned about C++ Classes and Objects

1 What is the default access modifier for class members in C++?
2 Which of the following correctly defines a constructor for class Car?
3 What does the const keyword mean when placed after a member function declaration?
4 When is a destructor called?
5 What is the correct syntax for a destructor of class Widget?
6 What does this pointer refer to inside a member function?
Answer all questions to check your score