public
and private
specifiers.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.
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.
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 :(
In programming, the process of encapsulation involves:
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.
What is an example of encapsulation?
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.
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
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
};
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}
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.
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 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
In this lesson, we explored the concept of encapsulation and its implementation in C++ using access modifiers. The key points include:
Monster
's health never goes negative.public
and private
keywords to control access to class members. Public members are accessible from outside the class, while private members are not.TakeDamage
function in the Monster
class, ensuring health does not go negative.In the next lesson, we will delve into constructors and destructors in C++. Here’s what you can expect to learn:
A guide to encapsulation, class invariants, and controlling data access with public
and private
specifiers.
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way