Module 02: Encapsulation & Abstraction
Goal: Understand data hiding, access control, abstract classes, and interfaces. Time: 2 days of focused study Prerequisites: Module 01
Table of Contents
- Encapsulation — What and Why
- Access Modifiers Deep Dive
- Getters and Setters
- The
friendKeyword - Abstraction — What and Why
- Abstract Classes & Pure Virtual Functions
- Interfaces in C++
- Encapsulation vs Abstraction
- Real-World Examples
- Common Mistakes
- Practice Problems
- Interview Questions
1. Encapsulation
What Is Encapsulation?
Encapsulation is bundling data and methods that operate on that data into a single unit (class) and restricting direct access to the internal state.
Real-World Analogy: A CAPSULE (medicine pill)
The medicine (data) is INSIDE the capsule.
You can't see or touch the chemicals directly.
You only interact through the prescribed interface (swallow the pill).
The capsule PROTECTS the contents and controls HOW they're used.
Another: ATM MACHINE
Internal state: cash reserves, user accounts, transaction logs
Interface: screen, keypad, card slot
You can't reach into the machine and grab cash.
You MUST use the interface: insert card → enter PIN → select amount.
The machine validates everything internally.Without Encapsulation (Bad)
// ❌ No encapsulation — data is public
struct BankAccount {
string owner;
double balance;
};
int main() {
BankAccount acc;
acc.owner = "Alice";
acc.balance = 1000;
acc.balance = -99999; // ← Anyone can set negative balance!
acc.balance += 1000000; // ← Anyone can give themselves money!
// NO validation, NO audit trail, NO control
}With Encapsulation (Good)
// ✅ Encapsulated — data is private, controlled through methods
class BankAccount {
private:
string owner;
double balance;
vector<string> transactionLog;
void log(const string& msg) {
transactionLog.push_back(msg);
}
public:
BankAccount(const string& name, double initial)
: owner(name), balance(initial) {
log("Account created with $" + to_string(initial));
}
bool deposit(double amount) {
if (amount <= 0) {
cout << "Error: Deposit must be positive" << endl;
return false;
}
balance += amount;
log("Deposited $" + to_string(amount));
return true;
}
bool withdraw(double amount) {
if (amount <= 0) {
cout << "Error: Withdrawal must be positive" << endl;
return false;
}
if (amount > balance) {
cout << "Error: Insufficient funds" << endl;
return false;
}
balance -= amount;
log("Withdrew $" + to_string(amount));
return true;
}
double getBalance() const { return balance; }
string getOwner() const { return owner; }
void printStatement() const {
cout << "=== Statement for " << owner << " ===" << endl;
for (const auto& entry : transactionLog) {
cout << " " << entry << endl;
}
cout << "Balance: $" << balance << endl;
}
};
int main() {
BankAccount acc("Alice", 1000);
acc.deposit(500); // ✅ Validated
acc.withdraw(200); // ✅ Validated
acc.withdraw(9999); // ❌ "Insufficient funds"
acc.deposit(-100); // ❌ "Must be positive"
// acc.balance = -99; // ❌ COMPILE ERROR — private!
acc.printStatement();
}Benefits of Encapsulation
1. DATA PROTECTION — Invalid states are impossible
2. CONTROLLED ACCESS — Validation in getters/setters
3. IMPLEMENTATION HIDING — Can change internals without breaking callers
4. AUDIT TRAIL — All changes go through controlled methods
5. DEBUGGING — Bugs are localized (only class methods can modify data)
6. THREAD SAFETY — Can add locking inside methods without changing interface2. Access Modifiers Deep Dive
Three Levels
class Example {
public: // Accessible from everywhere
void publicMethod() {}
protected: // Accessible from this class AND derived classes
void protectedMethod() {}
private: // Accessible ONLY from this class
void privateMethod() {}
};
class Derived : public Example {
void test() {
publicMethod(); // ✅
protectedMethod(); // ✅ Accessible in derived class
// privateMethod(); // ❌ Not accessible in derived class
}
};
int main() {
Example obj;
obj.publicMethod(); // ✅
// obj.protectedMethod(); // ❌
// obj.privateMethod(); // ❌
}Access Modifier and Inheritance
class Base {
public:
int pub = 1;
protected:
int prot = 2;
private:
int priv = 3;
};
// PUBLIC inheritance: everything stays as-is
class PubDerived : public Base {
// pub → public
// prot → protected
// priv → NOT accessible
};
// PROTECTED inheritance: public becomes protected
class ProtDerived : protected Base {
// pub → protected
// prot → protected
// priv → NOT accessible
};
// PRIVATE inheritance: everything becomes private
class PrivDerived : private Base {
// pub → private
// prot → private
// priv → NOT accessible
};Inheritance Type │ Public member │ Protected member │ Private member
─────────────────┼───────────────┼──────────────────┼───────────────
public │ public │ protected │ not accessible
protected │ protected │ protected │ not accessible
private │ private │ private │ not accessibleJava equivalent: Java only has
publicinheritance (extends). Noprotectedorprivateinheritance.
3. Getters and Setters
Basic Pattern
class Temperature {
private:
double celsius;
public:
// Getter — read access
double getCelsius() const { return celsius; }
// Setter — write access with VALIDATION
void setCelsius(double value) {
if (value < -273.15) {
throw invalid_argument("Temperature below absolute zero!");
}
celsius = value;
}
// Computed property (derived from internal state)
double getFahrenheit() const { return celsius * 9.0 / 5.0 + 32; }
void setFahrenheit(double f) {
setCelsius((f - 32) * 5.0 / 9.0); // Reuse validation
}
};When To Use / Not Use Getters and Setters
✅ USE when:
- You need validation (range checks, format checks)
- The value is computed/derived
- You might change the internal representation later
- You need side effects (logging, notifications, caching)
- You want read-only access (getter only, no setter)
❌ DON'T blindly add getters/setters for every field:
- A class with getters and setters for ALL fields is just a struct
with extra steps — you're not actually encapsulating anything!
- Ask: "Does the outside world NEED to know/change this?"
❌ BAD — Anemic class (no real encapsulation):
class User {
string name;
string email;
public:
string getName() { return name; }
void setName(string n) { name = n; }
string getEmail() { return email; }
void setEmail(string e) { email = e; }
// This is just a struct with extra steps!
};
✅ GOOD — Meaningful behavior:
class User {
string name;
string email;
bool verified = false;
public:
string getName() const { return name; } // Read-only is fine
void changeEmail(string newEmail) {
if (!isValidEmail(newEmail)) throw invalid_argument("Bad email");
email = newEmail;
verified = false; // Changing email requires re-verification
sendVerificationEmail(newEmail);
}
};4. The friend Keyword
friend grants a function or class access to private/protected members.
class Wallet {
private:
double money;
public:
Wallet(double m) : money(m) {}
// Friend function — can access private members
friend void audit(const Wallet& w);
// Friend class — ALL methods of Bank can access Wallet's privates
friend class Bank;
// Friend member function of specific class
friend void Inspector::inspect(const Wallet& w);
};
// This function is NOT a member of Wallet, but can access its privates
void audit(const Wallet& w) {
cout << "Audit: Wallet contains $" << w.money << endl; // ✅ Direct access!
}
class Bank {
public:
void checkBalance(const Wallet& w) {
cout << "Balance: $" << w.money << endl; // ✅ Access via friendship
}
};Operator Overloading with friend
class Vector2D {
double x, y;
public:
Vector2D(double x, double y) : x(x), y(y) {}
// Friend allows operator<< to access private members
friend ostream& operator<<(ostream& os, const Vector2D& v) {
os << "(" << v.x << ", " << v.y << ")";
return os;
}
// Friend for commutative operator: 3 * vec (not vec * 3)
friend Vector2D operator*(double scalar, const Vector2D& v) {
return Vector2D(scalar * v.x, scalar * v.y);
}
};
int main() {
Vector2D v(3, 4);
cout << v << endl; // (3, 4) — uses friend operator<<
Vector2D v2 = 2.0 * v; // (6, 8) — uses friend operator*
}When to Use friend
✅ USE friend for:
- Operator overloading (<<, >>, binary operators)
- Tightly coupled classes that are part of the same abstraction
- Test classes that need to verify internal state
❌ AVOID friend when:
- You're using it because getters/setters seem tedious
- It's used across unrelated classes
- It breaks encapsulation without good reason
friend is NOT "breaking" encapsulation — it's EXTENDING it.
The class EXPLICITLY grants access. It's a controlled decision.5. Abstraction
What Is Abstraction?
Abstraction means showing only the essential details and hiding the complexity.
Real-World Analogy: DRIVING A CAR
What you see (abstraction):
Steering wheel, pedals, gear shift, dashboard
What's hidden (implementation):
Engine combustion, fuel injection, transmission gears,
electronic control units, coolant system
You don't need to understand HOW the engine works to DRIVE.
The car ABSTRACTS away the complexity.
Programming:
What the user sees: sort(arr);
What's hidden: Quicksort with 3-way partition, insertion sort
for small arrays, introsort fallbackAbstraction in Code
// ---- WITHOUT ABSTRACTION ----
// The user must know ALL the details to send an email
void sendEmail() {
int socket = createSocket(AF_INET, SOCK_STREAM, 0);
connect(socket, smtpServer, port);
send(socket, "HELO myserver\r\n");
recv(socket, buffer, 1024);
send(socket, "MAIL FROM:<sender@test.com>\r\n");
recv(socket, buffer, 1024);
send(socket, "RCPT TO:<recipient@test.com>\r\n");
recv(socket, buffer, 1024);
send(socket, "DATA\r\n");
send(socket, "Subject: Hello\r\n\r\nBody text\r\n.\r\n");
close(socket);
}
// ---- WITH ABSTRACTION ----
// The user just calls simple methods
class EmailService {
public:
void send(const string& to, const string& subject, const string& body) {
// All SMTP complexity is hidden inside
}
};
EmailService email;
email.send("bob@test.com", "Hello", "Hi Bob!");
// Simple! The user doesn't need to know about sockets, SMTP, etc.6. Abstract Classes & Pure Virtual Functions
An abstract class is a class that cannot be instantiated — it exists only as a base for other classes.
// PURE VIRTUAL FUNCTION: = 0 at the end
// Makes the class abstract — can't create objects of this class
class Shape {
protected:
string color;
public:
Shape(string c) : color(c) {}
// Pure virtual functions — MUST be overridden by derived classes
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual void draw() const = 0;
// Regular virtual function — CAN be overridden (has default)
virtual string getInfo() const {
return color + " shape";
}
// Non-virtual function — NOT meant to be overridden
string getColor() const { return color; }
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
Circle(string color, double r) : Shape(color), radius(r) {}
// MUST implement ALL pure virtual functions
double area() const override { return 3.14159 * radius * radius; }
double perimeter() const override { return 2 * 3.14159 * radius; }
void draw() const override { cout << "Drawing circle with radius " << radius << endl; }
};
class Rectangle : public Shape {
double width, height;
public:
Rectangle(string color, double w, double h) : Shape(color), width(w), height(h) {}
double area() const override { return width * height; }
double perimeter() const override { return 2 * (width + height); }
void draw() const override { cout << "Drawing " << width << "x" << height << " rect" << endl; }
};
int main() {
// Shape s("red"); // ❌ ERROR! Can't instantiate abstract class
Circle c("red", 5);
Rectangle r("blue", 4, 6);
// Polymorphism — treat all shapes uniformly
vector<Shape*> shapes = {&c, &r};
for (const auto* shape : shapes) {
shape->draw();
cout << "Area: " << shape->area() << endl;
}
}Abstract Class Rules
1. A class with at least ONE pure virtual function is abstract
2. Abstract classes CANNOT be instantiated
3. Derived classes MUST implement ALL pure virtual functions
(or they become abstract too!)
4. Abstract classes CAN have:
- Constructors (used by derived constructors)
- Non-pure virtual functions (with default implementations)
- Non-virtual functions
- Data members
- Static members
5. You CAN have pointers/references to abstract classes (polymorphism)7. Interfaces in C++
C++ doesn't have a dedicated interface keyword. An interface is an abstract class with ONLY pure virtual functions and no data members.
// INTERFACE — pure contract, no implementation
class ISerializable {
public:
virtual string serialize() const = 0;
virtual void deserialize(const string& data) = 0;
virtual ~ISerializable() = default;
};
class IPrintable {
public:
virtual void print(ostream& os) const = 0;
virtual ~IPrintable() = default;
};
class IComparable {
public:
virtual int compareTo(const IComparable& other) const = 0;
virtual ~IComparable() = default;
};
// A class can "implement" multiple interfaces
class User : public ISerializable, public IPrintable, public IComparable {
string name;
int age;
public:
User(string n, int a) : name(n), age(a) {}
// Implement ISerializable
string serialize() const override {
return name + ":" + to_string(age);
}
void deserialize(const string& data) override {
auto pos = data.find(':');
name = data.substr(0, pos);
age = stoi(data.substr(pos + 1));
}
// Implement IPrintable
void print(ostream& os) const override {
os << "User(" << name << ", " << age << ")";
}
// Implement IComparable
int compareTo(const IComparable& other) const override {
const User& u = dynamic_cast<const User&>(other);
return name.compare(u.name);
}
};Interface vs Abstract Class
Feature │ Interface │ Abstract Class
─────────────────────┼────────────────────────┼────────────────────────
Pure virtual funcs │ ALL (only pure virtual)│ At least ONE
Data members │ None │ Can have
Constructors │ None (usually) │ Can have
Implementation │ None │ Can have some methods
Purpose │ Define a CONTRACT │ Define a TEMPLATE
Multiple inheritance │ ✅ Safe (no data) │ ⚠️ Can cause diamondJava equivalent: Java has explicit
interfacekeyword withimplements. From Java 8, interfaces can havedefaultmethods (like C++ non-pure virtual functions).
8. Encapsulation vs Abstraction
These are DIFFERENT concepts but often confused:
ENCAPSULATION: ABSTRACTION:
"How do we HIDE the data?" "What do we SHOW to the user?"
Binding data + methods together Hiding complexity
Controlling access (private/public) Showing only relevant details
Implementation mechanism Design philosophy
EXAMPLE — A TV:
Encapsulation: The circuit board Abstraction: The remote control
is SEALED inside the case. You has only a few buttons: power,
can't touch the wires. volume, channel. You don't see
signal processing or decoding.
EXAMPLE — A class:
Encapsulation: Making data `private` Abstraction: Providing public methods
and accessing through methods. like deposit()/withdraw() instead of
exposing internal transaction logic.
SUMMARY:
Encapsulation = HOW you hide (access modifiers, classes)
Abstraction = WHAT you hide (complexity, unnecessary details)
Encapsulation is SUBSET of Abstraction.
You can have abstraction without encapsulation (e.g., functions that
abstract away complexity without classes).9. Real-World Examples
Database Connection Pool
// Abstraction: User doesn't know how connections are pooled
// Encapsulation: Pool internals are private
class IDatabase {
public:
virtual bool execute(const string& query) = 0;
virtual vector<map<string, string>> fetch(const string& query) = 0;
virtual ~IDatabase() = default;
};
class ConnectionPool : public IDatabase {
private:
vector<Connection*> available;
vector<Connection*> inUse;
string connectionString;
int maxConnections;
mutex poolMutex;
Connection* getConnection() {
lock_guard<mutex> lock(poolMutex);
if (available.empty()) {
if (inUse.size() < maxConnections) {
auto* conn = new Connection(connectionString);
inUse.push_back(conn);
return conn;
}
throw runtime_error("Pool exhausted");
}
auto* conn = available.back();
available.pop_back();
inUse.push_back(conn);
return conn;
}
void releaseConnection(Connection* conn) {
lock_guard<mutex> lock(poolMutex);
inUse.erase(remove(inUse.begin(), inUse.end(), conn), inUse.end());
available.push_back(conn);
}
public:
ConnectionPool(const string& connStr, int maxConn)
: connectionString(connStr), maxConnections(maxConn) {}
bool execute(const string& query) override {
auto* conn = getConnection();
bool result = conn->run(query);
releaseConnection(conn);
return result;
}
// User just calls: pool.execute("INSERT INTO users ...");
// No idea about pooling, thread safety, connection management!
};Logger with Multiple Outputs
// Interface for log destinations
class ILogDestination {
public:
virtual void write(const string& level, const string& message) = 0;
virtual ~ILogDestination() = default;
};
class ConsoleLogger : public ILogDestination {
public:
void write(const string& level, const string& message) override {
cout << "[" << level << "] " << message << endl;
}
};
class FileLogger : public ILogDestination {
ofstream file;
public:
FileLogger(const string& filename) : file(filename, ios::app) {}
void write(const string& level, const string& message) override {
file << "[" << level << "] " << message << endl;
}
};
// Encapsulated logger — users don't know about destinations
class Logger {
vector<unique_ptr<ILogDestination>> destinations;
public:
void addDestination(unique_ptr<ILogDestination> dest) {
destinations.push_back(move(dest));
}
void info(const string& msg) { log("INFO", msg); }
void error(const string& msg) { log("ERROR", msg); }
void warn(const string& msg) { log("WARN", msg); }
private:
void log(const string& level, const string& msg) {
for (auto& dest : destinations) {
dest->write(level, msg);
}
}
};10. Common Mistakes
// ❌ MISTAKE 1: Making everything public
class User {
public:
string password; // NEVER expose sensitive data!
};
// ❌ MISTAKE 2: Anemic getters/setters with no validation
class Age {
int age;
public:
int getAge() { return age; }
void setAge(int a) { age = a; } // No validation — accepts -5 or 999!
};
// ❌ MISTAKE 3: Overusing friend
class A {
friend class B;
friend class C;
friend class D;
// If everything is a friend, nothing is private!
};
// ❌ MISTAKE 4: Not overriding ALL pure virtual functions
class Animal {
public:
virtual void speak() = 0;
virtual void move() = 0;
};
class Dog : public Animal {
void speak() override { cout << "Woof"; }
// Forgot move() — Dog is STILL abstract! Can't instantiate!
};
// ❌ MISTAKE 5: Confusing abstract class with interface
class ILogger {
string logFile; // ← DATA MEMBER! This is NOT an interface anymore!
public:
virtual void log(string msg) = 0;
};11. Practice Problems
Problem 1: Encapsulated Stack
// Implement a Stack class that:
// - Uses a private dynamic array
// - Has push, pop, peek, isEmpty, size methods
// - Validates all operations (can't pop empty stack)
// - Auto-resizes when full
class Stack {
int* data;
int topIndex;
int capacity;
void resize() {
capacity *= 2;
int* newData = new int[capacity];
for (int i = 0; i <= topIndex; i++) newData[i] = data[i];
delete[] data;
data = newData;
}
public:
Stack(int cap = 10) : capacity(cap), topIndex(-1) {
data = new int[capacity];
}
~Stack() { delete[] data; }
void push(int val) {
if (topIndex + 1 >= capacity) resize();
data[++topIndex] = val;
}
int pop() {
if (isEmpty()) throw underflow_error("Stack is empty!");
return data[topIndex--];
}
int peek() const {
if (isEmpty()) throw underflow_error("Stack is empty!");
return data[topIndex];
}
bool isEmpty() const { return topIndex < 0; }
int size() const { return topIndex + 1; }
};Problem 2: Shape Hierarchy with Interface
// Create an IDrawable interface and implement it for multiple shapes
// Each shape should calculate area and perimeter
class IDrawable {
public:
virtual void draw() const = 0;
virtual ~IDrawable() = default;
};
class IMeasurable {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual ~IMeasurable() = default;
};
class Triangle : public IDrawable, public IMeasurable {
double a, b, c;
public:
Triangle(double a, double b, double c) : a(a), b(b), c(c) {}
void draw() const override {
cout << "Drawing triangle with sides " << a << ", " << b << ", " << c << endl;
}
double area() const override {
double s = (a + b + c) / 2;
return sqrt(s * (s-a) * (s-b) * (s-c));
}
double perimeter() const override { return a + b + c; }
};12. Interview Questions
Q1: What is encapsulation? How is it achieved in C++?
Answer: Encapsulation bundles data and methods into a class and restricts direct access to internal state using access modifiers (private, protected, public). Data is made private, and controlled access is provided through public methods (getters, setters, or domain-specific methods like deposit()). This protects data integrity, enables validation, and allows changing internal implementation without affecting external code.
Q2: What is the difference between encapsulation and abstraction?
Answer: Encapsulation is about how you hide — it's the mechanism of bundling data and methods and restricting access via access modifiers. Abstraction is about what you hide — it's the design philosophy of exposing only relevant details and hiding complexity. Encapsulation is achieved through classes and access modifiers. Abstraction is achieved through abstract classes, interfaces, and well-designed APIs. Encapsulation is a tool for achieving abstraction.
Q3: What is an abstract class? Can it have a constructor?
Answer: An abstract class has at least one pure virtual function (= 0) and cannot be instantiated. YES, it can have a constructor — it's called by derived class constructors to initialize base members. It can also have data members, non-pure virtual functions, and non-virtual functions. Use abstract classes when you want to provide a common base with some shared implementation.
Q4: What is the difference between an abstract class and an interface?
Answer: In C++, both are implemented using abstract classes, but conceptually: an interface has ONLY pure virtual functions and no data members — it defines a pure contract. An abstract class can have data, constructors, and partial implementation — it defines a template. A class can implement multiple interfaces safely but inheriting multiple abstract classes risks the diamond problem.
Q5: What is the friend keyword? Does it break encapsulation?
Answer: friend grants a function or class access to private/protected members. It does NOT break encapsulation — the class explicitly and deliberately grants access. The friendship is one-way and cannot be inherited. Use it for: operator overloading (<<, >>), tightly coupled classes, and testing. Avoid overuse — if everything is a friend, nothing is really private.
Q6: What is a pure virtual function?
Answer: A function declared with = 0 in the base class: virtual void func() = 0;. It has no implementation in the base class and MUST be overridden by derived classes. A class with even one pure virtual function becomes abstract and cannot be instantiated. It defines a contract that all derived classes must fulfill.
Q7: Can we have a pointer/reference to an abstract class?
Answer: Yes! You can't create an object of an abstract class, but you CAN have pointers or references to it. This is the foundation of polymorphism: Shape* s = new Circle(5);. The pointer type is the abstract base class, but it points to a concrete derived class object.
Q8: What is data hiding? How is it different from encapsulation?
Answer: Data hiding is making class members inaccessible from outside (using private). It's a part of encapsulation. Encapsulation is the broader concept — bundling data and methods AND controlling access. Data hiding is the access-restriction aspect of encapsulation.
Q9: Why should we make data members private?
Answer: (1) Prevents invalid states (validation in setters), (2) Enables changing internal representation without breaking external code, (3) Allows adding side effects (logging, notifications) to access, (4) Makes debugging easier (only class methods can modify data), (5) Enables thread-safe access (mutex in methods).
Q10: Can an abstract class have non-virtual methods?
Answer: Yes. An abstract class can have any combination of: pure virtual functions (must override), virtual functions (can override, has default), and non-virtual functions (not meant to be overridden). Non-virtual methods in an abstract class provide shared behavior that all derived classes inherit as-is.
Q11: What happens if a derived class doesn't implement all pure virtual functions?
Answer: The derived class also becomes abstract and cannot be instantiated. This is sometimes intentional — to create a hierarchy of abstract classes where each level implements some functions but leaves others for further derivation.
Q12: Explain access modifiers with inheritance in C++.
Answer: With public inheritance, public stays public and protected stays protected. With protected inheritance, public becomes protected. With private inheritance, everything becomes private. In all cases, private members of the base are NEVER accessible in derived classes (they exist in memory but can't be accessed directly). Most inheritance is public. Private inheritance models "implemented-in-terms-of" (not "is-a").
Next Module: 03 - Inheritance — Types of inheritance, diamond problem, virtual inheritance, and method overriding.