Classes, Encapsulation, and Access Control
Explore the fundamental concepts of classes, encapsulation, and access control in C++. Learn how to safeguard data by hiding internal state with private members and providing controlled access through public methods. Understand how these practices maintain object validity and prevent errors as your programs grow.
Up until now, we have learned how to store values in variables and change them using functions. This works for small programs, but it becomes unsafe as programs grow. A variable can be changed to any value, even an invalid one, because nothing protects it. This can lead to mistakes and inconsistent data. To avoid this, we need a way to keep data and the code that controls it together in one place. This idea is called object-oriented programming (OOP). In C++, we do this using a class.
How to create an object in C++ using Structs
Before we jump into powerful objects, let's look at how to create an object in C++ using the simplest method: the struct (structure).
A struct is a user-defined type that bundles related variables together. By default, everything inside a struct is public, which means anyone can read or write the data directly.
Let's build a simple banking system.
Let’s break this down into steps:
Lines 4–7: We define the
BankAccounttype. It holds two variables.Lines 14–15: We modify the data directly using the dot operator (
.).Line 20: This is the danger. We set
balanceto a negative number, which shouldn't be possible for this type of account. Thestructcannot stop us.
From structs to classes
To prevent the "broken balance" issue, we need encapsulation. Encapsulation is the practice of hiding internal data and restricting access to it.
In C++, we use the keyword class to define types that enforce encapsulation.
struct: Members are public by default.class: Members are private by default.
If we simply change the keyword struct to class in our example, the compiler will immediately stop us from making mistakes.
The compiler now protects the data. main() cannot touch balance directly. This is good for safety, but now our object is useless because we can't use it at all! We need a way to grant controlled access.
Access specifiers: The public interface
We control visibility using access specifiers. These are labels that divide the class into sections:
public: The interface is accessible from anywhere. We put functions here that allow users to interact with the object safely.private: The implementation is accessible only from inside the class. We keep our data here.
Let's evolve our BankAccount to allow deposits while keeping the balance hidden.
Let’s break this down into steps:
Lines 5–8: The
privatesection holds the raw data.Lines 10–28: The
publicsection defines how the world interacts with the bank account.Line 19: We added logic! The
depositfunction checksif (amount > 0). We can no longer accidentally add negative money.
Enforcing correctness (invariants)
The rule "balance must not be negative" is called an invariant, a condition that must always be true for the object to be valid.
By forcing all changes to go through public functions (setters or action methods), we ensure the invariant is never broken. Let's add a withdraw method to our class that prevents overdrafting.
Let’s understand the addition:
Lines 22–31: The
withdrawfunction acts as a gatekeeper. It checks the math before updating the private variablebalance.Line 41: The user tries to break the rule (withdraw more than they have).
Line 24: The class catches this attempt and prevents the modification. The object remains valid.
Designing for clarity: Private helpers
Finally, we can also make functions private. If a function is only used internally by the class (like a helper), it should not be exposed to the user. This keeps the public interface clean and simple.
Let's finalize our BankAccount by adding a private logTransaction helper:
Let’s understand these step by step:
Lines 12–15:
logTransactionis private. It handles the details of updating the vector and printing.Lines 21 and 28: The public methods
depositandwithdrawcall the private helper.
Note that the user of BankAccount doesn't need to know how logging works, or even that it exists. They just call deposit, and the class handles the rest responsibly.
We have moved from raw structs, where data is exposed and fragile, to classes, where data is encapsulated and safe. By making members private, we force all interactions to go through a controlled public interface, ensuring our objects never enter an invalid state.
However, in all our examples, we had to call a manual .init() function to set up our object. If we forgot to call it, our data might be incorrect. In the next lesson, we will learn about constructors, which automate this setup process the moment an object is created.