Encapsulation and Access Specifiers

A guide to encapsulation, class invariants, and controlling data access with public and private specifiers.
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art of a character in a prison cell
Ryan McCombe
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.

Contracts

In programming, the term contract is often used to relate to these forms of assertions. A contract is simply a guarantee about how a system we write - such as a function or class - will behave. Sensible contracts make those systems easier to use.

A class itself is a form of contract that is enforced by the compiler. For example, if an object is a Monster, it is guaranteed to have an int variable called Health.

Class invariants are another form of contract - they’re just one we have to implement and document ourselves.

If we guarantee that Health is never negative, that means the consumers never need to handle that possibility by, for example, writing additional if statements when they’re working with our object.

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?

Functions are also Encapsulations

We've already been using a form of encapsulation. Consider what a function is. A function is a way to hide (or encapsulate) a block of code inside a nice, friendly package.

A function body can get as complicated as needed - it can have hundreds of lines of code, maybe dozens of nested function calls.

Yet, for a developer using the function, all that is hidden away. All they need to do is write a single line of code to call the function and trust that it will work.

We have similar goals with our class design.

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

Multiple Access Specifiers

Our class can have as many access specifiers as we want. Variables and functions have the access level of the nearest proceeding specifier, or private if there are no proceeding specifiers:

class MyClass{
  int VariableA; // private

public:
  int VariableB; // public
  int VariableC; // public

private:
  int VariableD; // private

public:
  int VariableE; // public
};
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.

Preview: Constructors and Destructors

In the next lesson, we will delve into constructors and destructors in C++. Here’s what you can expect to learn:

  • Understanding Constructors: Learn about constructors, special functions that are called when a new object is created. We will explore how they can be used to initialize objects.
  • Implementing Different Types of Constructors: Discover the different types of constructors in C++, including default constructors and parameterized constructors.
  • Destructors: Learn how to create functions within our class that are automatically called when our objects are destroyed.

Was this lesson useful?

Next Lesson

Constructors and Destructors

Learn about special functions we can add to our classes, control how our objects get created and destroyed.
3D art showing a blacksmith character
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Encapsulation and Access Specifiers

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

3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
Classes and Structs
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 56 Lessons
  • Over 200 Quiz Questions
  • 95% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Constructors and Destructors

Learn about special functions we can add to our classes, control how our objects get created and destroyed.
3D art showing a blacksmith character
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved