C++ โ€ข OBJECT-ORIENTED PROGRAMMING

Defining Classes with Access Specifiers โ€” Define classes with private/public sections

Control visibility and enforce encapsulation by partitioning class members into private and public sections.

Historical Context & Motivation

The idea of restricting access to an object's internal state did not originate with C++. Throughout the 1960s and 1970s, software projects grew in scale and complexity, exposing a recurring engineering problem: when every part of a program could read and modify every piece of data, bugs proliferated and maintenance became extraordinarily expensive. Researchers in programming language design recognized that information hiding โ€” the deliberate concealment of implementation details behind a well-defined interface โ€” was essential to managing large codebases. This principle influenced a succession of languages, ultimately shaping the access-specifier mechanism that C++ programmers use every day.

1967
Simula 67 Introduces Classes
Ole-Johan Dahl and Kristen Nygaard released Simula 67, the first language to feature classes and objects. Although it lacked formal access control, it demonstrated that bundling data with procedures improved code organization.
1972
Parnas on Information Hiding
David Parnas published his landmark paper arguing that module interfaces should expose only what clients need, hiding design decisions likely to change. This became the intellectual foundation for access specifiers.
1980
C with Classes
Bjarne Stroustrup created 'C with Classes,' introducing the class construct along with public and private sections. The design drew directly from Simula's class concept and Parnas's information-hiding principle.
1985
C++ 1.0 Released
The language was officially named C++ and published with three access specifiers โ€” public, private, and protected โ€” giving programmers fine-grained control over member visibility.
1998
ISO C++98 Standard
The first ISO standard codified the semantics of access specifiers. The rules established in C++98 remain essentially unchanged through C++23, underscoring their stability and importance.

The central question these developments addressed is straightforward yet profound: how can a language enforce the boundary between an object's internal representation and the interface it presents to the rest of the program? C++ answers this question with access specifiers โ€” the keywords public, private, and protected โ€” which partition class members into regions with distinct visibility rules.

Core Principles & Definitions

Before exploring syntax, it is important to internalize the conceptual framework that access specifiers serve. A C++ class is not merely a bundle of variables and functions; it is a contract between the class author (who defines the implementation) and the class client (who instantiates and uses objects of that type). Access specifiers formalize the terms of that contract by declaring which members are part of the public interface and which are reserved for internal use.

1

Encapsulation

Encapsulation bundles data and the functions that operate on it into a single unit โ€” the class โ€” and uses access specifiers to hide the data from unauthorized access. It is the cornerstone of object-oriented design in C++.
2

Public Interface

Members declared under public: are accessible from any scope that can see the class definition. They form the API through which client code interacts with objects.
3

Private Implementation

Members declared under private: are accessible only within the class's own member functions (and friend declarations). They embody the hidden internal state.
4

Class vs. Struct Default Access

In C++, class defaults to private access while struct defaults to public. This is the only semantic difference between the two keywords when defining a user-defined type.
5

Invariant Preservation

By making data private and exposing only validated mutator functions, a class can guarantee its internal invariants โ€” logical conditions that must always hold โ€” are never violated by external code.
โœฆ KEY TAKEAWAY
Think of a class like a bank vault. The public section is the lobby with teller windows โ€” customers interact through defined transactions (member functions). The private section is the vault itself โ€” only authorized bank employees (member functions and friends) can reach the cash (data). The vault's physical layout could be reorganized without affecting the lobby interface, just as private members can change without breaking client code.

Visual Explanation โ€” Anatomy of a Class

The diagram above shows the anatomy of a BankAccount class. The pink dashed region contains private members โ€” data fields and a helper function hidden from external code. The cyan solid region contains the public interface โ€” constructors, mutators, and accessors that any code with a BankAccount object may invoke.

Notice that all data members reside in the private section, a deliberate design choice that reflects the principle of least privilege: client code receives only the access it genuinely needs. If a client wants the balance, it calls getBalance(), which returns a copy rather than a reference to the internal field. If a client wants to change the balance, it must go through deposit() or withdraw(), both of which can validate inputs before modifying state. This pattern โ€” private data, public accessors and mutators โ€” is the canonical encapsulation idiom in C++.

How Access Specifiers Work

Access control in C++ is a compile-time mechanism. When the compiler processes a member access expression such as obj.balance, it checks whether the accessing context โ€” a function, another class, or a friend โ€” has the necessary permission as declared by the access specifier in the class definition. If the access is illegal, the compiler emits an error and compilation fails. No run-time overhead is involved: access specifiers generate no extra machine instructions and impose no performance penalty.

Syntax and Placement

An access specifier is one of the keywords public, private, or protected, followed by a colon. It may appear any number of times within a class body, and each occurrence sets the access level for all subsequent member declarations until the next specifier or the closing brace. There is no requirement to group all private members together or to place public members first; however, a conventional ordering โ€” public interface first, then protected, then private โ€” improves readability.

โ„น๏ธ class vs. struct
When you write class Foo { int x; };, x is private by default. When you write struct Foo { int x; };, x is public by default. Both forms can use explicit access specifiers to override the default. In idiomatic C++, use struct for plain data aggregates and class when enforcing invariants through encapsulation.

Access Rules Summary

Summary of C++ access specifier visibility rules
SpecifierAccessible ByTypical Usage
publicAny code that can see the class definitionConstructors, interface methods, operator overloads
privateMember functions and friends of the class onlyData members, internal helper functions
protectedMember functions, friends, and derived classesMembers needed by subclasses but not external clients

An important subtlety is that access control is per-class, not per-object. A member function of class Foo may access private members of any Foo object, not just this. This is why copy constructors and comparison operators can directly read the private fields of their arguments.

Common Patterns โ€” Getters, Setters, and the PIMPL Idiom

Once you understand the mechanics of access specifiers, the next question is how to design the boundary between public and private sections effectively. Several well-established patterns have emerged in the C++ community, each addressing a specific design need.

This diagram illustrates the flow of access. Client code on the left successfully calls public methods (green check marks) but is blocked by the compiler from accessing private members (red X marks). The arrows show legal call paths into the public interface.

Getter / Setter Pattern

The most common pattern is to declare data members as private and provide public accessor (getter) and mutator (setter) functions. Getters are typically declared const to signal that they do not modify the object. Setters can include validation logic โ€” for example, a setAge(int) function might reject negative values. Not every private field needs a getter or setter; expose only what clients genuinely require.

The PIMPL Idiom

The Pointer to Implementation (PIMPL) idiom takes access control a step further by moving all private data into a separate, forward-declared class whose definition lives only in the .cpp file. The visible class holds a single private pointer (std::unique_ptr<Impl>) to the implementation. This technique achieves true binary-level encapsulation: clients never see the private fields at all, and changing the implementation does not require recompiling client code.

Worked Example โ€” Building a Temperature Sensor Class

Let us design a TemperatureSensor class from scratch, applying the principles covered so far. The class will record temperature readings and provide a safe public interface while hiding internal bookkeeping.

Designing TemperatureSensor with Access Specifiers
1
Step 1 โ€” Identify the Data (Private Section)We need to store the sensor's current reading and a history of past readings. We also want to track the number of readings taken. These are implementation details that clients should not manipulate directly. We declare them under private:. private: double currentTempCelsius_; std::vector<double> history_; int readingCount_;
Three private data members identified, with trailing underscore naming convention.
2
Step 2 โ€” Define the Constructor (Public Section)Clients must be able to create sensor objects, so the constructor is public. We initialize currentTempCelsius_ to a sensible default and readingCount_ to zero. public: TemperatureSensor() : currentTempCelsius_(0.0), readingCount_(0) {}
Default constructor in the public section with member initializer list.
3
Step 3 โ€” Add a Mutator with ValidationThe recordReading() public method accepts a new temperature. Before updating state, it validates that the reading is within the physically plausible range (โˆ’273.15 ยฐC to 1000 ยฐC). Invalid inputs are rejected, protecting the class invariant. public: bool recordReading(double tempC) { if (tempC < -273.15 || tempC > 1000.0) return false; // reject invalid data currentTempCelsius_ = tempC; history_.push_back(tempC); ++readingCount_; return true; }
The mutator enforces the invariant: all stored temperatures are within physical bounds.
4
Step 4 โ€” Add AccessorsWe provide const accessors so client code can query the sensor without modifying its state. public: double getCurrentTemp() const { return currentTempCelsius_; } int getReadingCount() const { return readingCount_; } double getAverageTemp() const { if (history_.empty()) return 0.0; double sum = 0.0; for (double t : history_) sum += t; return sum / history_.size(); }
Three const accessors expose derived information without leaking internal containers.
5
Step 5 โ€” Client Code DemonstrationA client creates a TemperatureSensor and interacts only through public methods. Any attempt to access currentTempCelsius_ or history_ directly will trigger a compile-time error. int main() { TemperatureSensor sensor; sensor.recordReading(22.5); sensor.recordReading(23.1); sensor.recordReading(-300.0); // returns false, rejected std::cout << sensor.getAverageTemp(); // prints 22.8 // sensor.history_.clear(); // ERROR: 'history_' is private }
The class successfully enforces its invariant: no invalid temperature can enter the history, and no external code can corrupt internal state.

Strengths, Limitations, and Comparisons

Access specifiers are a powerful tool, but like all language features they involve trade-offs. Understanding these helps you make informed design decisions rather than mechanically applying patterns.

Strengths and limitations of C++ access specifiers
AspectStrengthsLimitations
EncapsulationPrevents external code from violating class invariants; internal representation can be changed without breaking clients.Private members are still visible in the header file, creating a compile-time dependency even though clients cannot access them directly.
MaintenanceLocalizes the impact of changes: modifying a private helper function affects only the class's own translation unit.Over-encapsulation with excessive getters/setters can be worse than public fields, adding boilerplate without real protection.
PerformanceZero run-time cost; access checking occurs entirely at compile time.Accessor/mutator function calls may prevent the optimizer from inlining in rare cases, though modern compilers typically inline trivial getters.
TestingEncourages testing through the public interface, producing more resilient tests that survive refactoring.Unit-testing private helper functions requires workarounds such as friend test classes, which can feel inelegant.
FlexibilityThe friend mechanism and protected access provide escape hatches for legitimate cross-class access.Overuse of friend declarations can erode encapsulation. Protected access couples base and derived classes tightly.
โœฆ KEY TAKEAWAY
Access specifiers are akin to the exterior walls and service corridors of a building. The public hallways let visitors navigate freely, while the service corridors (private) let maintenance crews work without disrupting guests. If you remove all interior walls โ€” making everything public โ€” the building functions but cannot be renovated without displacing everyone. If you over-partition with unnecessary doors โ€” trivial getters for every field โ€” you add complexity without true protection. Good class design finds the right boundary: expose behavior, not data.

Connection to Advanced OOP โ€” Friends, Protected, and Modules

The private/public dichotomy covered so far is the foundation, but real-world C++ programs often require more nuanced access policies. This section briefly surveys three advanced mechanisms that extend or complement basic access specifiers.

Advanced access control mechanisms in C++
FeaturePurposeRelationship to Access Specifiers
friend declarationsGrant a specific non-member function or another class access to private and protected members.Punches a controlled hole in the encapsulation boundary. Use sparingly and document the rationale.
protected specifierAllow derived classes to access base class members that remain hidden from unrelated code.Sits between public and private; enables polymorphic extension while preserving encapsulation from external clients.
C++20 ModulesReplace header-file inclusion with a module system that supports exported and non-exported entities.Modules complement access specifiers by controlling visibility at the file level, while access specifiers control it at the class level.

As you advance through inheritance hierarchies, design patterns, and large-scale project architecture, the simple rule you learned here โ€” make data private, expose behavior publicly โ€” remains the guiding heuristic. The protected specifier and friend declarations are refinements that handle specific scenarios where the strict private/public boundary is too coarse. C++20 modules add yet another layer of control at the translation-unit boundary, but they do not replace class-level access specifiers โ€” rather, the two mechanisms operate orthogonally.

Practice Problems

PROBLEM 1 โ€” CONCEPTUAL
Explain why the default access level for a class is private while the default for a struct is public. What design philosophy does each default reflect, and when would you choose one keyword over the other?
PROBLEM 2 โ€” BASIC
Given the following class definition, identify which member accesses in main() will compile and which will produce errors. class Counter { int count; public: Counter() : count(0) {} void increment() { ++count; } int getCount() const { return count; } }; int main() { Counter c; c.increment(); // (A) c.count = 10; // (B) int x = c.getCount();// (C) }
PROBLEM 3 โ€” INTERMEDIATE
Design a class Fraction that represents a rational number. The class should store a numerator and denominator as private data, enforce the invariant that the denominator is never zero, and automatically reduce the fraction to lowest terms on construction. Provide public accessors and a public toString() method. Write the complete class definition with a private helper gcd() method.
PROBLEM 4 โ€” APPLIED
You are writing a library for an embedded temperature controller. The Thermostat class must store a target temperature, a current temperature (updated via hardware polling), and a boolean indicating whether the heater relay is on. The target temperature must be between 5.0 ยฐC and 35.0 ยฐC. Design the class with appropriate access specifiers. Then explain why making the relay state private, even though a test engineer might want to inspect it, is the correct design choice.
PROBLEM 5 โ€” CRITICAL THINKING
A colleague argues that access specifiers are 'security theater' because a determined programmer can bypass them using pointer arithmetic, reinterpret_cast, or the #define private public hack. Construct a rigorous counter-argument explaining the actual purpose of access specifiers and why the existence of workarounds does not invalidate their value.

Lesson Summary

C++ access specifiers โ€” public, private, and protected โ€” partition a class's members into regions with distinct visibility rules. The private section hides data members and helper functions that only the class's own member functions (and declared friends) may access, while the public section exposes the interface โ€” constructors, accessors, mutators, and operators โ€” through which all external code interacts with objects of the class.

This mechanism implements encapsulation, the foundational OOP principle that binds data with the operations acting on it and shields internal representation from external interference. Access control is enforced entirely at compile time with zero run-time overhead. The class keyword defaults to private access while struct defaults to public โ€” the only semantic distinction between the two. By making data private, validating inputs in public mutators, and returning copies from accessors, a class preserves its invariants and enables safe, maintainable evolution of its internal design.

Varsity Tutors โ€ข C++ โ€ข Defining Classes with Access Specifiers