Skip to main content

📝 Lesson 9: Classes and Objects

Building your own types with classes, objects, constructors, destructors, and encapsulation — the foundation of object-oriented C++.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what object-oriented programming is and why classes organize related data and behavior together.
  • Define a class with private data members and a public interface, and instantiate objects from it.
  • Write constructors (default, parameterized, copy) and a destructor, and explain when each runs.
  • Apply encapsulation with access specifiers, and use the this pointer to disambiguate members.
  • Build objects out of other objects with composition, and share state across objects with static members.
  • Overload operators and grant friend access so custom types behave like built-in ones.

Estimated Time: 90–120 minutes

Project: Design a small banking system (accounts, transactions, a customer with multiple accounts) using classes, constructors/destructors, and static bank-wide statistics.

In This Lesson

What is Object-Oriented Programming?

Imagine you're organizing a zoo. Instead of tracking "animal name 1", "animal age 1", "animal type 1", etc., you create an "Animal" blueprint that combines all related information and behaviors. That's OOP - organizing code around objects that represent real things!

Classes: Blueprints for Objects

A class is like an architect's blueprint - it defines what something should look like and what it can do. An object is the actual house built from that blueprint!

graph TD A[Class: Blueprint] --> B[Defines Structure] A --> C[Defines Behavior] B --> D["Properties/Attributes"] C --> E["Methods/Functions"] A --> F[Object: Instance] F --> G[Actual Data] F --> H[Can Use Methods] style A fill:#E3F2FD,color:#111827 style F fill:#FFE4B5,color:#111827

Creating Your First Class

Anatomy of a Class class Rectangle private: double width; double height; public: Rectangle(double w, double h); double getArea(); double getPerimeter(); void setDimensions(double w, double h); Hidden Visible

Complete Rectangle Class Example

class Rectangle {
private:
    double width;
    double height;

public:
    // Constructor - called when object is created
    Rectangle(double w, double h) {
        width = w;
        height = h;
    }

    // Default constructor
    Rectangle() {
        width = 0;
        height = 0;
    }

    // Methods (member functions) - const: they only read, never modify
    double getArea() const {
        return width * height;
    }

    double getPerimeter() const {
        return 2 * (width + height);
    }

    // Setters - modify private data
    void setWidth(double w) {
        if (w > 0) {
            width = w;
        }
    }

    void setHeight(double h) {
        if (h > 0) {
            height = h;
        }
    }

    // Getters - access private data
    double getWidth() const {
        return width;
    }

    double getHeight() const {
        return height;
    }
};

// Using the class
int main() {
    Rectangle rect1(5, 3);           // Create object with constructor
    Rectangle rect2;                 // Create with default constructor

    cout << "Area: " << rect1.getArea() << endl;           // 15
    cout << "Perimeter: " << rect1.getPerimeter() << endl; // 16

    rect2.setWidth(10);
    rect2.setHeight(4);
    cout << "Rect2 area: " << rect2.getArea() << endl;     // 40

    return 0;
}

Encapsulation: Protecting Your Data

Encapsulation is like a car - you don't need to understand the engine to drive. You just use the steering wheel and pedals (public interface) while the complex mechanics (private data) are hidden!

Constructors and Destructors

Constructors are like a welcome party when an object is born, and destructors are the cleanup crew when it's time to go!

graph LR A[Object Lifecycle] --> B[Constructor Called] B --> C[Object Lives] C --> D[Destructor Called] B --> E[Initialize Data] B --> F[Allocate Resources] D --> G[Clean Up] D --> H[Free Resources] style B fill:#4CAF50,color:#111827 style D fill:#F44336,color:#111827

Constructor and Destructor Examples

class Student {
private:
    string name;
    int* grades;
    int numGrades;

public:
    // Default constructor
    Student() {
        cout << "Default constructor called!" << endl;
        name = "Unknown";
        numGrades = 0;
        grades = nullptr;
    }

    // Parameterized constructor
    Student(string n, int num) {
        cout << "Parameterized constructor for " << n << endl;
        name = n;
        numGrades = num;
        grades = new int[numGrades];  // Dynamic allocation

        // Initialize grades to 0
        for (int i = 0; i < numGrades; i++) {
            grades[i] = 0;
        }
    }

    // Copy constructor (important for dynamic memory!)
    Student(const Student& other) {
        cout << "Copy constructor called!" << endl;
        name = other.name;
        numGrades = other.numGrades;

        // Deep copy of array
        grades = new int[numGrades];
        for (int i = 0; i < numGrades; i++) {
            grades[i] = other.grades[i];
        }
    }

    // Copy assignment operator (Rule of 3: if you write one of
    // destructor / copy constructor / copy assignment, write all three)
    Student& operator=(const Student& other) {
        if (this == &other) return *this;  // Guard against self-assignment
        delete[] grades;                   // Release what we already own

        name = other.name;
        numGrades = other.numGrades;

        // Deep copy of array
        grades = new int[numGrades];
        for (int i = 0; i < numGrades; i++) {
            grades[i] = other.grades[i];
        }
        return *this;
    }

    // Destructor - cleanup!
    ~Student() {
        cout << "Destructor for " << name << endl;
        delete[] grades;  // Free dynamic memory
    }

    void setGrade(int index, int grade) {
        if (index >= 0 && index < numGrades) {
            grades[index] = grade;
        }
    }

    double getAverage() const {
        if (numGrades == 0) return 0;

        int sum = 0;
        for (int i = 0; i < numGrades; i++) {
            sum += grades[i];
        }
        return static_cast<double>(sum) / numGrades;
    }
};

The this Pointer

The 'this' pointer is like saying "myself" - it's how an object refers to itself!

Class Composition: Objects Within Objects

Composition is like building with LEGO blocks - you create complex objects by combining simpler ones!

Class Composition Example Date month: int day: int year: int Time hour: int minute: int second: int Event name: string location: string date: Date time: Time Event "has a" Date and Time

Composition Example

class Engine {
private:
    int horsepower;
    double displacement;

public:
    Engine(int hp, double disp) : horsepower(hp), displacement(disp) {}

    void start() {
        cout << "Engine starting... Vroom!" << endl;
    }

    int getHorsepower() const { return horsepower; }
};

class Wheel {
private:
    double diameter = 0.0;
    string type;

public:
    Wheel() = default;  // Needed so an array of Wheels can be default-constructed
    Wheel(double d, string t) : diameter(d), type(t) {}

    void rotate() {
        cout << "Wheel rotating..." << endl;
    }
};

class Car {
private:
    string make;
    string model;
    Engine engine;        // Composition: Car "has an" Engine
    Wheel wheels[4];      // Composition: Car "has" Wheels

public:
    // Constructor initialization list
    Car(string mk, string md, int hp)
        : make(mk), model(md), engine(hp, 2.0) {
        // Initialize all wheels
        for (int i = 0; i < 4; i++) {
            wheels[i] = Wheel(17.0, "All-Season");
        }
    }

    void start() {
        cout << "Starting " << make << " " << model << endl;
        engine.start();
        cout << "Ready to drive!" << endl;
    }

    void drive() {
        cout << "Driving..." << endl;
        for (int i = 0; i < 4; i++) {
            wheels[i].rotate();
        }
    }
};

Static Members: Shared Among All Objects

Static members are like a shared bulletin board in an office - all employees (objects) can see and use the same information!

Static Members Example

class BankAccount {
private:
    string accountNumber;
    double balance;
    static int totalAccounts;      // Shared by all accounts
    static double totalBalance;    // Track total money in bank

public:
    BankAccount(string accNum, double initialBalance) {
        accountNumber = accNum;
        balance = initialBalance;
        totalAccounts++;           // Increment shared counter
        totalBalance += balance;   // Add to total
    }

    ~BankAccount() {
        totalAccounts--;           // Decrement when destroyed
        totalBalance -= balance;
    }

    void deposit(double amount) {
        balance += amount;
        totalBalance += amount;    // Update total
    }

    void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            totalBalance -= amount;
        }
    }

    // Static method - can be called without object
    static int getTotalAccounts() {
        return totalAccounts;
    }

    static double getTotalBalance() {
        return totalBalance;
    }
};

// Initialize static members outside class
int BankAccount::totalAccounts = 0;
double BankAccount::totalBalance = 0;

// Usage
int main() {
    cout << "Accounts: " << BankAccount::getTotalAccounts() << endl; // 0

    BankAccount acc1("123", 1000);
    BankAccount acc2("456", 2000);

    cout << "Accounts: " << BankAccount::getTotalAccounts() << endl; // 2
    cout << "Total balance: $" << BankAccount::getTotalBalance() << endl; // 3000

    return 0;
}

Operator Overloading: Making Objects Natural

Operator overloading lets your objects work with operators like +, -, ==, just like built-in types!

🔭 A first taste — full treatment in Lesson 13

This is a preview so the Vector2D example below reads naturally. Operator overloading gets its own dedicated lesson — Lesson 13: Operator Overloading — where you'll learn the full rules (member vs. friend functions, return types, and which operators to overload versus leave alone). For now, just notice that a class can teach the compiler what + or == means for its own type.

graph TD A[Operator Overloading] --> B[Binary Operators] A --> C[Unary Operators] A --> D[Comparison Operators] A --> E[Stream Operators] B --> F["+ - * /"] C --> G["++ -- -"] D --> H["== != < >"] E --> I["<< >>"]
class Vector2D {
private:
    double x, y;

public:
    Vector2D(double x = 0, double y = 0) : x(x), y(y) {}

    // Addition operator
    Vector2D operator+(const Vector2D& other) const {
        return Vector2D(x + other.x, y + other.y);
    }

    // Subtraction operator
    Vector2D operator-(const Vector2D& other) const {
        return Vector2D(x - other.x, y - other.y);
    }

    // Scalar multiplication
    Vector2D operator*(double scalar) const {
        return Vector2D(x * scalar, y * scalar);
    }

    // Equality operator
    bool operator==(const Vector2D& other) const {
        return (x == other.x && y == other.y);
    }

    // Stream insertion operator (friend function)
    friend ostream& operator<<(ostream& out, const Vector2D& v) {
        out << "(" << v.x << ", " << v.y << ")";
        return out;
    }

    double magnitude() const {
        return sqrt(x * x + y * y);
    }
};

// Usage
int main() {
    Vector2D v1(3, 4);
    Vector2D v2(1, 2);

    Vector2D v3 = v1 + v2;      // (4, 6)
    Vector2D v4 = v1 - v2;      // (2, 2)
    Vector2D v5 = v1 * 2;       // (6, 8)

    cout << "v1 = " << v1 << endl;
    cout << "v3 = " << v3 << endl;
    cout << "Magnitude of v1: " << v1.magnitude() << endl; // 5

    return 0;
}

Practice Exercise: Bank System

🏋️ Build a Banking System

Create a complete banking system with the following features:

  1. Account class with deposit, withdraw, transfer
  2. Customer class that can have multiple accounts
  3. Transaction history
  4. Static member to track total bank assets
class Transaction {
private:
    string type;  // "Deposit", "Withdrawal", "Transfer"
    double amount;
    string date;

public:
    // TODO: Constructor and display method
};

class Account {
private:
    string accountNumber;
    double balance;
    vector<Transaction> history;

public:
    // TODO: Constructor, deposit, withdraw, getBalance
    // TODO: addTransaction, showHistory
};

class Customer {
private:
    string name;
    string customerId;
    vector<Account*> accounts;

public:
    // TODO: Constructor, addAccount, getTotalBalance
    // TODO: transfer between accounts
};
💡 Hint
  • Use dynamic allocation for accounts
  • Implement proper copy constructor for Customer
  • Add validation for withdrawals
  • Use static members for bank-wide statistics

Friend Functions and Classes

Friends are like trusted individuals who have keys to your house - they can access private members!

Friend Access Class A Private Data 🔒 Locked Friend Function Has Access! 🔑

Challenge Exercise: Game Development

🏋️ Create a Simple Game Framework

Design classes for a simple 2D game:

  1. GameObject base class with position and velocity
  2. Player class with health and score
  3. Enemy class with AI behavior
  4. Collision detection between objects
  5. Game class to manage all objects
✅ Solution
class GameObject {
protected:
    double x, y;          // Position
    double vx, vy;        // Velocity
    double width, height; // Size

public:
    virtual ~GameObject() = default;  // Virtual: safe delete through a base pointer
    virtual void update(double deltaTime);
    virtual void render();
    bool collidesWith(const GameObject& other) const;
};

class Player : public GameObject {
private:
    int health;
    int score;

public:
    void handleInput();
    void takeDamage(int damage);
};

🎯 Quick Quiz

Question 1: In the Student class example, why must Student have a custom copy constructor that performs a deep copy of grades?

Question 2: In the BankAccount example, what does declaring totalAccounts as static mean?

Question 3: In the Vector2D example, why is operator<< implemented as a friend function instead of a member function?

Summary

🎉 Key Takeaways

  • Classes are blueprints for creating objects
  • Encapsulation hides implementation details behind a public interface
  • Constructors initialize objects, destructors clean up
  • The copy constructor must deep-copy any owned pointers, or two objects end up sharing (and double-freeing) the same memory
  • The this pointer refers to the current object
  • Composition builds complex objects from simpler ones
  • Static members are shared among all objects of a class
  • Operator overloading makes custom types feel natural
  • Friend functions can access private members when needed
graph LR A[Master OOP] --> B[Design Better Code] B --> C[Create Reusable Components] C --> D[Build Complex Systems] D --> E[Professional Software Engineer!]

📚 Additional Resources

🚀 What's Next?

You can now design your own types from scratch — bundling data with the behavior that operates on it. Next, in Lesson 10: Smart Pointers, you'll let those objects manage their own memory automatically — using unique_ptr, shared_ptr, and weak_ptr so ownership is handled for you through RAII instead of manual new/delete.

🎉 You're thinking in objects now!

Classes are the turning point where C++ stops being "just functions and data" and starts being real software design. Nice work building your first ones.