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 frommain()
and observe the compiler error. - Convert that
class
to astruct
and note how default access changes. Then explicitly mark the member asprivate
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 reuseswithdraw
anddeposit
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 fromVehicle
that limitssetSpeed
to a maximum (e.g., 120). - Demonstrate that
Truck
can accessspeed
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’sprotected
→ protected; base’sprivate
→ not accessible. - protected inheritance: base’s
public
/protected
→ protected; base’sprivate
→ not accessible. - private inheritance: base’s
public
/protected
→ private; base’sprivate
→ 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 withprotected
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 printBox
’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 changerole
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.