Encapsulation — Public, Private, Protected, Friends, and Access Control
In this tutorial, you will learn about Encapsulation. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ encapsulation enforces data hiding through access specifiers, with friend declarations providing controlled exceptions to the encapsulation boundary for specific functions or classes.
What You'll Learn
You will apply public, private, and protected access specifiers to control visibility, write friend functions and friend classes that access private members, understand the difference between class and struct defaults, design interfaces that hide implementation details, and compare the C++ approach to encapsulation with Java and C.
Why It Matters
Encapsulation is the fundamental principle of object-oriented design: an object's internal state should only be modified through its public interface. This decouples the implementation from the usage, allowing you to change internals without affecting code that uses the class. Friend declarations seem to break encapsulation, but they actually preserve it by keeping the breach explicit and controlled.
Learning Path
graph LR
A["13: Destructors"] --> B["14: Encapsulation"]
B --> C["15: Inheritance"]
C --> D["16: Polymorphism"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Access Specifiers
#include <iostream>
#include <string>
class BankAccount {
private:
std::string accountNumber_;
double balance_;
protected:
double getBalanceForInheritance() const {
return balance_; // accessible in derived classes
}
public:
BankAccount(const std::string& accNum, double initialBalance)
: accountNumber_(accNum), balance_(initialBalance) {}
void deposit(double amount) {
if (amount > 0) balance_ += amount;
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance_) {
balance_ -= amount;
return true;
}
return false;
}
double getBalance() const {
return balance_;
}
};
int main() {
BankAccount acc("12345", 1000);
acc.deposit(500);
acc.withdraw(200);
std::cout << "Balance: " << acc.getBalance() << "\n";
// acc.balance_ = 0; // Error: private
// std::cout << acc.accountNumber_; // Error: private
}
Getter/Setter Patterns
class Temperature {
private:
double celsius_;
public:
void setCelsius(double c) {
celsius_ = c;
}
void setFahrenheit(double f) {
celsius_ = (f - 32.0) * 5.0 / 9.0;
}
double getCelsius() const {
return celsius_;
}
double getFahrenheit() const {
return celsius_ * 9.0 / 5.0 + 32.0;
}
};
int main() {
Temperature t;
t.setCelsius(100);
std::cout << t.getFahrenheit() << "\n"; // 212
t.setFahrenheit(32);
std::cout << t.getCelsius() << "\n"; // 0
}
Getters and setters provide controlled access while maintaining encapsulation. The internal representation (celsius_) can change (e.g., to Kelvin) without affecting users.
Friend Functions
A friend function is a non-member function that can access private and protected members.
#include <iostream>
#include <cmath>
class Complex {
private:
double real_;
double imag_;
public:
Complex(double r, double i) : real_(r), imag_(i) {}
// Declare a non-member function as a friend
friend double magnitude(const Complex& c);
// Friend operator
friend Complex operator+(const Complex& a, const Complex& b);
void print() const {
std::cout << real_ << " + " << imag_ << "i\n";
}
};
double magnitude(const Complex& c) {
return std::sqrt(c.real_ * c.real_ + c.imag_ * c.imag_);
}
Complex operator+(const Complex& a, const Complex& b) {
return Complex(a.real_ + b.real_, a.imag_ + b.imag_);
}
int main() {
Complex c1(3.0, 4.0);
Complex c2(1.0, 2.0);
std::cout << magnitude(c1) << "\n"; // 5
Complex sum = c1 + c2;
sum.print(); // 4 + 6i
}
Friend Classes
#include <iostream>
class Engine {
private:
bool running_;
int rpm_;
friend class Car; // Car can access Engine's private members
public:
Engine() : running_(false), rpm_(0) {}
};
class Car {
private:
Engine engine_;
public:
void start() {
engine_.running_ = true;
engine_.rpm_ = 800;
std::cout << "Car started\n";
}
void accelerate() {
if (engine_.running_) {
engine_.rpm_ += 500;
std::cout << "RPM: " << engine_.rpm_ << "\n";
}
}
};
int main() {
Car car;
car.start();
car.accelerate();
car.accelerate();
}
Friend classes are useful when two classes are tightly coupled, like a container and its Iterator, or a Builder and its product.
protected Access
#include <iostream>
class Base {
private:
int private_ = 1;
protected:
int protected_ = 2;
public:
int public_ = 3;
};
class Derived : public Base {
public:
void show() {
// std::cout << private_; // Error: not accessible
std::cout << protected_ << "\n"; // OK: 2
std::cout << public_ << "\n"; // OK: 3
}
};
int main() {
Base b;
// std::cout << b.private_; // Error
// std::cout << b.protected_; // Error
std::cout << b.public_ << "\n"; // OK: 3
Derived d;
d.show();
}
protected is between private and public: accessible in the class and its derived classes, but not from outside.
Encapsulation Design Guidelines
- Make data members
privateby default - Provide
publicmember functions as the interface - Use
protectedfor members that derived classes need but external code should not touch - Use friend declarations sparingly — they increase coupling
- Prefer getters/setters with invariants over public data
- Consider the Pimpl idiom (pointer to implementation) for hiding implementation details
Pimpl Idiom
// widget.h
#include <memory>
class Widget {
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
public:
Widget();
~Widget();
void doSomething();
};
// widget.cpp
#include "widget.h"
#include <iostream>
struct Widget::Impl {
std::string secret;
void internalLogic() {
std::cout << "Hidden from header\n";
}
};
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
void Widget::doSomething() { pImpl->internalLogic(); }
The Pimpl idiom hides the implementation completely, reducing compile-time dependencies and protecting implementation details.
Common Mistakes
Mistake 1: Making Everything Public
class Point {
public: // BAD: no encapsulation
int x;
int y;
};
This is acceptable for plain data aggregates (structs) but not for classes with invariants.
Mistake 2: Overusing Friend Declarations
Friendship breaks encapsulation. Use public interfaces first; resort to friends only when necessary (e.g., operator overloading).
Mistake 3: Forgetting that Friendship is Not Inherited
If Base declares a friend, Derived does not inherit that friendship. Each class must declare its own friends.
Mistake 4: Protected Data Members
Protected data members are almost as bad as public ones. They create coupling between base and derived classes. Prefer protected member functions and private data.
Mistake 5: Returning Non-const References to Private Data
class Bad {
private:
std::vector<int> data_;
public:
std::vector<int>& getData() { return data_; } // exposes internals
};
Return a const reference or a copy.
Practice Questions
- What is the difference between
privateandprotected? - When would you use a friend function instead of a member function?
- Why might returning a const reference to a private member still be problematic?
- Implement a
Loggerclass with a private file handle and a friendlogMessagefunction. - What is the Pimpl idiom and what problem does it solve?
Challenge
Design a Matrix class with private data (a 2D array of doubles) and public operations (add, multiply, transpose). Make the operator<< for output a friend function. Ensure proper encapsulation.
FAQ
Mini Project
Build a SecureVault class:
#include <iostream>
#include <string>
class Vault {
private:
std::string secret_;
int accessCode_;
bool authenticate(int code) const {
return code == accessCode_;
}
friend class VaultManager;
friend void emergencyReset(Vault& v, int newCode);
public:
Vault(const std::string& secret, int code)
: secret_(secret), accessCode_(code) {}
std::string getSecret(int code) const {
if (authenticate(code)) return secret_;
return "ACCESS DENIED";
}
};
class VaultManager {
public:
void resetSecret(Vault& v, const std::string& newSecret) {
v.secret_ = newSecret;
}
};
void emergencyReset(Vault& v, int newCode) {
v.accessCode_ = newCode;
v.secret_ = "RESET";
}
int main() {
Vault v("My secret data", 1234);
std::cout << v.getSecret(1234) << "\n";
std::cout << v.getSecret(0000) << "\n";
VaultManager mgr;
mgr.resetSecret(v, "New secret");
std::cout << v.getSecret(1234) << "\n";
emergencyReset(v, 9999);
std::cout << v.getSecret(1234) << "\n";
std::cout << v.getSecret(9999) << "\n";
}
What's Next
Encapsulation protects internal state. The next lesson covers inheritance: creating derived classes, controlling access inheritance, and virtual base classes for resolving ambiguities.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro