Lesson 1 — Objects, Classes, and Encapsulation

← Module home · Lesson 2 — Constructors, destructors, lifetime →

In this lesson you will learn


1. Why OOP — the two problems you have already felt

Problem 1: data and functions live apart. In every project since the Functions module, you maintained a struct (or parallel arrays) and a set of functions that operate on it — printStudent(const StudentRecord&), isValidMark(int), findByName(...). The pairing is enforced only by discipline: any function anywhere can read or write any field. When a bug corrupts a mark to −40, the search space is the whole program.

Problem 2: nothing stops invalid data. The Records module taught you validation — but a free function like setMark(StudentRecord&, int) must be called to work. Nothing stops a careless line elsewhere from doing student.mark = -40; directly.

OOP answers both with one move: put the data and the functions inside one package, and let the package enforce its own rules. The functions that belong to the data become methods; the data becomes attributes hidden behind them; the language itself now enforces what discipline used to. The bug search space shrinks from “the whole program” to “the class” — the Debugging module’s modularity dividend, now with a wall the compiler polices.

The honest framing for this course: OOP is not a new kind of computing. It is your existing toolbox — structs, functions, guard chains — rearranged so the compiler enforces the organization you were already doing by hand.


2. Class and object — blueprint and building

        CLASS  =  the blueprint (written once)          OBJECTS  =  the buildings
   ┌───────────────────────────────┐              ┌─────────────┐  ┌─────────────┐
   │ class BankAccount {           │              │ acc1        │  │ acc2        │
   │   string owner;               │   build →    │ owner:"Sana"│  │ owner:"Omar"│
   │   long long balance = 0;      │              │ balance:500 │  │ balance:1200│
   │   void deposit(long long a);  │              └─────────────┘  └─────────────┘
   │ };                            │                   each object owns its OWN data
   └───────────────────────────────┘                   and shares the SAME methods

A class is a type you define: it names the attributes every instance will have and the methods every instance can perform. An object (instance) is one concrete realization — acc1 and acc2 each hold their own owner and balance, but deposit is written once. Exactly the struct/array relationship you know: StudentRecord was the blueprint; roster[3] was a built thing. A class is a struct that also carries its functions.


3. Attributes and methods — the first class, fully explained

// bankaccount.cpp — Programming Fundamentals Using C++
// Unit 15 · Lesson 1 · The first class
// Compile: g++ -std=c++17 -Wall -Wextra bankaccount.cpp -o bankaccount

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

class BankAccount {
public:                              // the interface: what the outside world may do
    void deposit(long long amount) {
        if (amount <= 0) return;     // the object defends itself
        balance += amount;
    }
    bool withdraw(long long amount) {
        if (amount <= 0 || amount > balance) return false;
        balance -= amount;
        return true;
    }
    long long getBalance() const { return balance; }
    string getOwner() const { return owner; }
    void setOwner(const string& name) {
        if (!name.empty()) owner = name;      // validation lives here now
    }

private:                             // the interior: only methods may touch this
    string owner;
    long long balance = 0;
};

int main() {
    BankAccount acc1;                       // an object exists
    acc1.setOwner("Sana");
    acc1.deposit(500);
    acc1.withdraw(120);
    cout << acc1.getOwner() << ": " << acc1.getBalance() << "\n";  // Sana: 380
    return 0;
}

Explanation, line group by line group:

Test cases for the class (the Debugging module’s families, applied)

Case Call Expected
normal deposit deposit(500) then getBalance() 500
invalid deposit deposit(-50) then getBalance() unchanged (0)
valid withdrawal deposit 500, withdraw(120) true, balance 380
overdraft attempt deposit 100, withdraw(500) false, balance still 100
empty-name setter setOwner("") then getOwner() unchanged

4. public/private and encapsulation — the wall that pays rent

Encapsulation is the combination: attributes private, behaviour public, invariants enforced at the interface. mark under private is not about secrecy — every user of your program could always type any value. It is about who must obey the rules: with the field private, the only path to it is through a method, so the rules on that path are mandatory, not optional.

The concrete payoffs, each one you have already paid for procedurally:

  1. Invariants become guaranteed. “Balance is never negative” stops being a hope and becomes a structural fact — balance cannot change except through deposit/withdraw, which enforce it.
  2. Debugging narrows to the class. If balance is wrong, Lesson 1 of the Debugging module’s workflow now shrinks to: which method wrote it? Four methods, one wall.
  3. You may change the interior freely. Rename balance to balanceCents, cache a computed value, switch the storage — no code outside the class can break, because no outside code ever touched the field. This is why the Files module’s format contract idea and encapsulation are cousins: both let the inside change while the interface holds.

The course’s two naming rules (Records module convention, now standard C++ practice): attribute names in camelCase starting lowercase (balance, ownerName); getters named get..., setters set..., predicates is.../has.... Consistency here is what makes an unfamiliar class readable.

The getter/setter discipline — and when not to write them


5. struct vs class — the one-keyword truth

The only mechanical difference: in a struct, members are public by default; in a class, private by default. Everything else — methods, constructors, all of it — is identical. The convention this course follows: use struct for passive bundles of data (the Records module’s world), class when there is an invariant to protect (everything in this module). When you refactor a Records-module struct into a class, the fields keep their names; you add the labels, the methods, and the constructors.


Check yourself

Where next

— Engr. Dr. Muhammad Nadeem Majeed, Professor, Department of Data Science (PUCIT), University of the Punjab, Lahore · About the instructor