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.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

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.

SECTION 2

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.

SECTION 3

Visual Explanation — Anatomy of a Class

class BankAccountprivate:std::string ownerName;double balance;int accountNumber;bool validateAmount(double amt);🔒 HIDDEN fromoutside codepublic:BankAccount(std::string name, int num);void deposit(double amount);bool withdraw(double amount);double getBalance() const;std::string getOwner() const;✓ ACCESSIBLEfrom anywhere
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++.

SECTION 4

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

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

Summary of C++ access specifier visibility rules

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.

SECTION 5

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.

Client Code Interaction with Encapsulated ClassCLIENT CODEBankAccount acct ("Alice", 1001);acct.deposit(500);acct.getBalance();acct.balance = -1;// ✗ Compile error!acct.ownerName = "";// ✗ Compile error!✗✗class BankAccountpublic:BankAccount(string, int)void deposit(double)double getBalance() conststring getOwner() constprivate:string ownerNamedouble balanceint accountNumberbool validateAmount(double)✓✓
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.

SECTION 6

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. 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. 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. 3

    Step 3 — Add a Mutator with Validation

    The 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. 4

    Step 4 — Add Accessors

    We 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. 5

    Step 5 — Client Code Demonstration

    A 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.

SECTION 7

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.

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.

Strengths and limitations of C++ access specifiers

✦

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.

SECTION 8

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.

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.

Advanced access control mechanisms in C++

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.

SECTION 9

Practice Problems

Question 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?
Show answer
The class keyword defaults to private because it is intended for types that enforce invariants through encapsulation — the assumption is that data should be hidden unless explicitly exposed. The struct keyword defaults to public for backward compatibility with C and to serve as a lightweight aggregate where all fields are part of the public interface. Idiomatically, use struct for passive data holders (e.g., a POD type or a simple record) and class when the type has invariants, non-trivial constructors, or meaningful behavior. Both keywords produce the same kind of user-defined type; the choice signals intent to human readers.

Question 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) }
Show answer
Line (A) compiles because increment() is declared in the public section. Line (B) produces a compile error because count is declared before any access specifier in a class, so it defaults to private. Line (C) compiles because getCount() is in the public section. The key insight is that int count; appearing before the public: label makes it private by default.

Question 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.
Show answer
A correct solution places the numerator, denominator, and gcd() in the private section and the constructor, accessors, and toString() in the public section. The constructor validates that the denominator is not zero (e.g., throwing std::invalid_argument) and then divides both numerator and denominator by their GCD. The private gcd() helper can use the Euclidean algorithm. Crucially, because gcd() is private, clients cannot call it directly and cannot observe that reduction is performed — the implementation could switch to a different algorithm without affecting any external code.

Question 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.
Show answer
All three data members — targetTemp_, currentTemp_, and heaterOn_ — should be private. The public interface should include setTarget(double) (which validates the 5–35 range), updateCurrentTemp(double) (called by a hardware polling routine), and accessors isHeaterOn() and getTargetTemp(). Making the relay state private prevents external code from toggling the heater without the thermostat's control logic, which would bypass safety checks. A test engineer can inspect it via the public accessor or by declaring a friend test fixture — a controlled exception that does not weaken the encapsulation for production code.

Question 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.
Show answer
Access specifiers are a software engineering tool, not a security mechanism. Their purpose is to communicate and enforce a contract between the class author and class clients at compile time, ensuring that accidental misuse is caught early and that the class's invariants are maintained under normal usage. The workarounds your colleague describes all invoke undefined behavior (violating the strict aliasing rule, redefining a keyword), meaning any program that uses them has no guarantees about correct execution. More importantly, access specifiers reduce cognitive load: a developer reading a header file can instantly distinguish the public API from implementation details, enabling effective code review, documentation generation, and IDE support. The fact that a sufficiently determined programmer can circumvent them is no more relevant than the fact that a lockpick exists — the lock on a door still prevents accidental entry and signals the owner's intent. In large codebases, the disciplined use of access specifiers dramatically reduces coupling and makes refactoring tractable, benefits that are entirely independent of whether someone could theoretically bypass the controls.

SUMMARY

Lesson Summary

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