What Is It?
What Is Encapsulation in C++?
Encapsulation is the bundling of data (member variables) and the methods (member functions) that operate on that data into a single unit called a class, while restricting direct access to some of the object's components. It is the mechanism that enforces data hiding in C++.
In practical terms, encapsulation means making member variables private and providing controlled access through public getter and setter functions. The outside world interacts with the object only through its public interface, never touching internal state directly.
class BankAccount {
private:
double balance; // Hidden from outside
string accountId; // Hidden from outside
public:
BankAccount(string id, double initial) : accountId(id), balance(initial) {}
// Getter: read-only access
double getBalance() const { return balance; }
string getId() const { return accountId; }
// Setter: controlled write access with validation
void deposit(double amount) {
if (amount > 0) balance += amount;
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
};Without encapsulation, anyone could write account.balance = -5000; and corrupt the object's state. With encapsulation, the withdraw() method enforces the invariant that balance cannot go negative.
C++ provides three access specifiers to control visibility: private (accessible only within the class), protected (accessible within the class and its derived classes), and public (accessible from anywhere). C++ also provides friend functions and friend classes as a controlled escape hatch to grant specific external entities access to private members.
Why Does It Matter?
Why Does Encapsulation Matter?
Encapsulation is one of the four pillars of Object-Oriented Programming (along with inheritance, polymorphism, and abstraction). In C++, understanding access control is essential because the language gives the programmer explicit control over what is visible and what is hidden.
1. Data Integrity and Invariant Enforcement
When Arjun makes the age field private and exposes a setAge(int a) method that rejects negative values, he guarantees that no part of the codebase can ever create a Student with a negative age. Without encapsulation, any function could write student.age = -10; and the program would silently accept it.
2. Interview and Placement Essential
Every OOP interview at companies like TCS, Infosys, Wipro, Amazon, and Microsoft includes questions on access specifiers, the difference between private and protected, when to use friend functions, and the tradeoffs of exposing internal state. The friend keyword is a particularly common interview topic because it breaks encapsulation in a controlled way.
3. Decoupling Interface from Implementation
When Kavya changes the internal representation of a Date class from three separate integers (day, month, year) to a single Unix timestamp, the public interface (getDay(), getMonth(), getYear()) remains unchanged. Client code does not need to be modified at all. This is only possible because the internal data was private.
4. The friend Keyword: Controlled Access
Sometimes two classes are so tightly coupled that forcing everything through getters/setters is inefficient or awkward. C++ provides friend as a deliberate, controlled way to grant access. Understanding when friend is appropriate (operator overloading, tightly coupled classes) versus when it violates encapsulation is critical for writing clean C++ code.
5. The mutable Keyword: Logical Constness
The mutable keyword allows modifying a member even in a const member function. This is used for caching, logging, and mutex locking where the external state of the object is logically unchanged even though some internal bookkeeping data is modified. This is an advanced topic frequently asked in senior-level interviews.
Detailed Explanation
Detailed Explanation
1. Access Specifiers: private, protected, public
C++ has three access specifiers that control who can access class members:
class Example {
private:
int secret; // Only accessible within Example
protected:
int inherited; // Accessible in Example and derived classes
public:
int visible; // Accessible from anywhere
Example() : secret(1), inherited(2), visible(3) {}
};
class Child : public Example {
public:
void test() {
// cout << secret; // Error: private in Example
cout << inherited; // OK: protected, accessible in derived
cout << visible; // OK: public
}
};
int main() {
Example e;
// cout << e.secret; // Error: private
// cout << e.inherited; // Error: protected
cout << e.visible; // OK: public
}Default access: In a class, the default is private. In a struct, the default is public. This is the only difference between class and struct in C++.
2. Getters and Setters
Getters provide read access to private data. Setters provide write access with validation. This pattern is the primary mechanism for enforcing encapsulation.
class Student {
private:
string name;
int age;
double gpa;
public:
Student(string n, int a, double g) : name(n), age(a), gpa(g) {}
// Getters (const: promise not to modify object)
string getName() const { return name; }
int getAge() const { return age; }
double getGpa() const { return gpa; }
// Setters (with validation)
void setAge(int a) {
if (a >= 0 && a <= 150) age = a;
}
void setGpa(double g) {
if (g >= 0.0 && g <= 10.0) gpa = g;
}
};The const after the getter function signature is important: it tells the compiler (and the reader) that calling this function does not modify the object. This allows calling getters on const references and pointers.
3. Friend Functions
A friend function is a non-member function that has access to the private and protected members of a class. The class grants friendship by declaring the function inside the class body with the friend keyword.
class Box {
private:
double length, width, height;
public:
Box(double l, double w, double h) : length(l), width(w), height(h) {}
// Grant friendship to this function
friend double volume(const Box& b);
friend bool isLarger(const Box& a, const Box& b);
};
// Not a member function, but can access private members
double volume(const Box& b) {
return b.length * b.width * b.height;
}
bool isLarger(const Box& a, const Box& b) {
return volume(a) > volume(b);
}Key points about friend functions:
- Declared inside the class, defined outside (no
Box::scope) - Not a member: no
thispointer - Can access private and protected members of the class
- Friendship is not inherited (a friend of Base is not automatically a friend of Derived)
- Friendship is not mutual (if A is a friend of B, B is not automatically a friend of A)
- Friendship is not transitive (if A is a friend of B, and B is a friend of C, A is not a friend of C)
4. Friend Classes
An entire class can be declared as a friend. All member functions of the friend class can then access the private members of the granting class.
class Engine {
private:
int horsepower;
double temperature;
public:
Engine(int hp) : horsepower(hp), temperature(90.0) {}
friend class Mechanic; // Mechanic can access everything in Engine
};
class Mechanic {
public:
void diagnose(const Engine& e) {
// Can access private members of Engine
cout << "HP: " << e.horsepower << endl;
cout << "Temp: " << e.temperature << endl;
}
void tune(Engine& e, int newHp) {
e.horsepower = newHp; // Direct access to private member
}
};Use friend classes sparingly. They are appropriate when two classes are tightly coupled (e.g., an iterator class that needs access to a container's internals), but overuse breaks encapsulation.
5. The mutable Keyword
The mutable keyword allows a member to be modified even when the object is accessed through a const reference or when the member function is const. This is used for internal bookkeeping that does not affect the logical state of the object.
class DataCache {
private:
int data;
mutable int accessCount; // Can be modified in const methods
mutable bool cacheValid;
mutable int cachedResult;
public:
DataCache(int d) : data(d), accessCount(0), cacheValid(false), cachedResult(0) {}
int getData() const {
accessCount++; // OK: mutable
return data;
}
int expensiveCompute() const {
accessCount++;
if (!cacheValid) {
cachedResult = data * data + data; // Cache the result
cacheValid = true;
}
return cachedResult;
}
int getAccessCount() const { return accessCount; }
};Common uses of mutable: caching/memoization, access counters, mutex locks in const methods, lazy initialization.
6. Nested Classes
A class defined inside another class is called a nested class. The nested class can access private members of the enclosing class (in C++11 and later).
class LinkedList {
private:
struct Node { // Nested class (private)
int data;
Node* next;
Node(int d) : data(d), next(nullptr) {}
};
Node* head;
public:
LinkedList() : head(nullptr) {}
void push(int val) {
Node* newNode = new Node(val);
newNode->next = head;
head = newNode;
}
void print() const {
Node* curr = head;
while (curr) {
cout << curr->data << " ";
curr = curr->next;
}
cout << endl;
}
~LinkedList() {
while (head) {
Node* temp = head;
head = head->next;
delete temp;
}
}
};The Node struct is an implementation detail. By making it private, no outside code can create or manipulate Node objects directly. This is encapsulation at the class design level.
7. struct vs class in C++
In C++, struct and class are nearly identical. The only difference is default access: struct members are public by default, while class members are private by default. The same applies to inheritance: struct Derived : Base is public inheritance by default, while class Derived : Base is private inheritance by default.
struct Point {
int x, y; // public by default
};
class Point2 {
int x, y; // private by default
public:
Point2(int a, int b) : x(a), y(b) {}
int getX() const { return x; }
};
Code Examples
#include <iostream>
#include <string>
using namespace std;
class Employee {
private:
string name;
int age;
double salary;
public:
Employee(string n, int a, double s) : name(n), age(0), salary(0) {
setAge(a);
setSalary(s);
}
string getName() const { return name; }
int getAge() const { return age; }
double getSalary() const { return salary; }
void setAge(int a) {
if (a >= 18 && a <= 65) age = a;
else cout << "Invalid age: " << a << endl;
}
void setSalary(double s) {
if (s >= 0) salary = s;
else cout << "Invalid salary: " << s << endl;
}
void display() const {
cout << name << ", Age: " << age << ", Salary: " << salary << endl;
}
};
int main() {
Employee e("Arjun", 25, 50000);
e.display();
e.setAge(200);
e.setSalary(-1000);
e.display();
e.setAge(30);
e.setSalary(60000);
e.display();
return 0;
}Employee class keeps name, age, and salary private. The setters validate input before modifying state: age must be 18-65, salary must be non-negative. Invalid values are rejected. This prevents corrupt object state.#include <iostream>
using namespace std;
class Base {
private:
int pvt;
protected:
int prot;
public:
int pub;
Base() : pvt(1), prot(2), pub(3) {}
void showAll() const {
cout << "private=" << pvt << " protected=" << prot << " public=" << pub << endl;
}
};
class Derived : public Base {
public:
void access() {
// cout << pvt; // Error: private member of Base
cout << "Derived accesses protected: " << prot << endl;
cout << "Derived accesses public: " << pub << endl;
}
};
int main() {
Base b;
// cout << b.pvt; // Error: private
// cout << b.prot; // Error: protected
cout << "Main accesses public: " << b.pub << endl;
b.showAll();
Derived d;
d.access();
return 0;
}private members are only accessible within the class itself. protected members are accessible in the class and its derived classes. public members are accessible everywhere. This is demonstrated by trying to access each from main() and from a derived class.#include <iostream>
using namespace std;
class Rectangle {
private:
double length, width;
public:
Rectangle(double l, double w) : length(l), width(w) {}
friend double area(const Rectangle& r);
friend bool isBigger(const Rectangle& a, const Rectangle& b);
friend ostream& operator<<(ostream& os, const Rectangle& r);
};
double area(const Rectangle& r) {
return r.length * r.width;
}
bool isBigger(const Rectangle& a, const Rectangle& b) {
return area(a) > area(b);
}
ostream& operator<<(ostream& os, const Rectangle& r) {
os << "Rect(" << r.length << " x " << r.width << ")";
return os;
}
int main() {
Rectangle r1(5, 3), r2(4, 4);
cout << r1 << " area = " << area(r1) << endl;
cout << r2 << " area = " << area(r2) << endl;
cout << "r1 bigger? " << (isBigger(r1, r2) ? "Yes" : "No") << endl;
return 0;
}area() and isBigger() functions are not members of Rectangle, but because they are declared as friend, they can access length and width directly. The operator<< must be a friend because its left operand is ostream.#include <iostream>
#include <string>
using namespace std;
class Patient {
private:
string name;
int age;
string diagnosis;
public:
Patient(string n, int a, string d) : name(n), age(a), diagnosis(d) {}
friend class Doctor;
};
class Doctor {
public:
void examine(const Patient& p) {
cout << "Examining " << p.name << ", age " << p.age << endl;
cout << "Diagnosis: " << p.diagnosis << endl;
}
void updateDiagnosis(Patient& p, string newDiag) {
p.diagnosis = newDiag;
cout << "Updated diagnosis for " << p.name << ": " << p.diagnosis << endl;
}
};
int main() {
Patient p("Ravi", 28, "Flu");
Doctor doc;
doc.examine(p);
doc.updateDiagnosis(p, "Viral Fever");
doc.examine(p);
return 0;
}Doctor is declared as a friend class of Patient. This means every member function of Doctor can access the private members of Patient. The relationship is one-directional: Patient cannot access Doctor's private members.#include <iostream>
using namespace std;
class Counter {
private:
int value;
mutable int readCount;
public:
Counter(int v) : value(v), readCount(0) {}
int getValue() const {
readCount++; // OK because readCount is mutable
return value;
}
int getReadCount() const { return readCount; }
};
int main() {
const Counter c(42);
cout << "Value: " << c.getValue() << endl;
cout << "Value: " << c.getValue() << endl;
cout << "Value: " << c.getValue() << endl;
cout << "Read count: " << c.getReadCount() << endl;
return 0;
}c is const and getValue() is a const method, the mutable keyword on readCount allows it to be incremented. The logical state of the object (its value) is unchanged, but the bookkeeping (how many times it was read) is tracked.#include <iostream>
using namespace std;
class Stack {
private:
struct Node {
int data;
Node* next;
Node(int d, Node* n) : data(d), next(n) {}
};
Node* top;
int count;
public:
Stack() : top(nullptr), count(0) {}
void push(int val) {
top = new Node(val, top);
count++;
}
int pop() {
if (!top) throw runtime_error("Stack empty");
int val = top->data;
Node* temp = top;
top = top->next;
delete temp;
count--;
return val;
}
int size() const { return count; }
bool empty() const { return top == nullptr; }
~Stack() {
while (top) {
Node* temp = top;
top = top->next;
delete temp;
}
}
};
int main() {
Stack s;
s.push(10);
s.push(20);
s.push(30);
cout << "Size: " << s.size() << endl;
cout << "Pop: " << s.pop() << endl;
cout << "Pop: " << s.pop() << endl;
cout << "Size: " << s.size() << endl;
return 0;
}Node struct is a private nested class inside Stack. No code outside Stack can create, access, or even know about Node. This is proper encapsulation of implementation details. Users interact only with push(), pop(), size(), and empty().#include <iostream>
using namespace std;
struct PointStruct {
int x, y; // public by default
PointStruct(int a, int b) : x(a), y(b) {}
};
class PointClass {
int x, y; // private by default
public:
PointClass(int a, int b) : x(a), y(b) {}
int getX() const { return x; }
int getY() const { return y; }
};
int main() {
PointStruct ps(3, 4);
cout << "Struct: " << ps.x << ", " << ps.y << endl;
PointClass pc(5, 6);
// cout << pc.x; // Error: private
cout << "Class: " << pc.getX() << ", " << pc.getY() << endl;
return 0;
}struct, members are public by default, so ps.x works directly. In a class, members are private by default, so pc.x would fail and getters are needed. This is the only functional difference between struct and class in C++.Common Mistakes
Making All Members Public (No Encapsulation)
class Student {
public:
string name;
int age;
double gpa;
};
Student s;
s.age = -5; // No validation!
s.gpa = 99.9; // Invalid GPA accepted!class Student {
private:
string name;
int age;
double gpa;
public:
Student(string n, int a, double g) : name(n), age(0), gpa(0) {
setAge(a); setGpa(g);
}
void setAge(int a) { if (a >= 0 && a <= 150) age = a; }
void setGpa(double g) { if (g >= 0 && g <= 10) gpa = g; }
int getAge() const { return age; }
double getGpa() const { return gpa; }
};Forgetting const on Getter Functions
class Account {
double balance;
public:
Account(double b) : balance(b) {}
double getBalance() { return balance; } // Missing const!
};
void print(const Account& a) {
cout << a.getBalance(); // Error: calling non-const on const reference
}class Account {
double balance;
public:
Account(double b) : balance(b) {}
double getBalance() const { return balance; } // const!
};
void print(const Account& a) {
cout << a.getBalance(); // OK now
}const because they do not modify the object. Without const, they cannot be called on const objects or through const references, breaking the ability to pass objects by const reference.Confusing protected with private in Derived Classes
class Base {
private:
int secret;
public:
Base(int s) : secret(s) {}
};
class Derived : public Base {
public:
Derived(int s) : Base(s) {}
void reveal() {
cout << secret; // Error: secret is private in Base!
}
};class Base {
protected:
int secret; // Changed to protected
public:
Base(int s) : secret(s) {}
};
class Derived : public Base {
public:
Derived(int s) : Base(s) {}
void reveal() {
cout << secret; // OK: protected is accessible in derived
}
};protected instead of private when derived classes need access. Private members are invisible to all derived classes. This is one of the most common access specifier mistakes.Assuming Friendship Is Inherited or Mutual
class A {
private:
int x = 10;
friend class B;
};
class B {
private:
int y = 20;
};
class C : public B {
public:
void test(A& a) {
cout << a.x; // Error: C is not a friend of A!
}
};
void test(A& a, B& b) {
// A cannot access b.y just because B is a friend of A
// Friendship is not mutual
}class A {
private:
int x = 10;
friend class B;
friend class C; // Explicitly grant friendship to C
};
class B {
private:
int y = 20;
friend class A; // Explicitly grant mutual friendship if needed
};
class C : public B {
public:
void test(A& a) {
cout << a.x; // OK: C is now a friend of A
}
};Modifying Members in a const Method Without mutable
class Logger {
int callCount; // Not mutable!
public:
Logger() : callCount(0) {}
void log(const string& msg) const {
callCount++; // Error: modifying member in const method!
cout << msg << endl;
}
};class Logger {
mutable int callCount; // mutable!
public:
Logger() : callCount(0) {}
void log(const string& msg) const {
callCount++; // OK: mutable allows modification in const method
cout << msg << endl;
}
int getCallCount() const { return callCount; }
};const method (for bookkeeping, caching, etc.), mark it mutable. Without mutable, the compiler enforces that const methods cannot modify any member.Summary
- Encapsulation bundles data and methods into a class while restricting direct access to internal state. It enforces data hiding and integrity.
- C++ has three access specifiers: private (class only), protected (class + derived classes), and public (accessible everywhere).
- In a class, the default access is private. In a struct, the default access is public. This is the only difference between class and struct in C++.
- Getters provide read access to private members. Setters provide controlled write access with validation. Getters should always be marked const.
- A friend function is a non-member function that can access private and protected members of the class that declares it as friend.
- A friend class grants all its member functions access to the private members of the declaring class. Friendship is not inherited, not mutual, and not transitive.
- The mutable keyword allows a member variable to be modified in const member functions. Use it for caching, counters, and mutex locks.
- Nested classes (classes defined inside other classes) are used to hide implementation details like nodes in a linked list or stack.
- Always prefer private data with public methods over public data. This allows changing the internal representation without affecting client code.
- The friend keyword should be used sparingly. Legitimate uses include operator<< overloading, tightly coupled helper classes, and iterator patterns.