Access Specifiers in C++: Public, Private, Protected

Learning C++ for the first time? This guide explains Access Specifiers in C++ with simple analogies, clean code, and “Try it yourself” challenges after every section to build confidence step by step. Think of access specifiers like doors and keys in a house: some rooms are open to everyone, some are private, and some are for family only.

In short, Access Specifiers in C++ control who can see and use the data and functions inside a class. Using them wisely protects data (encapsulation), keeps code clean, and prevents misuse.

🔹 What Are Access Specifiers in C++?

C++ provides three access specifiers: public, private, and protected. They define visibility of class members (attributes and methods) from outside the class and inside derived (child) classes. The analogy: public is a front door anyone can use, private is a locked bedroom, and protected is a family‑only room that children (derived classes) can enter.

  • public: Accessible from anywhere (outside code, inside the class, and in derived classes).
  • private: Accessible only inside the class that declares it.
  • protected: Accessible inside the class and its derived classes, but not from unrelated outside code.

Try it yourself

  • Write a small list of “house areas” and classify them as public, private, or protected using the analogy above.
  • Explain in one sentence when each specifier should be used in a program (e.g., “settings should be private because…”).

🔹 Syntax and Defaults: class vs struct

In C++, members of a class are private by default, while members of a struct are public by default. The specifiers can be used multiple times in the same class to group related members under different visibility.

#include <iostream>
using namespace std;
// Default access in class: private
class A {
    int x; // private by default
public:
    void setX(int v) { x = v; }
    int getX() const { return x; }
};
// Default access in struct: public
struct B {
    int y; // public by default
    void setY(int v) { y = v; }
};
int main() {
    A a;
    a.setX(10); // OK via public method
    cout << "a.x = " << a.getX() << "\n";
    B b;
    b.y = 20; // OK: y is public by default
    b.setY(30); // Also OK
    cout << "b.y = " << b.y << "\n";
}

Output

a.x = 10
b.y = 30

Try it yourself

  • Create a class with a private integer and a public setter. Try accessing the integer directly from main() and observe the compiler error.
  • Convert that class to a struct and note how default access changes. Then explicitly mark the member as private and re‑test.

🔹 The public Specifier: Open Interface

Use public to define the minimal, safe interface others can call. Keep data private when possible, and make only well‑named, validated functions public. This leads to clean, self‑documenting APIs.

#include <iostream>
using namespace std;
class StopWatch {
public:
    void start() { running = true; }
    void stop() { running = false; }
    bool isRunning() const { return running; }
private:
    bool running{false}; // hidden implementation detail
};
int main() {
    StopWatch sw;
    sw.start();
    cout << boolalpha << sw.isRunning() << "\n"; // true
    sw.stop();
    cout << boolalpha << sw.isRunning() << "\n"; // false
}

Output

true
false

Here, only the behavior (start/stop/check) is public. The internal state (running) stays private, preventing accidental misuse from outside code.

Try it yourself

  • Add a reset() method and decide whether it should be public or private. Justify the choice.
  • Expose a public method toggle() that flips the running state and test it.

🔹 The private Specifier: True Data Hiding

Use private to protect data and enforce invariants through validated methods. This is core to encapsulation: keep raw data hidden and allow controlled access via functions that check inputs and maintain consistency.

#include <iostream>
#include <stdexcept>
using namespace std;
class BankAccount {
public:
    explicit BankAccount(double initial) {
        if (initial < 0) 
        throw invalid_argument("Initial balance cannot be negative");
        balance = initial;
    }
    void deposit(double amount) {
        if (amount <= 0) 
        throw invalid_argument("Deposit must be positive");
        balance += amount;
    }
    void withdraw(double amount) {
        if (amount <= 0) 
        throw invalid_argument("Withdrawal must be positive");
        if (amount > balance) 
        throw invalid_argument("Insufficient funds");
        balance -= amount;
    }
    double getBalance() const { return balance; }
private:
    double balance{0.0}; // private data: not directly accessible
};
int main() {
    BankAccount acc(200.0);
    acc.deposit(50.0);
    acc.withdraw(25.0);
    cout << "Balance: " << acc.getBalance() << "\n";
}

Output

Balance: 225

Direct writes like acc.balance = -999 are impossible here, which prevents invalid states. All state changes pass through safety checks in public methods.

Try it yourself

  • Add a transferTo(BankAccount& other, double amount) that reuses withdraw and deposit for safe transfers.
  • Write a try/catch test that triggers and prints error messages for invalid deposits/withdrawals.

🔹 The protected Specifier: For Family (Inheritance)

Use protected for members that should be hidden from the outside world, but still accessible to derived classes. This is useful when children need to customize or extend behavior using some internal state or helper methods.

#include <iostream>
using namespace std;
class Vehicle {
protected:
    int speed{0}; // visible to derived classes
    void setSpeed(int s) { // internal helper for children
        speed = (s < 0) ? 0 : s;
    }
public:
    void accelerate(int delta) {
        setSpeed(speed + delta);
    }
    int getSpeed() const {
        return speed;
    }
};
class Car : public Vehicle {
public:
    void turboBoost() {
        setSpeed(speed + 50);
    } // allowed: protected access
};
int main() {
    Car c;
    c.accelerate(20);
    c.turboBoost();
    cout << c.getSpeed() << "\n"; // 70
    // c.speed = 100; // ERROR: cannot access protected from outside
}

Output

70

The derived class can use speed and setSpeed() because they’re protected, but outside code cannot touch them. This keeps the API clean while enabling controlled extension.

Try it yourself

  • Create a Truck derived from Vehicle that limits setSpeed to a maximum (e.g., 120).
  • Demonstrate that Truck can access speed but external code cannot.

🔹 Access in Inheritance Modes (Public/Protected/Private)

When deriving, the inheritance mode also affects visibility of base members in the derived class’s public interface. Most of the time, use public inheritance for “is‑a” relationships. Quick rules:

  • public inheritance: base’s public → public; base’s protected → protected; base’s private → not accessible.
  • protected inheritance: base’s public/protected → protected; base’s private → not accessible.
  • private inheritance: base’s public/protected → private; base’s private → not accessible.
#include <iostream>
using namespace std;
class Base {
public:
    void pub() {}
protected:
    void pro() {}
private:
    void pri() {}
};
class D1 : public Base {
    void test() {
        pub(); // OK
        pro(); // OK
        // pri(); // ERROR
    }
};
class D2 : protected Base {
    void test() {
        pub(); // OK (becomes protected)
        pro(); // OK (remains protected)
        // pri(); // ERROR
    }
};
class D3 : private Base {
    void test() {
        pub(); // OK (becomes private)
        pro(); // OK (becomes private)
        // pri(); // ERROR
    }
};

Try it yourself

  • Create a Child class with protected inheritance and test which members are callable from outside vs inside.
  • Explain in comments why private inheritance often signals “implemented in terms of,” not a true “is‑a.”

🔹 Friends: Limited, Explicit Backdoor

A friend function or class can access private/protected members. Use this sparingly and only with clear intent, such as stepping around encapsulation for operators or tightly‑coupled helpers.

#include <iostream>
using namespace std;
class Box {
    double w{0}, h{0};
public:
    Box(double W, double H) : w(W), h(H) {}
    friend double area(const Box& b); // grants access to private members
};
double area(const Box& b) {
    return b.w * b.h; // OK: friend can access private members
}
int main() {
    Box b(3, 4);
    cout << area(b) << "\n"; // 12
}

Output

12

Friendship is not inherited or transitive; it’s granted explicitly where needed to keep visibility tight and code maintainable.

Try it yourself

  • Add a friend class Inspector that can print Box’s dimensions.
  • Demonstrate that a non‑friend utility cannot access w/h directly.

🔹 Design Tips: Choosing the Right Specifier

  • Default to private for data; expose a small, safe public API.
  • Use protected only when derived classes truly need internals; prefer private + public hooks when possible.
  • Avoid public data members; favor getters/setters or intention‑revealing methods that validate and preserve invariants.
  • Keep friend usage minimal and well‑documented.

🔹 Mini Exercise: Putting It All Together

Build a simple User base class and an Admin derived class. Use private data for email/password, a protected role string for derived customization, and a small public API for login/reset flows.

#include <iostream>
#include <stdexcept>
#include <utility>
using namespace std;
class User {
private:
    string email;
    string passwordHash;
protected:
    string role{"user"}; // for derived customization
    static string hash(const string& s) {
        if (s.size() < 6) 
        throw invalid_argument("Password too short");
        return "h" + to_string(s.size());
    }
public:
    User(string mail, string pass) : email(move(mail)), passwordHash(hash(pass)) {
        if (email.empty()) 
        throw invalid_argument("Email required");
    }
    bool login(const string& pass) const {
        return passwordHash == hash(pass);
    }
    const string& getEmail() const { return email; }
    const string& getRole() const { return role; }
};
class Admin : public User {
public:
    using User::User; // inherit constructor
    void promoteTo(const string& r) {
        role = r; // allowed: protected in base
    }
};
int main() {
    Admin a("admin@example.com", "secure123");
    cout << a.getEmail() << " (" << a.getRole() << ")\n";
    a.promoteTo("superadmin");
    cout << a.getRole() << "\n";
}

Output

admin@example.com (user)
superadmin

Try it yourself

  • Add a resetPassword(old, fresh) method that validates both values and updates the hash.
  • Create a Guest class that cannot change role at all; show compile‑time access errors if attempted.

🔹 Common Mistakes to Avoid

  • Making data public “for convenience,” which breaks encapsulation and invites bugs.
  • Overusing protected when a private member plus a public hook would suffice.
  • Returning non‑const references/pointers to internal state, allowing outside mutation.
  • Leaning on friends everywhere; prefer clear APIs and minimal privileges.

🔹 FAQs: Access Specifiers in C++

Q1. What is the default access in a class vs a struct?
In a class, members are private by default. In a struct, members are public by default.

Q2. When should I use protected instead of private?
Use protected only when derived classes truly need to use or customize internal members. If not, keep them private and expose safe public hooks.

Q3. Can I mix public, private, and protected sections multiple times?
Yes. A class can contain multiple sections of each specifier to organize members clearly.

Q4. Do access specifiers affect performance?
No in any meaningful way. They control visibility at compile time and do not impose runtime overhead in typical scenarios.

Q5. Is it okay to expose data as public if it’s just a simple struct?
For plain‑old‑data (POD) or simple “bags of data,” public fields in a struct are common. For objects with invariants and logic, prefer private data with methods.

Q6. Do friends break encapsulation?
They bypass access checks, so use them sparingly and intentionally—typically for tightly‑coupled helpers or operators where they improve clarity without exposing raw internals broadly.

🔹 Wrapping Up

Mastering Access Specifiers in C++ is key to writing secure, maintainable, and beginner‑friendly code. Default to private data, expose a small public API, use protected only when inheritance truly needs it, and keep friends rare. Practice the “Try it yourself” tasks above to solidify these habits.

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.