| Lesson 4 |
Encapsulation |
| Objective |
Mechanism behind packaging Data in C++ |
C++ Encapsulation (Packaging Data and Behavior in an ADT)
C++ provides the encapsulation mechanism to implement ADTs. Encapsulation packages both the internal implementation details of the type and the externally available operations and functions that can act on variables of that type. The implementation details can be made inaccessible to code that uses the type. Code that uses the ADT is called the client code for the ADT. For example, a stack could be implemented as a fixed-length array, while the publicly available operations would include push and pop. Changing the internal implementation to a linked list should not affect how push and pop are used externally. Encapsulating the data and behavior of the stack keeps the implementation of the stack hidden from its clients, which means the client code using the stack will not need to be rewritten if the stack implementation changes. Lessons 2 and 3 established the OOP concepts and the ADT goal. This lesson examines the mechanism — how C++ classes package private data and public behavior to implement that goal in code.
The Encapsulation Mechanism in C++
What Encapsulation Packages Together
Encapsulation bundles two things into a single unit — the class. First, it packages the data: the member variables that store the object's state, kept private so that external code cannot read or write them directly. Second, it packages the behavior: the member functions that define what operations are valid on the data, kept public as the interface through which all interaction with the object must pass. The combination of private data and public behavior is the fundamental expression of encapsulation in C++. Neither element alone is sufficient: private data without public behavior produces an object that cannot be used; public behavior without private data produces an object whose state can be corrupted by any code that can name it.
Client Code and Supplier Code
The encapsulation model divides the world into two roles. Supplier code is the class definition itself — the implementation that packages the data and behavior, maintained by whoever wrote and owns the class. Client code is any code that uses the class — functions, other classes, or the main program that create objects of the type and invoke its public member functions. The critical property of encapsulation is that client code depends only on the public interface — the names and signatures of the public member functions. Client code does not depend on, and cannot see, the private implementation. This means the supplier can change the implementation — replacing an array with a linked list, adding a cache, changing a data member's type — without requiring any change in the client code. The encapsulation boundary is the contract that makes this independence possible.
The Stack Example — Implementation Transparency
The ch_stack class from lesson 9 demonstrates encapsulation at work. Clients call push(), pop(), empty(), and full() — the public interface. The private section contains the array s_[], the index top_, and the sentinel constants. If the implementation were changed from a fixed array to a std::vector<char> with dynamic sizing, every one of those private details would change. The public interface — the six function signatures — would remain identical. Every line of client code that uses ch_stack would compile and run correctly without modification. This is the practical payoff of encapsulation: implementations can be improved, optimized, and corrected without rippling changes through every piece of code that uses the type.
Packaging Data — Private Member Variables
Data Members Define Object State
Data in C++ classes refers to the member variables that store the state of an object. In an ADT, these data members are kept private to enforce encapsulation — the internal state of an object cannot be accessed directly from outside the class, ensuring data integrity and preventing unwanted modifications. The class packages these data members, providing a structured way to define and store the attributes of an object. Every object of the class type has its own copy of the private data members, initialized by the constructor and modified only through the public member functions.
The Tank Class — Private Data
The following Tank class illustrates data encapsulation. The private section contains the data members that define a tank's internal state — the periscope type and gun mantlet configuration. External code cannot read or write these fields directly:
#include <string>
class Tank {
private:
std::string periscope_; // private — inaccessible to client code
std::string gun_mantlet_; // private — inaccessible to client code
int crew_count_ = 0;
// public interface will be added below
};
Why Private Data Protects the Invariant
Making data members private enforces the class invariant — the condition that must always be true for an object to be in a valid state. For a Tank, a reasonable invariant might be that crew_count_ is always non-negative and never exceeds the tank's capacity. If crew_count_ were public, any code anywhere in the program could write myTank.crew_count_ = -5 — no compile error, immediate invalid state. With crew_count_ private, the only way to change it is through public member functions — each of which can check and maintain the invariant before modifying the data.
Packaging Behavior — Public Member Functions
Accessors and Mutators
Behavior in C++ refers to the member functions that operate on the encapsulated data. In an ADT, behavior is exposed via public member functions that act as the interface to the outside world — the actual implementation details are hidden, ensuring that data is only manipulated in controlled ways. Member functions that interact with data fall into two categories. Accessors (getters) are const member functions that return information about the object's state without modifying it. Mutators (setters) are member functions that modify one or more data members. The following modernized Tank class shows both:
#include <string>
#include <string_view>
#include <iostream>
class Tank {
private:
std::string periscope_;
std::string gun_mantlet_;
int crew_count_ = 0;
public:
// Mutator — modifies private data; uses string_view for efficiency
void set_periscope(std::string_view ps) {
periscope_ = ps;
}
// Accessor — read-only; const guarantees no modification
[[nodiscard]] std::string_view get_periscope() const noexcept {
return periscope_;
}
// Mutator — validates before modifying (maintains invariant)
void set_crew(int count) {
if (count < 0) throw std::invalid_argument("crew count cannot be negative");
crew_count_ = count;
}
[[nodiscard]] int crew_count() const noexcept { return crew_count_; }
};
const on Accessors
Every member function that does not modify the object's state must be declared const — the qualifier after the closing parenthesis of the parameter list. const member functions can be called on const objects and const references. Without const, a function that reads but does not modify the object cannot be called through a const Tank& parameter — which is the standard way to pass objects to functions that should not modify them. Marking accessors const is not optional; it is part of the correct interface design for any ADT. The [[nodiscard]] attribute on accessors instructs the compiler to warn when the return value is ignored — calling get_periscope() and discarding the result is almost certainly a bug.
Beyond Getters and Setters — Behavior-Oriented Design
A class with a private field and a matching public getter plus setter is not meaningfully more encapsulated than a class with a public field — the data is equally accessible, just through an extra function call. True encapsulation means the public interface expresses operations in terms of the abstraction, not in terms of the data layout. get_periscope() and set_periscope() expose the existence and type of the periscope data member through the interface. A behavior-oriented interface would instead expose what a tank does with its periscope: scan(Angle elevation), is_optics_clear() const, retract_periscope(). These operations express the Tank's capabilities as an abstraction; the implementation is free to store the periscope state as a string, an enum, a boolean, or any other representation without changing the interface. Getters and setters are useful for simple data-transfer objects; for types with meaningful invariants and behavior, the interface should be designed around what the type does rather than what data it holds.
The Complete ADT — Interface and Implementation
The Interface — Public Member Functions
An Abstract Data Type is a type whose internal structure (data) and operations (behavior) are hidden from the user — only the interface (public methods) is exposed. The user of an ADT interacts with it purely through its defined methods, without knowing the specifics of its internal implementation. The following complete Tank class brings together private data, public accessors and mutators, and a display operation:
#include <string>
#include <string_view>
#include <iostream>
#include <stdexcept>
class Tank {
private:
std::string periscope_; // hidden from client code
std::string gun_mantlet_; // hidden from client code
int crew_count_ = 0;
public:
// Constructor — initializes all data members to a valid state
Tank(std::string_view periscope, std::string_view gun_mantlet, int crew)
: periscope_{periscope}, gun_mantlet_{gun_mantlet}
{
set_crew(crew); // use mutator to validate
}
void set_periscope(std::string_view ps) { periscope_ = ps; }
void set_gun_mantlet(std::string_view gm){ gun_mantlet_ = gm; }
void set_crew(int count) {
if (count < 0) throw std::invalid_argument("crew count cannot be negative");
crew_count_ = count;
}
[[nodiscard]] std::string_view periscope() const noexcept { return periscope_; }
[[nodiscard]] std::string_view gun_mantlet() const noexcept { return gun_mantlet_; }
[[nodiscard]] int crew_count() const noexcept { return crew_count_; }
void display_info() const {
std::cout << "Periscope : " << periscope_ << '\n';
std::cout << "Gun Mantlet: " << gun_mantlet_ << '\n';
std::cout << "Crew Count : " << crew_count_ << '\n';
}
};
// Client code — uses only the public interface
int main() {
Tank t{"panoramic", "cast steel", 4};
t.display_info();
t.set_crew(5);
// t.crew_count_ = -1; // compile error: crew_count_ is private
}
The Two-File Pattern — Header and Source
In production C++, the class interface is placed in a header file (.h or .hpp) and the implementation is placed in a separate source file (.cpp). Client code includes only the header — it sees the class declaration, the public member function signatures, and the private data member names (which must appear in the header for the compiler to know the object size), but it does not see the implementation of any member function. The implementation file contains the definitions of all member functions. When the implementation changes, only the source file needs to be recompiled — client code that includes only the header is unaffected. This is the physical expression of encapsulation in the C++ build system: the header is the contract, the source file is the fulfillment.
A Class Is a Type You Define
The Interface Is All You Need to Use the ADT
A class is a type that you define, as opposed to the types such as int and char that are already defined for you. A value for a class type is the set of values of the member variables. The interface of an ADT tells you how to use the ADT in your program. When you define an ADT as a C++ class, the interface consists of the public member functions of the class along with the comments that tell you how to use the public member functions. The interface of the ADT should be all you need to know in order to use the ADT in your program.
The Implementation Is Hidden from Client Code
The implementation of the ADT explains how the interface is realized as C++ code. The implementation consists of the private members of the class and the definitions of both the public and private member functions. Although you need the implementation in order to compile and run a program that uses the ADT, you should not need to know anything about the implementation in order to write the rest of a program that uses the ADT — that is, you should not need to know anything about the implementation in order to write the main part of the program and to write any nonmember functions used by the main part of the program.
Changing the Implementation Without Breaking Clients
The most practical test of whether a class is a true ADT is whether its implementation can be changed without modifying any client code. If the Tank class's internal representation changes — replacing std::string with a custom TankComponent type, adding a database lookup for gun mantlet specifications, or caching the display string — and no client code needs to be modified, then the encapsulation is working correctly. If a change to the private section requires changes in client code, the encapsulation has leaked: some implementation detail was accessible through the interface and is now broken. Designing the public interface in terms of the abstraction rather than the implementation data is what prevents this leakage from occurring.
In the next lesson, you will learn about the C++ class concept — the syntax and structure of class definitions in detail.

