C++ Access Specifiers
Access specifiers are keywords in C++ that control which parts of a program are allowed to read or modify the members (variables and functions) of a class. They are the mechanism behind encapsulation — one of the four pillars of object-oriented programming — because they let a class hide its internal details and expose only a controlled interface to the outside world. C++ has three access specifiers: public, private, and protected, and understanding exactly how each one behaves (including during inheritance) is essential to writing safe, maintainable classes.
Overview / How Access Specifiers Work
Every member of a class — whether a data member (variable) or a member function (method) — has an access level. That access level determines who is allowed to reference that member by name. There are three levels:
public— the member can be accessed from anywhere the object is visible: inside the class, by derived classes, and by external code such asmain().private— the member can only be accessed by code inside the same class (and byfriendfunctions/classes explicitly granted access). Not even derived classes can touch it directly.protected— behaves likeprivatefor the outside world, but derived classes can access it directly. It exists specifically to support inheritance hierarchies.
Internally, access specifiers are a purely compile-time concept. They add no extra bytes, no runtime checks, and no performance cost to an object — a private int occupies memory exactly like a public int. The compiler simply refuses to generate code that references a member from a context that isn’t allowed to see it. If you try to write obj.privateMember from outside the class, compilation fails with an “is private within this context” error before a single instruction is produced. This means access control in C++ is a tool for the programmer, not a runtime security boundary — it can technically be bypassed with pointer tricks or macros, but doing so is undefined behavior and is never something you should do in real code.
Access specifiers also control inheritance. When one class derives from another, the specifier written before the base class name (public, protected, or private) determines how the base class’s public and protected members appear inside the derived class. This is a separate, second use of the same three keywords, and it trips up many beginners — we’ll cover it in detail below.
Syntax
class ClassName {
public:
// accessible from anywhere
int publicMember;
protected:
// accessible in this class and derived classes
int protectedMember;
private:
// accessible only inside this class
int privateMember;
};
public:,protected:, andprivate:are labels, not blocks — everything declared after one applies until the next label appears (or the class ends).- You can repeat the same label multiple times in a class; members are simply grouped by whichever label most recently appeared above them.
- If you declare a class with the
classkeyword and specify no label at all, every member before the first label isprivateby default. - If you declare it with the
structkeyword instead, members default topublic. This is the only functional difference betweenclassandstructin C++.
The access-level matrix below summarizes who can reach a member declared with each specifier:
| Specifier | Same class | Derived class | Outside code |
|---|---|---|---|
public |
Yes | Yes | Yes |
protected |
Yes | Yes | No |
private |
Yes | No | No |
Examples
Example 1: Encapsulating a bank account
The most common use of access specifiers is hiding data behind a controlled interface, so invalid states (like a negative balance from a bad withdrawal) can never happen.
#include <iostream>
#include <string>
using namespace std;
class BankAccount {
private:
double balance;
string owner;
public:
BankAccount(string ownerName, double initialBalance) {
owner = ownerName;
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;
}
void printInfo() const {
cout << owner << "'s balance: $" << balance << endl;
}
};
int main() {
BankAccount acc("Alice", 100.0);
acc.deposit(50.0);
acc.withdraw(30.0);
acc.printInfo();
cout << "Direct balance: " << acc.getBalance() << endl;
return 0;
}
Output:
Alice's balance: $120
Direct balance: 120
balance and owner are private, so main() cannot write acc.balance = -1000; to corrupt the account. Instead, all changes must go through deposit() and withdraw(), which validate the amount first. This is encapsulation in action: the class guarantees its own invariants because nothing outside it can bypass the rules.
Example 2: protected members and inheritance
protected exists so a base class can share implementation details with its subclasses without exposing them to everyone else.
#include <iostream>
#include <string>
using namespace std;
class Animal {
protected:
string name;
int age;
public:
Animal(string n, int a) : name(n), age(a) {}
void baseInfo() const {
cout << name << " is " << age << " years old." << endl;
}
};
class Dog : public Animal {
public:
Dog(string n, int a) : Animal(n, a) {}
void bark() const {
cout << name << " says Woof!" << endl;
}
};
int main() {
Dog d("Rex", 3);
d.baseInfo();
d.bark();
return 0;
}
Output:
Rex is 3 years old.
Rex says Woof!
Dog::bark() reaches directly into name, which is declared in the base class Animal. That’s only legal because name is protected — if it were private, this line would fail to compile even though Dog inherits from Animal. Note also : public Animal in the Dog declaration; this is inheritance access, discussed below.
Example 3: struct default access and friend functions
This example shows the class/struct default-access difference, plus how friend grants an outside function explicit access to private members.
#include <iostream>
using namespace std;
class Point {
private:
int x, y;
public:
Point(int xVal, int yVal) : x(xVal), y(yVal) {}
friend void printPoint(const Point& p);
};
void printPoint(const Point& p) {
cout << "(" << p.x << ", " << p.y << ")" << endl;
}
struct Vector2D {
int x, y;
};
int main() {
Point p(3, 4);
printPoint(p);
Vector2D v;
v.x = 5;
v.y = 10;
cout << "Vector: (" << v.x << ", " << v.y << ")" << endl;
return 0;
}
Output:
(3, 4)
Vector: (5, 10)
x and y inside Point are private, yet the free function printPoint reads them directly because it was declared a friend inside the class. Friendship is granted, never taken — a class must explicitly list which outside functions or classes may bypass its access control. Meanwhile, Vector2D uses struct, so x and y are public by default and main() can set them with no accessor functions needed.
Under the Hood: Inheritance Access
When you write class Derived : public Base, the word before Base is the inheritance access specifier. It caps how accessible the base class’s members can become inside Derived:
- public inheritance —
publicmembers ofBasestaypublicinDerived;protectedmembers stayprotected. This is the normal “is-a” relationship and is used in the vast majority of real code. - protected inheritance — both
publicandprotectedmembers ofBasebecomeprotectedinDerived. - private inheritance — both
publicandprotectedmembers ofBasebecomeprivateinDerived, meaning code outsideDerivedcan no longer reach them at all, even thoughDeriveditself still can.
In every case, members that were already private in Base remain completely inaccessible to Derived — inheritance access specifiers only affect how already-visible (public/protected) members propagate. A crucial default to memorize: if you write class Derived : Base with no keyword at all, C++ silently uses private inheritance for a class, and public inheritance for a struct. Forgetting the keyword on a class is a classic bug — see Common Mistakes below.
Common Mistakes
Mistake 1: Accessing a private member from outside the class
class Box {
private:
int width;
};
int main() {
Box b;
b.width = 10; // Error: 'width' is private within this context
return 0;
}
The fix is to never expose raw data members directly. Add a public setter (and getter, if reading is needed) that validates the input:
#include <iostream>
using namespace std;
class Box {
private:
int width;
public:
void setWidth(int w) {
if (w >= 0) width = w;
}
int getWidth() const {
return width;
}
};
int main() {
Box b;
b.setWidth(10);
cout << b.getWidth() << endl;
return 0;
}
Output:
10
Mistake 2: Forgetting the inheritance keyword on a class
class Base {
public:
int value;
};
class Derived : Base { // no keyword -> private inheritance for a class!
public:
void show() {
cout << value << endl; // OK: Derived itself can still see it
}
};
int main() {
Derived d;
d.value = 5; // Error: 'value' is inaccessible, inherited as private
return 0;
}
Because Derived omitted the keyword, C++ used private inheritance, so value — though public in Base — becomes private in Derived and is unreachable from main(). The fix is simply to be explicit:
#include <iostream>
using namespace std;
class Base {
public:
int value;
};
class Derived : public Base {
public:
void show() {
cout << value << endl;
}
};
int main() {
Derived d;
d.value = 5;
d.show();
return 0;
}
Output:
5
Always write public, protected, or private explicitly before a base class name — never rely on the implicit default, since it differs between class and struct and is a frequent source of confusing “inaccessible base” errors.
Best Practices
- Make data members
privateby default and expose behavior throughpublicmethods — this is the core of encapsulation and lets you change internal representation later without breaking callers. - Reserve
protectedfor members a subclass genuinely needs to touch directly; overusing it weakens encapsulation almost as much as making everythingpublic. - Prefer
publicinheritance for “is-a” relationships; avoidprivate/protectedinheritance unless you specifically need to hide the base interface (composition is usually a clearer alternative). - Use
structfor simple, passive data bundles with no invariants to protect, andclassfor types that enforce rules about their own state. - Use
friendsparingly — it breaks encapsulation deliberately, so it should be reserved for tightly coupled helper functions/operators (likeoperator<<overloads), not as a general workaround for access errors. - Always write the inheritance access keyword explicitly (
public,protected, orprivate) rather than relying on the class/struct default. - Provide
constgetter methods for read-only access to private data, so callers can inspect state without being able to modify it.
Practice Exercises
- Exercise 1: Write a
Rectangleclass with private memberswidthandheight. Add public methodssetDimensions(double w, double h)that rejects negative values, andarea()that returns the computed area. Test it with a rectangle of width 4 and height 5; the expected output isArea: 20. - Exercise 2: Create a base class
Vehiclewith aprotectedmemberstring brandand a public constructor that sets it. Derive aCarclass using public inheritance with a methoddescribe()that prints a sentence usingbrand. Confirm that trying to accessbranddirectly frommain()produces a compile error, whiledescribe()works fine. - Exercise 3: Write a class
Temperaturethat stores a privatedouble celsius. Add afriendfunctionprintFahrenheit(const Temperature& t)that reads the private value directly and prints it converted to Fahrenheit using the formulaF = C * 9/5 + 32. Test with 100 degrees Celsius; expected output is212.
Summary
publicmembers are accessible from anywhere;privatemembers only from inside the class;protectedmembers from inside the class and its derived classes.- Access specifiers are enforced entirely at compile time and add no runtime cost.
classdefaults toprivatemember access andprivateinheritance;structdefaults topublicfor both — always be explicit to avoid surprises.- The inheritance access specifier (
public,protected,privatebefore the base class name) caps how visible inherited members become in the derived class. friendfunctions and classes are an explicit, deliberate exception to the normal access rules.- Good encapsulation means keeping data
privateand exposing a minimal, validatedpublicinterface.
