Encapsulation and Access Specifiers

A guide to encapsulation, class invariants, and controlling data access with public and private specifiers.

Ryan McCombe
Updated

We've now successfully abstracted our Monster objects into a class. This is keeping our code nice and organized.

The next step we want to consider is how we keep the objects of our classes in a valid state.

Class Invariants

To make our classes useful, we generally want to establish some rules that users of our class can rely on.

For example, consider a simple class like this:

class Monster {
public:
  int Health { 150 };
};

It doesn't make sense for a Health value to be negative, so it would be useful to establish that as a rule that developers using our class can rely on.

A rule, such as "the Health of a Monster is never negative" is sometimes referred to as a class invariant.

We can attempt to set up the "Health is never negative" invariant by providing a TakeDamage() function that implements this rule:

class Monster {
public:
  int Health { 150 };

  void TakeDamage(int Damage) {
    Health -= Damage;
    if (Health < 0) { Health = 0; } 
  };
};

Our rule is now guaranteed if consumers only ever use the TakeDamage() function. But they can just bypass our rule by setting Health directly, so we're not done:

#include <iostream>
using namespace std;

class Monster {
public:
  int Health{150};

void TakeDamage(int Damage){/*...*/}; } int main(){ Monster Goblin; Goblin.Health -= 200; cout << "Health: " << Goblin.Health << " :("; }
Health: -50 :(

Encapsulation

In programming, the process of encapsulation involves:

  1. Bundling data and the functions that act on that data into the same object
  2. Hiding the inner workings so consumers can only interact with the object in a controlled way

We have implemented the first step of encapsulation, by bundling our variables and functions into a class.

However, we have not implemented the second step. Consumers can see all the inner workings of our object.

This is making it unclear how they're supposed to use our object - "do I use TakeDamage() or do I modify Health?". And, as we've seen, the lack of encapsulation is making it impossible for us to implement our class invariants.

Test your Knowledge

Understanding Encapsulation

What is an example of encapsulation?

Public and Private Class Members

To express this in C++ terminology, we want to have part of our class be public and part of it to be private.

The parts of the class that we want external code to use will be public. This will be the friendly, external-facing interface.

The parts that we don't want people messing with will be private.

We saw in the class definition above, we already have the word public in our code:

class Monster {
public:
  int Health { 150 };

void TakeDamage(int Damage){/*...*/}; };

In classes, all the members are private by default. What we were doing here was making everything public. Any code that creates a new object from our class can access and change everything on that object.

Let's update our class to introduce a private section, and move our Health variable there:

class Monster {
public:
void TakeDamage(int Damage){/*...*/}; private: int Health { 150 }; };

Private members of a class can still be modified by the functions of the class. Our class functions like TakeDamage() will be able to modify the Health value, but code outside our class will no longer have access to it.

Now, the public interface is very simple and protects users of our class from sneaking past our intended behavior

Monster Goblin;
Goblin.TakeDamage(50); // This is allowed
Goblin.Health -= 50; // This isn't

Test your Knowledge

Accessing Class Members

In the following example, what is the first line in the code that will cause an error?

1class Weapon {
2public:
3  int Damage{50};
4};
5
6int main(){
7  Weapon IronSword;
8  IronSword.Damage;
9  IronSword.Damage += 30;
10}

What is the first line in the code below that will cause an error?

1class Weapon {
2private:
3  int Damage{50};
4};
5
6int main(){
7  Weapon IronSword;
8  IronSword.Damage;
9  IronSword.Damage += 30;
10}

Getters

After refactoring our previous class to move Health to the private section, we are now successfully implementing our class invariant. The Health of a Monster can never be negative.

But we've indirectly added more restrictions. Code outside our class can no longer tell how much Health our monsters have:

#include <iostream>
using namespace std;

class Monster {
public:
void TakeDamage(int Damage){/*...*/}; private: int Health{150}; }; int main(){ Monster Goblin; cout << "Health: " << Goblin.Health; }
error: 'Monster::Health': cannot access
private member declared in class 'Monster'

The typical approach for allowing external code access to our private members is to provide a simple function within the public part of our class.

A function like this is sometimes called a getter. Some programming languages offer a dedicated syntax for this, but C++ keeps it simple. We just create a function in the normal way:

#include <iostream>
using namespace std;

class Monster {
public:
  int GetHealth() { return Health; }

void TakeDamage(int Damage){/*...*/}; private: int Health{150}; }; int main(){ Monster Goblin; cout << "Health: " << Goblin.GetHealth(); }
Health: 150

This allows outside code to see the current Health using the public GetHealth() function. But, they cannot change the Health, because the variable itself is still private.

Test your Knowledge

Adding Getters

Line 7 in the below code example is an error, as Damage is a private member. How should we modify our class to allow line 7 to read the weapon's Damage, but not change it?

1class Weapon {
2private:
3  int Damage { 50 };
4}
5
6Weapon IronSword;
7IronSword.Damage;

Setters

Setters have a similar purpose to getters except, predictably, they are functions that allow the outside world to update variables on our object.

The difference between making a setter available and just making the underlying variable public is that, with a setter being a function, we can control the process.

We can provide a setter for our Health variable, but in a way that maintains the invariant that the Health will never be negative:

#include <iostream>
using namespace std;

class Monster {
public:
  int GetHealth(){ return Health; }

  void SetHealth(int IncomingHealth){
    if (IncomingHealth < 0) {
      Health = 0;
    } else {
      Health = IncomingHealth;
    }
  }

void TakeDamage(int Damage){/*...*/}; private: int Health{150}; }; int main(){ Monster Goblin; cout << "Health: " << Goblin.GetHealth(); Goblin.SetHealth(-50); cout << "\nHealth: " << Goblin.GetHealth(); }
Health: 150
Health: 0

Summary

In this lesson, we explored the concept of encapsulation and its implementation in C++ using access modifiers. The key points include:

  • Class Invariants: Establishing rules for class behavior that users can rely on. For instance, ensuring a Monster's health never goes negative.
  • Encapsulation in C++: The process of bundling data and functions within a class and controlling how they are accessed and modified.
  • Public and Private Access Modifiers: Using public and private keywords to control access to class members. Public members are accessible from outside the class, while private members are not.
  • Implementing Class Invariants: Demonstrated by the TakeDamage function in the Monster class, ensuring health does not go negative.
  • Getters and Setters: Functions that allow controlled access to private class members. Getters return the value of a private member, while setters allow modifying it under certain conditions.
Next Lesson
Lesson 22 of 60

Constructors and Destructors

Learn about special functions we can add to our classes, control how our objects get created and destroyed.

Have a question about this lesson?
Answers are generated by AI models and may not have been reviewed for accuracy