Skip to content

Encapsulation — Public, Private, Protected, Friends, and Access Control

DodaTech Updated 2026-06-28 7 min read

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

  1. Make data members private by default
  2. Provide public member functions as the interface
  3. Use protected for members that derived classes need but external code should not touch
  4. Use friend declarations sparingly — they increase coupling
  5. Prefer getters/setters with invariants over public data
  6. 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

  1. What is the difference between private and protected?
  2. When would you use a friend function instead of a member function?
  3. Why might returning a const reference to a private member still be problematic?
  4. Implement a Logger class with a private file handle and a friend logMessage function.
  5. 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

Does encapsulation affect performance?

No. Access specifiers are checked at compile time and have zero runtime cost. Private and public members generate the same machine code.

Can I friend a function template?

Yes. template<typename T> friend void func(T); makes all instantiations of the template friends.

Is it bad to use friend declarations?

Not inherently. Friends are a controlled mechanism for breaking encapsulation. They are necessary for operator overloading and certain design patterns (Visitor, Bridge).

How does encapsulation differ between C++ and Java?

In C++, you control per-member access. In Java, you control per-member and also have package-private access. C++ uses friend declarations; Java does not have friends.

Can I change access for individual members in a derived class?

Yes, with a using declaration: using Base::protected_member; in the public section of the derived class changes its access.

What is the 'narrow contract' versus 'wide contract' in encapsulation?

A narrow contract requires inputs to meet preconditions (caller responsibility). A wide contract handles all inputs gracefully. Encapsulation supports both by validating inputs in setters.

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