Chapter 8 of 23

Object-Oriented Programming — Classes and Objects

Learn the class keyword, access specifiers, member functions, the this pointer, getters and setters, static members, and friend functions — with a BankAccount worked example.

Meritshot10 min read
C++OOPClassesObjectsEncapsulationStatic Members
All C++ Chapters

Why Object-Oriented Programming?

Before OOP, large programs were written as long sequences of procedures — fine for small scripts, but unmanageable as software grew. Object-Oriented Programming (OOP) organises code around objects: self-contained units that bundle related data and behaviour together.

OOP is the dominant paradigm in the industry. Companies like TCS, Infosys, Wipro, and virtually every product company interviewing in India (Google, Amazon, Microsoft, Flipkart) expect candidates to understand OOP deeply. C++ supports OOP natively and adds the performance characteristics that Java and Python cannot match.

The four pillars of OOP are:

PillarMeaning
EncapsulationBundling data and methods; hiding internal state
AbstractionExposing only what is necessary
InheritanceDeriving new classes from existing ones
PolymorphismOne interface, many implementations

This chapter focuses on encapsulation — the foundation on which the other three rest.


The class Keyword

A class is a blueprint. An object is a concrete instance created from that blueprint. The syntax mirrors a struct, but with one key default difference: members are private by default in a class (public by default in a struct).

class Student {
    // members go here
};

Objects are created like any other variable:

Student s1;           // default-constructed object
Student s2, s3;       // two more objects

Access Specifiers

Access specifiers control which parts of the program can read or modify a class member.

SpecifierWho Can Access
publicAny code anywhere
privateOnly member functions and friend functions of this class
protectedMember functions, friend functions, and derived classes
class Employee {
public:
    string name;         // accessible from anywhere

private:
    double salary;       // only accessible inside the class

protected:
    int employeeId;      // accessible in derived classes too
};

The golden rule: data members should almost always be private; public exposure is provided through methods.


Member Variables and Methods

A class combines member variables (data) and member functions (methods) in one unit.

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

class Rectangle {
private:
    double width;
    double height;

public:
    // Method to set dimensions
    void setDimensions(double w, double h) {
        width  = w;
        height = h;
    }

    double area() const {
        return width * height;
    }

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

    void display() const {
        cout << "Width: "    << width
             << ", Height: " << height
             << ", Area: "   << area() << "\n";
    }
};

int main() {
    Rectangle room;
    room.setDimensions(5.0, 3.5);
    room.display();        // Width: 5, Height: 3.5, Area: 17.5
    return 0;
}

The const after a method signature means the method does not modify any member variables — always mark read-only methods as const.


The this Pointer

Inside every non-static member function, the compiler implicitly passes a pointer called this that points to the current object. You rarely need to use this explicitly, but it is essential in two scenarios:

  1. Resolving name ambiguity when a parameter has the same name as a member variable.
  2. Returning the current object from a method (method chaining).
class Circle {
private:
    double radius;

public:
    void setRadius(double radius) {
        // 'radius' (parameter) shadows the member
        this->radius = radius;   // this->radius is the member
    }

    // Method chaining: return *this
    Circle& scale(double factor) {
        radius *= factor;
        return *this;
    }

    double area() const {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Circle c;
    c.setRadius(5.0);
    cout << c.scale(2.0).area() << "\n";  // area of circle with r=10
    return 0;
}

Getters and Setters

Keeping data private and exposing it via getter (accessor) and setter (mutator) methods is the standard encapsulation pattern. It lets you add validation, logging, or computation without changing how external code uses the class.

class Temperature {
private:
    double celsius;

public:
    // Setter with validation
    void setCelsius(double c) {
        if (c < -273.15) {
            cout << "Temperature below absolute zero!\n";
            return;
        }
        celsius = c;
    }

    // Getter
    double getCelsius() const {
        return celsius;
    }

    // Derived getter — no extra storage needed
    double getFahrenheit() const {
        return (celsius * 9.0 / 5.0) + 32.0;
    }
};

Static Members

A static member variable is shared by all instances of the class — there is exactly one copy regardless of how many objects exist. A static member function can be called without any object and can only access other static members.

#include <iostream>
using namespace std;

class Employee {
private:
    string name;
    static int count;   // ONE shared copy across all Employee objects

public:
    Employee(string n) : name(n) {
        count++;
    }

    static int getCount() {
        return count;
    }

    string getName() const { return name; }
};

// Static member must be defined outside the class
int Employee::count = 0;

int main() {
    cout << "Employees: " << Employee::getCount() << "\n";  // 0

    Employee e1("Anjali");
    Employee e2("Ravi");
    Employee e3("Preethi");

    cout << "Employees: " << Employee::getCount() << "\n";  // 3
    return 0;
}

Static members are useful for counters, shared configuration, or factory methods.


Friend Functions

A friend function is a non-member function that is granted access to the private and protected members of a class. Declare it inside the class with the friend keyword.

#include <iostream>
using namespace std;

class Wallet {
private:
    double balance;

public:
    Wallet(double b) : balance(b) {}

    // Grant friendship to a free function
    friend void transfer(Wallet& from, Wallet& to, double amount);
    friend void printBalance(const Wallet& w);
};

void transfer(Wallet& from, Wallet& to, double amount) {
    if (from.balance >= amount) {
        from.balance -= amount;
        to.balance   += amount;
    } else {
        cout << "Insufficient funds\n";
    }
}

void printBalance(const Wallet& w) {
    cout << "Balance: Rs. " << w.balance << "\n";
}

int main() {
    Wallet alice(5000.0), bob(1000.0);
    transfer(alice, bob, 2000.0);
    printBalance(alice);   // Balance: Rs. 3000
    printBalance(bob);     // Balance: Rs. 3000
    return 0;
}

Use friend functions sparingly — they weaken encapsulation. They are justified when two classes need tightly coupled access, such as operator overloading.


Worked Example: BankAccount Class

Indian banks like SBI, HDFC, ICICI, and Kotak all implement accounts that share a common set of operations: deposit, withdrawal, and balance enquiry. Let us model a simple bank account with proper encapsulation.

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

class BankAccount {
private:
    string ownerName;
    string accountNumber;
    double balance;
    static double interestRate;     // shared by all accounts

public:
    // Constructor
    BankAccount(string name, string accNo, double initialDeposit) {
        ownerName     = name;
        accountNumber = accNo;
        if (initialDeposit < 0) {
            cout << "Invalid initial deposit. Setting to 0.\n";
            balance = 0.0;
        } else {
            balance = initialDeposit;
        }
        cout << "Account created for " << ownerName << "\n";
    }

    // Deposit money
    void deposit(double amount) {
        if (amount <= 0) {
            cout << "Deposit amount must be positive.\n";
            return;
        }
        balance += amount;
        cout << "Deposited Rs. " << amount
             << ". New balance: Rs. " << balance << "\n";
    }

    // Withdraw money
    bool withdraw(double amount) {
        if (amount <= 0) {
            cout << "Withdrawal amount must be positive.\n";
            return false;
        }
        if (amount > balance) {
            cout << "Insufficient funds. Balance: Rs. " << balance << "\n";
            return false;
        }
        balance -= amount;
        cout << "Withdrawn Rs. " << amount
             << ". Remaining balance: Rs. " << balance << "\n";
        return true;
    }

    // Apply annual interest
    void applyInterest() {
        double interest = balance * interestRate / 100.0;
        balance += interest;
        cout << "Interest of Rs. " << interest
             << " applied. New balance: Rs. " << balance << "\n";
    }

    // Getters
    double      getBalance()       const { return balance; }
    string      getOwnerName()     const { return ownerName; }
    string      getAccountNumber() const { return accountNumber; }

    // Static getter/setter for interest rate
    static double getInterestRate()          { return interestRate; }
    static void   setInterestRate(double r)  { interestRate = r; }

    // Display account summary
    void displaySummary() const {
        cout << "\n--- Account Summary ---\n";
        cout << "Owner:   " << ownerName     << "\n";
        cout << "Acc No:  " << accountNumber << "\n";
        cout << "Balance: Rs. " << balance   << "\n";
        cout << "Rate:    " << interestRate  << "% p.a.\n";
        cout << "-----------------------\n";
    }
};

// Define static member
double BankAccount::interestRate = 6.5;

int main() {
    BankAccount acc1("Meera Nair", "SBI00123456", 50000.0);
    BankAccount acc2("Rahul Gupta", "SBI00789012", 25000.0);

    acc1.deposit(15000.0);
    acc1.withdraw(8000.0);
    acc1.withdraw(100000.0);   // should fail

    acc2.deposit(5000.0);

    // Change interest rate for all accounts
    BankAccount::setInterestRate(7.0);

    acc1.applyInterest();
    acc2.applyInterest();

    acc1.displaySummary();
    acc2.displaySummary();

    return 0;
}

Sample Output:

Account created for Meera Nair
Account created for Rahul Gupta
Deposited Rs. 15000. New balance: Rs. 65000
Withdrawn Rs. 8000. Remaining balance: Rs. 57000
Insufficient funds. Balance: Rs. 57000
Deposited Rs. 5000. New balance: Rs. 30000
Interest of Rs. 3990 applied. New balance: Rs. 60990
Interest of Rs. 2100 applied. New balance: Rs. 32100

--- Account Summary ---
Owner:   Meera Nair
Acc No:  SBI00123456
Balance: Rs. 60990
Rate:    7% p.a.
-----------------------

--- Account Summary ---
Owner:   Rahul Gupta
Acc No:  SBI00789012
Balance: Rs. 32100
Rate:    7% p.a.
-----------------------

Common Pitfalls

  • Forgetting to initialise member variables: Uninitialised numeric members hold garbage values. Always initialise in the constructor (or with a default member initialiser).
  • Making everything public: This defeats encapsulation. If external code can freely modify balance, your validation logic in deposit and withdraw is useless.
  • Forgetting to define static member variables: Declaring static int count; inside the class is just a declaration. You must define it (int Employee::count = 0;) in exactly one .cpp file or you will get a linker error.
  • Using this when it is unnecessary: Overusing this-> for every member access makes code noisy. Reserve it for disambiguation.
  • Non-const methods on const objects: If you have a const BankAccount, you can only call methods marked const. Forgetting to mark read-only methods as const breaks this.
  • Friend functions breaking encapsulation: Grant friendship only when truly needed (e.g., operator overloading). Prefer public methods wherever possible.

Practice Exercises

  1. Create a Student class with private members name, rollNumber, and an array of 5 marks. Add methods to set marks, compute the average, and print a report card.
  2. Extend the BankAccount class to support a transaction history — store the last 10 transactions as a string array and add a printHistory() method.
  3. Create a Counter class with a private static int count. Every time a Counter object is constructed, increment the count; every time one is destroyed, decrement it. Print the live count.
  4. Write a Matrix class for 2×2 matrices with friend functions for addition and multiplication of two Matrix objects.
  5. Create a Circle class and a Rectangle class, both with a getArea() method. Write a compareBigger friend function that takes one of each and prints which has the larger area.

Summary

  • A class is a user-defined blueprint; an object is a concrete instance of that blueprint.
  • Access specifierspublic, private, protected — control visibility. Data members should generally be private.
  • Member functions (methods) define the behaviour of a class. Mark read-only methods as const.
  • The this pointer refers to the current object inside a member function; use it to resolve name ambiguity or enable method chaining.
  • Getters and setters expose private data safely and allow validation logic to be centralised.
  • Static members are shared across all objects of a class; static functions can be called without any object.
  • Friend functions can access private members but should be used sparingly to preserve encapsulation.
  • The BankAccount example tied all these concepts together in a realistic Indian banking scenario.