Hello! Welcome back.
In our last few lessons, we've focused on giving our classes correct and efficient lifecycle management through the "Rule of Five," analyzing the deep mechanics of shallow copies, deep copies, and move semantics. You now have a solid understanding of how to control what happens when your objects are created, destroyed, copied, and moved.
Today, we shift our focus from object lifecycle to object boundaries. This lesson is dedicated to applying access specifiers (public, private, and protected) and explaining their compile-time enforcement. This is the C++ mechanism for encapsulation—a cornerstone of object-oriented programming that allows us to hide implementation details and present a clean, stable interface to the users of our class.
For someone with your background in C, you're used to having unrestricted access to the members of a struct. C++ introduces a formal system to control this access. We'll explore not only what these controls do, but, more importantly, how they are enforced by the compiler and what they mean for the memory layout of an object—peeling back the abstraction to see the "under the hood" reality.
1. The Three Levels of Visibility
C++ provides three keywords to define the accessibility of class members (both data and functions): public, private, and protected. These specifiers control which parts of your code are allowed to "see" and interact with the members of a class.
A great way to start is with a simple visual summary.

Now, let's watch a quick video that walks through these three specifiers with code examples.
The video 'Visibility in C++' by The Cherno provides a concise and clear introduction to the three access specifiers. It also touches on the default visibility for class versus struct.
Please watch from the beginning to 04:15. This segment covers: The concept of visibility (0:00 - 0:49) The private specifier (1:09 - 3:23) The protected specifier (3:12 - 3:56) The public specifier (3:45 - 4:15)
To summarize:
public: Members are accessible from anywhere the object is visible. This forms the public interface of your class. This is how users of your class are intended to interact with it.private: Members can only be accessed by other member functions of the same class (or byfriends, a concept we'll visit later). They are completely hidden from the outside world, including derived classes. This is for the implementation details.protected: Members can be accessed by member functions of the same class and by member functions of any derived classes. They are not accessible from unrelated, external code. This is for building class hierarchies.
A key detail mentioned in the video is the default access level:
- For a
class, members areprivateby default. - For a
struct, members arepublicby default.
Other than this default, there is no difference between a class and a struct in C++. This is a direct nod to C compatibility, where struct members are always accessible.
2. Encapsulation: The "Why" Behind Access Control
Why do we need these restrictions? The core principle is encapsulation: bundling an object's data with the functions that operate on that data, and hiding the internal details from the outside world.
This isn't about security, but about creating robust, maintainable, and easy-to-use code.
-
Protecting Invariants: By making data members
private, you force all modifications to go throughpublicmember functions (often called "setters"). This allows you to validate input and ensure the object remains in a consistent, valid state. For example, aTimeclass can prevent a user from setting theminutesmember to61. -
Decoupling and Maintainability: By hiding implementation details, you are free to change them later without breaking the code of everyone who uses your class. As long as the
publicinterface remains the same, you can refactor the internalprivatemembers and logic. For instance, you could change a class that internally uses an array to one that uses a linked list, and no client code would need to be re-written.
Let's return to The Cherno's video for a great explanation of the rationale behind using visibility rules.
Watch from 04:15 to 07:53. The speaker gives a practical example of why you'd want to make a variable private and only expose a public function to modify it. This reinforces the concepts of protecting invariants and designing a clear API.
3. The Enforcement Mechanism: A Compile-Time Check
This brings us to the most important question from your perspective: how does this actually work "under the hood"?
Access specifiers are a purely compile-time construct. They are instructions to the C++ compiler, not to the CPU. They do not add any runtime overhead, and they have no representation in the final compiled machine code.
When you compile your code, the compiler checks every member access. If you try to access a private member from an invalid context, the compiler simply issues an error and refuses to compile the program. If all accesses are valid, the compiler generates machine code that accesses the object's memory directly via offsets, just as it would for a C struct. The CPU, when executing your program, has no knowledge of public or private.
This means access specifiers are not a security feature. Since C++ is not a memory-safe language, a determined programmer can always bypass these compile-time checks and access "private" data at runtime.
Is it a security vulnerability to declare class members as public?
This Stack Exchange thread directly addresses the question of whether access specifiers have security implications. The answers provide an excellent 'under the hood' perspective on how they are enforced.
Please read the top-voted answer (by 'amon') and the answer by 'gbe'. The top answer demonstrates two ways to 'attack' private members at runtime: using reinterpret_cast to treat the object as a struct with an identical layout, and calculating the memory offset manually. This proves that at runtime, it's all just a contiguous block of memory. The answer by 'gbe' emphasizes that 'in C++, almost all type information exists only at compile time.' It correctly points out that CPUs have no concept of C++ classes, let alone access rules. This directly addresses your interest in the boundary between language features and machine execution.
This is a fundamental point. Access specifiers are a tool for software engineering—they help us write better, more correct, and more maintainable code by enforcing contracts at compile time. They do not and cannot provide runtime security in C++.
4. Memory Layout and Access Specifiers
Since access specifiers are a compile-time check, how do they affect the memory layout of an object? You might assume members are laid out in the order you declare them: public first, then protected, then private. The reality is more nuanced.
The C++ standard makes two guarantees:
- All members within a single access-specifier block (e.g., all members between one
public:and the next specifier) are allocated in memory in the order they are declared. - There is no standard-guaranteed ordering between different access-specifier blocks.
This means a compiler is free to re-arrange the blocks. It could place all private members first, then public, etc.
class MyClass {
public:
int a; // a is before b
int b;
private:
int c; // c is before d
int d;
};
In this example, the standard guarantees that a's memory address will be lower than b's, and c's will be lower than d's. However, the compiler could lay out the object in memory as {c, d, a, b} or {a, b, c, d}.
Why this freedom? It allows compilers to perform optimizations, like reordering members to minimize padding and reduce the object's overall size.
Class layout in C++: Why are members sometimes ordered?
To understand this specific rule, let's look at a Stack Overflow discussion on the topic. It quotes the relevant part of the standard.
Read the question and the first answer (by 'paxdiablo'). Focus on the standard quote provided: 'The order of allocation of nonstatic data members separated by an access-specifier is unspecified'. This is the key takeaway.
5. Access Specifiers in Inheritance
The protected keyword is meaningless outside the context of inheritance. When one class derives from another, we can also specify an access level for the inheritance itself: public, protected, or private. This controls the maximum visibility of the inherited members in the derived class.
class Derived : public Base: Public inheritance. This is the most common form. It models an "is-a" relationship.Base'spublicmembers becomeDerived'spublicmembers, andBase'sprotectedmembers becomeDerived'sprotectedmembers.class Derived : protected Base: Protected inheritance.Base'spublicandprotectedmembers all becomeDerived'sprotectedmembers.class Derived : private Base: Private inheritance. This is the default if you omit a specifier. It models an "is-implemented-in-terms-of" relationship.Base'spublicandprotectedmembers all becomeDerived'sprivatemembers.
In all cases, private members of the base class are never accessible to the derived class.
Demystifying C++ Access Specifiers: Protected Keyword and Class Access Specifiers [Explained] [2]
This can be a bit confusing, so let's watch a video that steps through each inheritance type and shows how it affects the visibility of base class members.
Please watch from 04:58 to 07:54. The video clearly demonstrates: public inheritance (04:58 - 06:32) protected inheritance (06:32 - 07:16) private inheritance (07:16 - 07:54) Focus on how the accessibility of the same base class members changes when you change the inheritance specifier.
Test your understanding!
Consider the following code:
class Base {
public:
int public_member;
protected:
int protected_member;
private:
int private_member;
};
class Derived : public Base {
public:
void member_function() {
public_member = 1; // Line 1
protected_member = 2; // Line 2
private_member = 3; // Line 3
}
};
int main() {
Derived d;
d.public_member = 10; // Line 4
d.protected_member = 20; // Line 5
d.private_member = 30; // Line 6
return 0;
}
Which lines will cause a compilation error, and why?
Show answer
The following lines will cause compilation errors:
-
Line 3 (
private_member = 3;): A derived class cannot access theprivatemembers of its base class.private_memberis private toBase. -
Line 5 (
d.protected_member = 20;):protected_memberisprotectedinDerived(due to public inheritance). Code outside the class hierarchy (likemain) cannot access protected members. -
Line 6 (
d.private_member = 30;):private_memberis not accessible outside theBaseclass at all, for two reasons: it's private toBase, and even if it were not, it would not be accessible frommain.
Lines 1, 2, and 4 are all valid.
Conclusion
You have now learned how C++ uses access specifiers to enforce encapsulation, a key principle for writing clean and maintainable object-oriented code. You've seen that these are not runtime security mechanisms but powerful compile-time checks that guide developers toward a better API design.
Key Takeaways:
public,protected,privatecontrol member visibility within the class, in derived classes, and from outside code, respectively.- Their primary purpose is to enable encapsulation, hiding implementation details and protecting class invariants.
- Enforcement is done entirely at compile-time. There is no runtime overhead, and these checks can be bypassed using low-level memory manipulation (
reinterpret_cast). - The memory layout of members within an access block is ordered, but the compiler is free to reorder the blocks themselves for optimization.
- Inheritance access specifiers (
public,protected,private) further refine the visibility of inherited members in a derived class.
We have now finished our second module, covering the fundamentals of classes from their memory layout and lifecycle to their boundaries. We are now prepared to tackle one of the most powerful features of C++: polymorphism.
In our next lesson, we will begin Module 3 by examining the memory layout of derived classes and understanding what a "base class subobject" is. This will lay the groundwork for understanding how C++ implements dynamic behavior at runtime.