Encapsulation
So far, class members have often been publicly accessible, allowing outside code to modify object data directly. In this lesson, you will learn how C++ uses encapsulation, access specifiers, getters, setters, and validation to protect object state and provide controlled access.
Introduction
In the previous lessons, you learned how classes define objects and how constructors and destructors participate in object lifetime.
So far, many examples have placed data members inside the public section of a class. This allows outside code to read and modify those members directly.
class Student
{
public:
int marks;
};
Student student1;
student1.marks = 85;
student1.marks = -50;The second assignment stores an invalid value, but the object has no opportunity to reject it because outside code can directly modify its internal data.
Encapsulation solves this problem by controlling how the internal state of an object can be accessed and modified.
Encapsulation allows a class to protect its internal state and define the operations through which outside code can interact with that state.
What is Encapsulation?
Encapsulation is the object-oriented principle of grouping related data and behavior inside a class while controlling access to the internal implementation.
Instead of allowing outside code to modify object data freely, a class can keep its internal state private and provide carefully designed public member functions.
class Student
{
private:
int marks;
public:
void setMarks(int value)
{
// Control and validation
}
int getMarks() const
{
// Controlled access
return marks;
}
};Outside Code
│
▼
┌─────────────────────┐
│ Public Interface │
│ │
│ setMarks() │
│ getMarks() │
├─────────────────────┤
│ Private State │
│ │
│ marks │
└─────────────────────┘| Concept | Meaning |
|---|---|
| Class | Groups related state and behavior |
| Private State | Internal implementation hidden from direct outside access |
| Public Interface | Operations available to outside code |
| Validation | Rules applied before changing state |
| Encapsulation | Controlled interaction with an object |
Encapsulation means keeping an object’s internal details under the control of the class and exposing only the operations that outside code needs.
Why Do We Need Encapsulation?
Objects often have rules that must remain true throughout their lifetime. A student mark should remain within a valid range, a bank balance should change only through valid operations, and a product price should not become negative.
Without Encapsulation
- Outside code can modify data directly.
- Invalid values may enter the object.
- Business rules can be bypassed.
- Implementation details become exposed.
- Changes can affect many parts of the program.
With Encapsulation
- Internal state can be protected.
- Changes pass through controlled operations.
- Validation rules can be enforced.
- Implementation details remain hidden.
- Internal changes can be localized.
class BankAccount
{
public:
double balance;
};
BankAccount account;
// No protection
account.balance = -1000000;class BankAccount
{
private:
double balance = 0.0;
public:
void deposit(double amount)
{
if (amount > 0)
{
balance += amount;
}
}
};Protection
Prevents unrestricted direct modification of internal state.
Validation
Checks values before accepting changes.
Abstraction
Hides implementation details behind meaningful operations.
Maintainability
Allows internal implementation to change with less impact on users of the class.
Invariants
Helps keep objects in valid states.
Clear Responsibility
Places rules close to the data they protect.
Real-World Analogy
Imagine using an ATM. A customer cannot directly open the machine and change the stored account balance. Instead, the customer interacts with a controlled interface.
The ATM buttons and screen represent the public interface. The internal banking data and processing rules remain protected.
| ATM | C++ Encapsulation |
|---|---|
| Customer | Outside code |
| ATM screen and controls | Public interface |
| PIN verification | Validation logic |
| Internal banking system | Private implementation |
| Account data | Protected object state |
👤 Customer
│
▼
┌─────────────────────┐
│ Public Interface │
│ │
│ Enter PIN │
│ Withdraw Money │
│ Check Balance │
├─────────────────────┤
│ Protected System │
│ │
│ Account Data │
│ Validation Rules │
└─────────────────────┘The customer can use approved operations without gaining direct access to the system’s internal data.
Characteristics of Encapsulation
Grouping
Related data and functions are placed together inside a class.
Access Control
Access specifiers control member visibility.
Public Interface
Outside code interacts through selected public operations.
State Protection
Internal data can be protected from uncontrolled changes.
Validation
Input can be checked before state changes.
Implementation Hiding
Internal details can change without exposing them to users of the class.
Key Characteristics
- Groups related data and behavior inside a class.
- Uses access specifiers to control visibility.
- Protects internal implementation details.
- Provides selected operations through a public interface.
- Can validate changes before modifying object state.
- Helps maintain valid object invariants.
- Reduces unnecessary dependencies on implementation details.
- Supports maintainable object-oriented design.
Simply making every data member private and adding a getter and setter for each one does not automatically create good encapsulation. A well-designed class exposes meaningful operations and protects its own rules.
Access Specifiers
Access specifiers determine where class members can be accessed. C++ provides three primary access specifiers: public, private, and protected.
public
Members are accessible wherever the object itself is accessible.
private
Members are accessible only through the class and its friends.
protected
Members are accessible through the class, its friends, and derived classes under C++ access rules.
class Example
{
public:
int publicValue;
private:
int privateValue;
protected:
int protectedValue;
};class Student
{
public:
int marks;
};
Student student1;
student1.marks = 85; // Allowedclass Student
{
private:
int marks;
};
Student student1;
// ❌ Compilation error
// student1.marks = 85;Members of a class are private by default. Members of a struct are public by default.
A protected member cannot normally be accessed directly through an object from unrelated outside code. Protected access is primarily relevant to inheritance.
Access Specifier Comparison
| Access Specifier | Same Class | Derived Class | Unrelated Outside Code |
|---|---|---|---|
| public | Yes | Yes | Yes |
| private | Yes | No direct access | No |
| protected | Yes | Yes | No |
Public Members
- Form the external interface.
- Can be accessed by outside code.
- Should expose meaningful operations.
- Should avoid exposing unnecessary implementation details.
Non-Public Members
- Hide implementation details.
- Protect internal state.
- Support controlled modification.
- Reduce direct outside dependencies.
Same Derived Outside
Class Class Code
───── ─────── ───────
public ✓ ✓ ✓
private ✓ ✗ ✗
protected ✓ ✓ ✗Access specifiers are language mechanisms. Encapsulation is the broader design principle of deciding what a class should expose and what it should keep internal.
Getters and Setters
One common encapsulation technique is to keep a data member private and provide public member functions for controlled access.
Setter
- Receives a proposed value.
- Can validate the value.
- Can reject invalid changes.
- Can update internal state.
Getter
- Provides read access.
- Returns selected information.
- Can hide internal representation.
- Should usually avoid modifying the object.
class Student
{
private:
int marks = 0;
public:
void setMarks(int value)
{
if (value >= 0 && value <= 100)
{
marks = value;
}
}
int getMarks() const
{
return marks;
}
};| Function | Purpose |
|---|---|
| setMarks() | Request a change to marks |
| Validation | Check whether the new value is acceptable |
| marks | Store the protected state |
| getMarks() | Read the current value |
A getter that does not modify the object should usually be declared const, such as int getMarks() const.
If outside code should not be allowed to change a value freely, do not provide an unrestricted setter. Expose operations that represent valid behavior instead.
Example 1: Without Encapsulation
This example exposes the marks data member publicly, allowing outside code to store any integer value.
#include <iostream>
class Student
{
public:
int marks;
};
int main()
{
Student student1;
student1.marks = -50;
std::cout << student1.marks;
return 0;
}-50What Outside Code Can Do
- Read marks directly.
- Change marks directly.
- Store negative values.
- Store values above 100.
What the Class Can Control
- No validation opportunity.
- No rejection of invalid values.
- No central rule enforcement.
- No protection of the invariant.
Because marks is public, the class cannot guarantee that its value remains within the valid range of 0 to 100.
Example 2: With Encapsulation
The improved version keeps marks private and allows changes only through a public member function that validates the proposed value.
#include <iostream>
class Student
{
private:
int marks = 0;
public:
bool setMarks(int value)
{
if (value >= 0 && value <= 100)
{
marks = value;
return true;
}
return false;
}
int getMarks() const
{
return marks;
}
};
int main()
{
Student student1;
student1.setMarks(85);
if (!student1.setMarks(-50))
{
std::cout << "Invalid Marks!"
<< std::endl;
}
std::cout << student1.getMarks();
return 0;
}How the Program Works
- student1 is created with marks initialized to 0.
- setMarks(85) receives a valid value.
- The value 85 passes validation and is stored.
- setMarks(-50) receives an invalid value.
- The invalid value is rejected.
- The previous valid value remains unchanged.
- getMarks() returns 85.
Invalid Marks!
85Returning bool from a setter allows the caller to know whether the requested change succeeded without forcing the class to print messages directly.
Memory Representation
Private access does not mean that the data disappears from the object. The data still occupies storage as part of the object. The access restriction is enforced by the C++ language at compile time.
student1
┌──────────────────────┐
│ marks : 85 │
│ │
│ Access: private │
└──────────────────────┘
Outside code:
student1.marks ✗
Public interface:
student1.setMarks() ✓
student1.getMarks() ✓| Question | Answer |
|---|---|
| Does private data occupy memory? | Yes |
| Is private data encrypted? | No |
| Can unrelated outside code access it directly? | No |
| Who enforces the access rule? | The C++ language and compiler |
| Can public member functions use it? | Yes |
The private access specifier controls source-level access. It is not a security boundary against memory inspection, reverse engineering, or malicious code running in the same process.
Getter and Setter Functions Flow
Outside code interacts with the public interface instead of directly accessing the private data member.
Outside Code
│
│ setMarks(85)
▼
┌─────────────────────┐
│ setMarks() │
│ │
│ Is value valid? │
└──────────┬──────────┘
│ Yes
▼
┌─────────────────────┐
│ Private marks │
│ 85 │
└─────────────────────┘Outside Code
▲
│ returns 85
│
┌─────────────────────┐
│ getMarks() │
└──────────▲──────────┘
│ reads
│
┌─────────────────────┐
│ Private marks │
│ 85 │
└─────────────────────┘Write Path
- Receive proposed value.
- Check validation rules.
- Accept or reject the request.
- Update state only when valid.
Read Path
- Receive read request.
- Access internal state.
- Return selected information.
- Keep representation hidden.
Program Execution Flow
The complete program flow shows how encapsulation places validation between outside input and internal object state.
Program Starts
│
▼
Create student1
│
▼
setMarks(85)
│
▼
Is 85 between 0 and 100?
│
├── No ──► Reject
│
└── Yes
│
▼
Store 85
│
▼
getMarks()
│
▼
Return 85
│
▼
Display 85The public member function acts as a controlled boundary between outside requests and the object’s private state.
Advantages of Encapsulation
Data Protection
Prevents unrestricted direct access to internal state.
Data Integrity
Validation helps keep objects in valid states.
Implementation Hiding
Outside code depends on the interface instead of internal details.
Maintainability
Internal changes can often remain localized to the class.
Reusability
Well-designed interfaces make classes easier to reuse.
Testability
Clearly defined operations make class behavior easier to test.
- Controls access to object state.
- Helps prevent invalid modifications.
- Centralizes validation rules.
- Hides unnecessary implementation details.
- Reduces coupling between components.
- Makes internal changes easier to manage.
- Creates clearer public interfaces.
- Helps objects maintain their invariants.
One of the strongest benefits of encapsulation is that a class can protect conditions that should always remain true for valid objects.
Real-World Applications
Encapsulation is used wherever data must remain consistent and interactions should follow defined rules.
Banking
Balances change through deposit, withdrawal, and transfer operations instead of unrestricted assignment.
Hospital Systems
Patient records are accessed and updated through controlled operations.
Student Management
Marks, attendance, and academic state can be validated.
E-Commerce
Prices, stock quantities, and order state follow business rules.
Game Development
Health, score, inventory, and character state remain within valid limits.
Authentication
Credentials and authentication state are hidden behind controlled operations.
class BankAccount
{
private:
double balance = 0.0;
public:
bool deposit(double amount);
bool withdraw(double amount);
double getBalance() const;
};deposit() and withdraw() express valid banking behavior more clearly than a general setBalance() function.
Common Beginner Mistakes
Public data allows outside code to bypass the class’s rules.
Unrelated outside code cannot directly access private members.
An unrestricted setter may expose the same problem as a public data member.
A modification function should enforce the rules required by the object.
Private access is a C++ access-control mechanism, not encryption.
Returning mutable references or pointers can allow callers to bypass encapsulation.
// ❌ Weak encapsulation
class Employee
{
public:
double salary;
};// ✅ Better
class Employee
{
private:
double salary = 0.0;
public:
double getSalary() const
{
return salary;
}
};// ⚠️ This may not protect the object
void setBalance(double value)
{
balance = value;
}
// Better: expose valid operations
bool deposit(double amount);
bool withdraw(double amount);class Student
{
private:
int marks = 0;
public:
// ❌ Allows outside code to modify marks directly
int& getMarks()
{
return marks;
}
};Returning a mutable reference or pointer to private state may allow outside code to modify that state without passing through the class’s rules.
Best Practices
- Keep data members private by default unless there is a clear reason not to.
- Expose meaningful operations instead of unrestricted setters.
- Validate values before changing important object state.
- Use const member functions for read-only operations.
- Expose only the information and behavior callers actually need.
- Avoid returning mutable references or pointers to protected internal state.
- Keep invariants inside the class that owns the data.
- Use constructors to establish valid initial states.
- Prefer small and focused public interfaces.
- Keep implementation details separate from the public contract.
class Player
{
private:
int health = 100;
public:
void takeDamage(int amount)
{
if (amount <= 0)
{
return;
}
health -= amount;
if (health < 0)
{
health = 0;
}
}
int getHealth() const
{
return health;
}
};Weak Interface
- setHealth(any value)
- Public health variable
- No validation
- Rules spread across callers
Meaningful Interface
- takeDamage(amount)
- Private health state
- Rules enforced internally
- Class protects its invariant
A strong class interface often tells the object what operation to perform instead of exposing its internal state for callers to manipulate.
Frequently Asked Questions
What is encapsulation?
Encapsulation is the principle of grouping related state and behavior inside a class while controlling access to internal implementation details.
What are the three main access specifiers in C++?
They are public, private, and protected.
Which access specifier is commonly used for data members?
Private is commonly used when the class needs to control access to its internal state.
Can private members be accessed inside the same class?
Yes. Member functions of the class can access its private members.
Can private members be accessed directly from unrelated outside code?
No. Direct access from unrelated outside code is not allowed.
What is a getter?
A getter is a member function that provides read access to selected object state.
What is a setter?
A setter is a member function that requests a change to object state, often with validation.
Does every private member need a getter and setter?
No. A class should expose only the operations required by its design.
Is private data encrypted?
No. Private is a language-level access restriction, not encryption.
Why should getters often be const?
A const getter communicates and enforces that reading the value does not modify the observable state of the object.
What is an invariant?
An invariant is a condition that should remain true for every valid state of an object.
Is encapsulation the same as data hiding?
Data hiding is an important part of encapsulation, but encapsulation also includes designing controlled interfaces and keeping implementation details inside the responsible class.
Key Takeaways
- Encapsulation groups related state and behavior inside a class.
- It controls how outside code interacts with internal object state.
- C++ provides public, private, and protected access specifiers.
- Public members form the externally accessible interface.
- Private members cannot be accessed directly by unrelated outside code.
- Protected members are mainly relevant to inheritance.
- Getters provide controlled read access.
- Setters can provide controlled and validated write access.
- Not every private member needs a getter or setter.
- Meaningful operations are often better than unrestricted setters.
- Private data still occupies memory and is not encrypted.
- Encapsulation helps protect object invariants.
- Read-only member functions should often be declared const.
- Good encapsulation reduces coupling and improves maintainability.
Summary
Encapsulation is a fundamental object-oriented principle that allows a class to control access to its internal state and implementation.
C++ provides public, private, and protected access specifiers. Public members form the accessible interface, while private and protected members help hide implementation details from unrelated outside code.
Getters and setters are common techniques for controlled access, but good encapsulation is not simply about creating a getter and setter for every data member. A well-designed class exposes meaningful operations that preserve valid object state.
By centralizing validation and protecting invariants, encapsulation improves maintainability, reduces coupling, and makes classes easier to use correctly.
Now that you understand how a class can protect its internal state, the next lesson will introduce inheritance, which allows new classes to build upon existing classes.