OOP · The Four Pillars
Encapsulation & Abstraction
Expose the right things. Control the rest.
Sign in to track your score
What you'll learn
- Say what a class should expose and what it should protect — and why.
- Choose between public, protected and private as a design decision.
- Recognise a setter that guards nothing, and replace it with something better.
- Explain the difference between encapsulation and abstraction with one example.
How to use this module. Wherever you see THINK, PREDICT or SPOT THE PROBLEM, stop and answer before reading on. The answer always follows immediately — cover it with your hand if you need to. Every code sample here compiles and was run to confirm its output.
There is a parcel locker in the lobby of an apartment building. A courier puts a package inside and sets a code. Later you walk up, type the code, and the door opens.
From the outside, three things can happen: a package goes in, someone types a code, a package comes out.
Inside, the locker is keeping track of rather more:
- whether the door is currently locked
- whether there is actually a package inside
- what the correct access code is
- how many wrong attempts are still allowed before it shuts you out
Now picture the software that runs it. If every part of that program could reach in and change those four values directly, someone could set the attempt counter back to 100 and try codes all night. Someone could flip "locked" to false without typing anything at all.
Think — Nothing is broken in that situation. No function crashed, no value overflowed.
So what exactly went wrong — and whose fault is it?
Hold that question. This module is about two ideas that answer it:
- Encapsulation — the object deciding how its own information may be changed.
- Abstraction — deciding what someone needs to know in order to use the object at all.
Both get explained from scratch. No prior vocabulary needed.
3.1 Encapsulation: data guarded by its own code
Here is that locker, written the way a first version usually looks.
class PackageLocker {
public:
bool locked = true;
bool packagePresent = false;
int attemptsRemaining = 3;
};
Three values, all reachable from anywhere. Which means this compiles perfectly:
PackageLocker locker;
locker.locked = false; // door opens, no code typed
locker.attemptsRemaining = 999; // guess codes all day
locker.packagePresent = false; // the package vanishes from the records
Spot the problem — Every one of those three lines is valid C++. The class did exactly what it was written to do.
So where is the mistake?
Answer — The mistake is that the locker has no say in any of it.
Think about what should be true. You should not be able to open the door without the right code. You should get three tries, not unlimited. A package should only come out if one actually went in.
Those are real rules. But in the code above, they exist nowhere. They live only in the head of whoever writes the calling code, and every new caller has to remember them again.
A single public value looks harmless. Five different parts of a program writing to it, one of them added in a hurry on a Friday evening, is a different situation entirely.
Building up the idea
Three observations, in order:
- The locker keeps some information that describes its current situation — locked or not, package inside or not, tries remaining.
- The locker also has functions that know how to work with that information — opening the door, accepting a package, handing one over.
- The locker should be the one deciding how that information is allowed to change.
Those three points are the whole idea. Now the vocabulary, so the words mean something when you meet them later:
- The information an object keeps about its current situation is called its state.
- The functions that use or change that information are its behaviour.
- Keeping those two together, and controlling how the rest of the program interacts with them, is encapsulation.

The two halves of any object, with the words that describe them.
The same locker, redesigned
class PackageLocker {
public:
void insertPackage(int code) {
if (packagePresent) return; // cannot overwrite a live delivery
accessCode = code;
packagePresent = true;
locked = true;
attemptsRemaining = 3;
}
bool unlock(int code) {
if (attemptsRemaining == 0) return false;
if (code != accessCode) { attemptsRemaining--; return false; }
locked = false;
return true;
}
bool collectPackage() {
if (locked || !packagePresent) return false;
packagePresent = false;
locked = true;
return true;
}
bool isLocked() const { return locked; }
private:
bool locked = true;
bool packagePresent = false;
int accessCode = 0;
int attemptsRemaining = 3;
};
What is happening: the four values moved below private:, which means no code outside the class can touch them. Above that, the class offers three operations, and each one contains the rule that belongs to it. attemptsRemaining goes down inside unlock() — the only place in the program that could possibly know an attempt just failed.
Why it matters: the three nonsense lines from earlier no longer compile. They have not become bad practice; they have become impossible.

Outside code can still change the locker — but only by going through a function the locker wrote.
Predict — Someone types three wrong codes, then finally types the right one:
What gets printed?
> locker.insertPackage(4417);
> locker.unlock(1111);
> locker.unlock(2222);
> locker.unlock(3333);
> std::cout << locker.unlock(4417) << " " << locker.isLocked();
>
Answer — **** 0 1 The fourth call returns false and the door stays locked. By then attemptsRemaining had already reached zero, so the correct code arrives too late.
Notice what nobody had to do: no caller counted the attempts. The locker counted them, because the locker owns that number.
Quick check — The locker's
lockedvalue is private, yetunlock()changes it every time someone types the right code.Is that a hole in the design?
Answer — No — that is the design working. Private does not mean frozen. It means the only way to change the value is through code the class itself controls, which is exactly what unlock() is.
Remember — Encapsulation is not about hiding variables. It is about making the object responsible for managing its own information.
3.2 Access modifiers: public, private, protected
A hotel room door controller serves several very different parties.
- The front desk software needs to ask whether the room is free, and whether a keycard opens it.
- A special kind of room — say a suite with a housekeeping override — might need to reach one level deeper than an ordinary room does.
- Nobody at all should be able to read the master code out of the object.
Should a guest be able to change the hotel's internal security state? Obviously not. But "obviously" is not something a compiler understands, so C++ gives you three labels to say it explicitly.
In plain English first:
- public — outside code is allowed to use it.
- private — only the class itself can touch it directly.
- protected — the class can touch it, and so can classes built from it, but ordinary outside code cannot.
That third one mentions "classes built from it." That is inheritance, which gets its own module later. For now, treat it as: a more specialised version of this class.

class HotelRoom {
public:
bool isAvailable() const { return !occupied; }
bool requestAccess(int keyCard) { return keyCard == masterCode; }
protected:
void forceUnlock() { occupied = false; }
private:
bool occupied = false;
int masterCode = 884219;
};
Predict — Which of these four lines compile?
> HotelRoom room;
> room.isAvailable(); // 1
> room.requestAccess(884219); // 2
> room.forceUnlock(); // 3
> room.masterCode = 0; // 4
>
Answer — 1 and 2 compile. 3 and 4 do not. Line 3 fails because forceUnlock() is protected. It is worth being precise about why: main() is not a class built from HotelRoom, so it counts as ordinary outside code and gets no special treatment.
Line 4 fails because masterCode is private.
| Modifier | Who Can Reach It | What It Is Usually For |
|---|---|---|
public | outside code too | the operations the class offers to whoever uses it |
protected | the class and classes built from it | something a specialised version genuinely needs, but outsiders do not |
private | the class itself | internal details, and values that come with rules attached |
One point worth being stubborn about: these are not ranked from worst to best. private is not automatically the safe choice and public is not a mistake. isAvailable() is public precisely because answering that question is the entire job of the class — a door controller nobody can ask about availability would be useless.
What deserves care is public data, not public members generally. A public function is a decision about what the class offers. A public variable is a decision to give up control over a value, which is a much bigger thing to hand over.
3.3 Getters & setters: controlled doors
Start from a practical hole in what you have learned so far.
class Order {
private:
double tip = 0;
};
The tip is private, which stops anyone from setting it to nonsense. It also stops anyone from reading it. The screen that shows the customer their bill now has no way to find out what the tip is.
So how does outside code get at a private value?
Reading: the getter
A small public function that hands the value back.
double getTip() const { return tip; }
That is a getter — a function that lets outside code read a value in a controlled way. The class stays in charge: it can decide to return a rounded number, a formatted string, or nothing at all.
Changing: the setter
Reading is only half the problem. The customer may want to increase the tip.
void setTip(double amount) {
if (amount >= 0) {
tip = amount;
}
}
That is a setter — a function that lets outside code request a change.
The word "request" is doing real work in that sentence. Compare the two designs:
- A public variable says: here, change this to whatever you like.
- A setter says: tell me what you want, and I will decide whether it is allowed.
setTip(-30) does nothing at all. The value stays where it was, and no negative tip ever reaches the payout calculation.

Not every value needs a setter
This is where a lot of beginner code goes wrong — one private variable, one getter, one setter, repeated down the whole class out of habit.
An order ID is assigned once, when the order is created, and should never change afterwards. Writing setOrderId() advertises a change that ought to be impossible.
class Order {
public:
Order(int id) : orderId(id) {}
int getOrderId() const { return orderId; }
std::string getStatus() const { return status; }
bool cancelOrder() {
if (status == "DELIVERED") return false;
status = "CANCELLED";
return true;
}
private:
int orderId;
std::string status = "PLACED";
};
orderId gets a getter and no setter. Outside code can ask what the ID is and can never change it.
A better idea than a generic setter
Look at cancelOrder() and picture the alternative, setStatus(std::string).
A setStatus() would accept "CANCELLED", but also "DELIVERED", "PLACED", or "banana". It would let a delivered order be cancelled, because it has no idea what those words mean.
cancelOrder() carries a real rule — a delivered order cannot be cancelled — and its name records why the change happened rather than merely which variable got written.
Real systems are full of these: applyDiscount(percent), changeAddress(newAddress), increaseVolume(). Each one is a controlled door with a meaningful sign on it.
Spot the problem — The variable is private and both functions are public. Is this thermostat's data actually being looked after?
> class Thermostat {
> private:
> int targetTemp = 21;
> public:
> int getTargetTemp() const { return targetTemp; }
> void setTargetTemp(int t) { targetTemp = t; }
> };
>
Answer — no setTargetTemp(300) sets the target to 300 degrees and the class accepts it without blinking. The setter checks nothing, so it hands over exactly the same power a public variable would have handed over — just with more typing in between.
Private plus an unconditional setter is not protection. A guard that waves everyone through is not a guard.
Quick check — An order's
statusis private, has a getter, and has no setter — butcancelOrder()can change it.Can outside code still change the status?
Answer — Yes, through cancelOrder(), and only in the one way the class allows. That is the goal, not a leak. Encapsulation is about controlled change, not about making values permanent.
3.4 Abstraction: show the WHAT, hide the HOW
Walk up to an office coffee machine and press one button. A latte comes out.
For that to happen, the machine has to heat water to the right temperature, grind beans to the right coarseness, force water through the grounds at the right pressure, steam milk, and combine the two.
You pressed one button. You did not think about any of that, and you did not need to.
class CoffeeMachine {
public:
void makeLatte() {
heatWater();
grindBeans();
brewCoffee();
steamMilk();
std::cout << "Latte ready\n";
}
private:
int waterTemp = 20;
int beansGrams = 500;
void heatWater() { waterTemp = 93; }
void grindBeans() { beansGrams -= 18; }
void brewCoffee() { }
void steamMilk() { }
};
Whoever uses this class writes one line:
machine.makeLatte(); // Latte ready
What is happening: four internal steps sit behind one public function, and all four of those step functions are private — outside code cannot call them individually and does not need to.
Why it matters: if the grinding logic is rewritten next month, or a fifth step gets added, machine.makeLatte() still means the same thing. Nothing that uses the machine has to change.
Now the vocabulary. Showing someone the useful operations while keeping the internal steps out of their way is called abstraction.
Two words go with it, and they are worth separating carefully:
- WHAT — what the object lets you do. Here:
makeLatte(). - HOW — the steps it performs internally to do it. Here: heating, grinding, brewing, steaming.

The set of operations an object offers to the outside world has a name too: its interface. The code that actually performs the work is the implementation. Abstraction is the act of choosing a good interface and letting the implementation stay behind it.
Note the direction there. You are not mainly hiding things — you are choosing what to show. A class that hides everything and offers nothing useful has no abstraction at all, because it has no interface worth using.
Encapsulation and abstraction on the same machine
These two get confused constantly, so take one object and ask both questions about it.
Encapsulation asks: who is allowed to change the machine's information?
waterTemp and beansGrams are private. Only the machine's own functions set them. Nobody outside can push the boiler to 200 degrees.
Abstraction asks: what does the user need to know in order to use the machine?
That it can make a latte. Not that a latte requires four steps, and not what those steps are.

| Encapsulation | Abstraction | |
|---|---|---|
| The question | Who may change this data, and how? | What does the user need to know? |
| It is about | protecting the object's information | choosing which operations to offer |
| On this machine | waterTemp is private and only internal functions set it | makeLatte() is offered instead of four separate steps |
| It goes wrong when | outside code can put the object in a nonsense state | the user has to understand the internals to get anything done |
The two decisions are independent, which is the part most people miss. A coffee machine could keep every variable private with proper checks — good encapsulation — and still force you to call heatWater(), grindBeans(), brewCoffee() and steamMilk() yourself in the right order. Its data would be well protected and its interface would still be miserable.
Common Misconceptions
❌ Encapsulation just means making variables private. Why it seems right: private is the visible part, and it is what you actually type. What is true: private is one tool. Encapsulation is the object keeping its information together with the functions that manage it, and staying in charge of how that information changes. A class full of private variables with unchecked setters has the keyword and none of the idea.
❌ Every private variable needs a getter and a setter. Why it seems right: editors generate both in one keystroke, so it looks like the standard shape of a class. What is true: a full set re-opens everything you just closed. Add a getter when something outside genuinely needs to read the value, and a setter only when there is a rule worth enforcing.
❌ Public variables are always bad. Why it seems right: beginners are usually taught "make everything private" as a safe habit. What is true: public is how a class says what it is for. Public functions are the whole point. Public data is the thing to think twice about — and even that is fine for a simple bundle of values with no rules, such as a 2D point.
❌ Protected is basically a gentler private. Why it seems right: from ordinary code both of them block you, so they look identical from outside. What is true: protected opens the member to every class built from this one, now and in the future. That is a genuine commitment — you can no longer change that member freely without breaking those classes.
❌ If a class has setters, its encapsulation is good. Why it seems right: a setter looks more deliberate than a bare public variable. What is true: it only helps if it checks something. void setTemp(int t) { temp = t; } gives away exactly what a public variable would give away. The benefit comes from the rule inside the function, not from the function existing.
❌ Abstraction means hiding all the code. Why it seems right: the hidden part is the part you notice. What is true: abstraction is choosing what to show. Hiding follows from that choice. A class that exposes nothing useful is not well abstracted — it is unusable.
❌ Encapsulation and abstraction are two names for the same thing. Why it seems right: both involve private, and both reduce what outsiders can see. What is true: they answer different questions. Encapsulation asks who may change the data. Abstraction asks what the user needs to know. A class can get one right and the other wrong.
❌ If the implementation is hidden, that is abstraction. Why it seems right: invisibility feels like the defining feature. What is true: a compiled library hides its implementation completely and can still be painful to use. Abstraction is judged by whether the operations on offer are the right ones, not by how much is out of sight.
Interview Corner
Q1. What is encapsulation, in your own words? An object keeps its own information together with the functions that manage it, and stays in control of how the rest of the program changes that information. Marking variables private is one way to enforce that control — it is not the whole idea.
Q2. What is abstraction, and how is it different from simply hiding code? Abstraction is deciding what someone needs in order to use a class, and offering exactly that. Hiding the rest is a consequence of that decision. Judge an abstraction by whether the operations on offer are the right ones, not by how much is hidden.
Q3. A class has private fields and a public setter for every one of them. Is that good encapsulation?
class Elevator {
private:
int currentFloor = 0;
bool doorsOpen = false;
public:
void setCurrentFloor(int f) { currentFloor = f; }
void setDoorsOpen(bool b) { doorsOpen = b; }
};
Not by itself. Nothing here stops setDoorsOpen(true) followed by setCurrentFloor(7) — an elevator travelling between floors with its doors open. The fields are private, but the class gave away the same control a public variable would have. Encapsulation appears when a rule is enforced, or when the class offers a real operation such as moveTo(floor) that closes the doors first.
Q4. Why might a class expose cancelOrder() but deliberately not expose setStatus() ? setStatus() would allow any value, including transitions that should be impossible, such as reviving a delivered order. cancelOrder() carries the actual rule and its name records the caller's intent. The class keeps ownership of when the status may change.
Q5. Is this balance protected?
class Wallet {
private:
double balance = 0;
public:
void setBalance(double b) { balance = b; }
};
No. Any caller can set any value, including a negative one, so the power handed out is identical to a public variable. Real protection needs either a check inside the setter or, better, operations that match what actually happens to a wallet — deposit(), withdraw(), refund().
Q6. Which lines compile, and why?
class Door {
protected:
bool bolted = true;
public:
bool isBolted() const { return bolted; }
};
class SecureDoor : public Door {
public:
void release() { bolted = false; } // A
};
SecureDoor d;
d.release(); // B
d.bolted = false; // C
A and B compile; C does not. bolted is protected, so a function inside SecureDoor may touch it, but code outside the class may not — and main() counts as outside even though it is holding a SecureDoor.
Q7. Explain encapsulation and abstraction using one example. Take a coffee machine. Keeping waterTemp private so only the machine's own functions set it is encapsulation — a question of control. Offering makeLatte() instead of four separate steps is abstraction — a question of what the user should have to know. Same class, two decisions made separately.
Q8. When would you deliberately make data public? When the type is a simple bundle of values with no rules connecting them — a 2D point, a configuration struct, coordinates read from a file. There is nothing to enforce, so a getter and setter pair would add friction and protect nothing. As soon as the values have to stay consistent with each other, that changes.
Module Recap

ENCAPSULATION — "Don't let everyone directly mess with the object's important data." Keep the data and the functions that manage it together, and control how outside code interacts with that data.
ACCESS MODIFIERS — public: outside code can use it. private: the class controls direct access. protected: the class and classes built from it. None of them is the "good" one.
GETTERS & SETTERS — a getter is a controlled way to read; a setter is a controlled way to request a change. Not every value needs either, and a meaningful function often beats a generic setter.
ABSTRACTION — "Give me what I need to use; I don't need every internal step." WHAT is what the object lets you do. HOW is the way it does it internally.
Remember — Encapsulation asks: what can the outside world touch or change? Abstraction asks: what does the outside world actually need to know?
- 1.
class Locker { private: bool locked = true; public: void unlock(int code) { if (code == 4417) locked = false; } bool isLocked() const { return locked; } }; int main() { Locker l; l.locked = false; std::cout << l.isLocked(); }What happens?
- 2.
class Kiosk { public: void printTicket() { } protected: void calibrate() { } private: int serial = 77; }; class SelfServeKiosk : public Kiosk { public: void setup() { calibrate(); // 1 printTicket(); // 2 serial = 99; // 3 } }; int main() { SelfServeKiosk k; k.printTicket(); // 4 k.calibrate(); // 5 }Which numbered lines fail to compile?
- 3.
class Thermostat { private: int targetTemp = 21; public: void setTargetTemp(int t) { if (t >= 5 && t <= 30) targetTemp = t; } int getTargetTemp() const { return targetTemp; } }; int main() { Thermostat t; t.setTargetTemp(24); t.setTargetTemp(90); std::cout << t.getTargetTemp(); }What does this print?
- 4.
The scenario
A vending machine in an office corridor. It holds a number of items, each sold at a fixed price. A buyer inserts coins and presses a button. If there is enough money and stock remains, the machine releases an item and returns change. Staff restock it once a week.
Design the class. Aim for roughly fifteen minutes — this is about design decisions, not typing.
Work through these questions:
- What information does the machine need to keep?
- What should stay private?
- What should be public?
- What should the outside world actually be allowed to do?
- Which values should have no setter at all?
- Which functions should control the changes?
- Which internal steps can be hidden behind one public call?
- Where exactly is encapsulation happening?
- Where exactly is abstraction happening?
Hint — Two questions will get you most of the way.
For each value, ask: if outside code could set this to anything at all, what is the worst thing that happens?
For each public function, ask: does its name describe something the buyer wants, or something the code does internally?
A possible solution
class VendingMachine {
public:
VendingMachine(int stock, int unitPrice)
: itemsLeft(stock), price(unitPrice) {}
bool buy(int coinsInserted) {
if (itemsLeft == 0) return false;
if (coinsInserted < price) return false;
releaseItem();
returnChange(coinsInserted - price);
cashCollected += price;
itemsLeft--;
return true;
}
void restock(int howMany) {
if (howMany > 0) itemsLeft += howMany;
}
int getItemsLeft() const { return itemsLeft; }
bool isEmpty() const { return itemsLeft == 0; }
private:
int itemsLeft;
int price;
int cashCollected = 0;
void releaseItem() { }
void returnChange(int) { }
};
Running it:
VendingMachine m(2, 40);
m.buy(30); // false - not enough money
m.buy(50); // true - item out, 10 back as change
m.buy(40); // true - last item
m.buy(40); // false - empty
Why it is shaped this way
| Decision | Reason |
|---|---|
cashCollected has no getter and no setter | a buyer has no business reading the till, and nothing outside should be able to set it |