Encapsulation keeps an object’s valid state behind a small public interface. Private data alone is not enough: constructors and mutating methods must enforce the same invariant, and no returned reference or pointer should let callers bypass those checks.
The account never allows a negative balance because callers cannot assign the field directly and withdraw validates before mutation.
#include <iostream>
#include <stdexcept>
class Account {
public:
explicit Account(int opening) : balance_{opening} {
if (opening < 0) throw std::invalid_argument{"negative opening balance"};
}
void deposit(int amount) { if (amount <= 0) throw std::invalid_argument{"deposit"}; balance_ += amount; }
bool withdraw(int amount) { if (amount <= 0 || amount > balance_) return false; balance_ -= amount; return true; }
int balance() const { return balance_; }
private:
int balance_;
};
int main() { Account a{100}; a.withdraw(30); std::cout << a.balance() << '\n'; }
70
Returning a const reference can avoid a copy but ties callers to object lifetime and may expose internal storage. Return small values by value and expose views only when lifetime rules are clear.
Expose operations that name user intent rather than a sequence of setters. An Account::withdraw operation can check amount and balance together, while separate public balance mutations cannot preserve that rule. Keep representation details private so storage can change without rewriting callers.
Const member functions promise not to modify observable object state and make read-only use possible. Return values for small data and carefully bounded const references for owned data whose lifetime is clear. Avoid friend access unless two types genuinely implement one tightly coupled abstraction.
Encapsulation is useful when the class can guarantee a rule that callers cannot bypass. Keep representation private, validate constructor input, and expose operations named after domain behavior. Returning a mutable reference to private storage defeats the boundary even when the field itself is private.
A mutating method should either establish the complete next valid state or leave the object unchanged. Return a result that lets the caller distinguish rejection from success without exposing internal fields.
#include <iostream>
class Account {
int balance_;
public:
explicit Account(int opening) : balance_(opening >= 0 ? opening : 0) {}
bool withdraw(int amount) {
if (amount <= 0 || amount > balance_) return false;
balance_ -= amount;
return true;
}
int balance() const { return balance_; }
};
int main() {
Account account(50);
std::cout << std::boolalpha << account.withdraw(70) << '\n';
std::cout << account.balance() << '\n';
}
false
50
The rejected operation leaves the balance invariant intact, and callers can observe only the safe read interface.
Try this next
0 of 2 completed
No. Expose operations the object can safely perform instead of recreating public fields through accessors.
Validate them in constructors and public mutating methods so invalid state cannot enter through normal use.
It promises not to modify the object and allows the getter to be called on const instances.
Explore 500+ free tutorials across 20+ languages and frameworks.