Overview
This lab builds a Character class that keeps its name, health and strength private and reachable only through public getters and setters. You will implement those accessors, then take a plain setHealth that accepts any value at all and add a guard clause that enforces one rule, health can never go below zero, so that every path through the class, including takeDamage, ends up obeying it automatically.
Learning Objectives
By the end of this lab you will be able to:
- Declare private data members so they cannot be read or written from outside the class
- Implement public getter and setter methods that mediate every access to that private state
- Explain why a setter with no validation logic still allows an object to reach an invalid state
- Add a guard clause inside a setter to enforce a class invariant
- Explain why routing an internal method like takeDamage through that same setter fixes every caller at once, instead of only the caller that was tested
Concept
A Character in a game needs its health to stay sensible. never negative, since a health bar cannot show a negative amount and an "is this character still alive" check usually just asks whether health is greater than zero. If health were a plain public member, any code anywhere in the project could write character.health = -500; directly, and nothing would stop it. Every piece of code that later reads health would then have to defensively check it before trusting it, because there is no single place where an invalid value could have been caught.
Encapsulation is the practice of hiding a class's data behind an access specifier so it can only be reached through that class's own methods. Marking name, health and strength private makes them invisible to code outside Character. the only way in or out is through the public methods the class chooses to expose. On its own, though, a getter that returns a member and a setter that assigns one changes nothing about validity, a setter that just copies its parameter straight into the member is exactly as unsafe as a public field, it has only moved the unchecked write behind a function call.
The real benefit of encapsulation shows up once a setter adds a guard clause, a conditional check at the top of the function that catches an invalid value before it is assigned, such as if (h < 0) { h = 0; } inside setHealth. Because every write to health is required to go through setHealth, that one guard clause protects every caller, including takeDamage, as long as takeDamage itself calls setHealth instead of assigning to health directly.
Tasks
Task 1 - The Character class: private state and accessors
In Solution Explorer, use Add > Class... and name it Character. This generates a matching Character.h and Character.cpp. Replace their contents with the skeletons below, then complete them.
- In
Character.h, declare three private members:nameasstd::string, andhealthandstrengthasint. - In
Character.cpp, complete the constructor so it assigns each of the three parameters to the matching private member usingthis->. - Complete
getName(),getHealth()andgetStrength()so each returns its matching member.
Success condition: constructing a Character and calling all three getters returns exactly the values passed to the constructor.
#pragma once
#include <string>
class Character
{
private:
// TODO: declare three private members: name (std::string),
// health (int), strength (int)
public:
Character(std::string name, int health, int strength);
~Character();
std::string getName();
int getHealth();
int getStrength();
};
#include "Character.h"
Character::Character(std::string name, int health, int strength)
{
// TODO: assign each parameter to the matching private member using this->
}
Character::~Character() {}
std::string Character::getName()
{
// TODO: return name
}
int Character::getHealth()
{
// TODO: return health
}
int Character::getStrength()
{
// TODO: return strength
}
Task 2 - An unguarded setHealth and takeDamage
Open Character.h and add the three setters and takeDamage below to the public section, then open Character.cpp and complete each one.
- Implement
setNameandsetStrengthto assign their parameter straight to the matching member. - Implement
setHealth(int h)the same way for now, assignhstraight tohealthwith no check at all. - Implement
takeDamage(int damage)to callsetHealth(getHealth() - damage).
Then create main.cpp with Add > New Item... and use the reference code below, already complete, to construct a Character and deal it far more damage than its starting health.
Success condition: the project builds and running it prints a health value, whatever setHealth was given, unchecked.
#pragma once
#include <string>
class Character
{
private:
std::string name;
int health;
int strength;
public:
Character(std::string name, int health, int strength);
~Character();
std::string getName();
int getHealth();
int getStrength();
void setName(std::string name);
void setHealth(int h);
void setStrength(int s);
void takeDamage(int damage);
};
#include "Character.h"
Character::Character(std::string name, int health, int strength)
{
this->name = name;
this->health = health;
this->strength = strength;
}
Character::~Character() {}
std::string Character::getName()
{
return name;
}
int Character::getHealth()
{
return health;
}
int Character::getStrength()
{
return strength;
}
void Character::setName(std::string name)
{
// TODO: assign the parameter to the name member using this->
}
void Character::setHealth(int h)
{
// TODO: assign h straight to health, no check yet
}
void Character::setStrength(int s)
{
// TODO: assign the parameter to the strength member using this->
}
void Character::takeDamage(int damage)
{
// TODO: call setHealth(getHealth() - damage)
}
#include <iostream>
#include "Character.h"
int main()
{
Character carys("Carys", 40, 12);
std::cout << carys.getName() << " has " << carys.getHealth() << " health." << std::endl;
carys.takeDamage(999);
std::cout << carys.getName() << " has " << carys.getHealth() << " health after taking 999 damage." << std::endl;
return 0;
}
Task 3 - Running it and observing the invalid state
Build and run the project. Nothing needs editing in this task, just read the output.
- Read the second line the program prints,
carys's health aftertakeDamage(999). - Compare it with what a health value is supposed to mean. a character with negative health has taken more damage than it had health to lose, which no part of the class currently prevents.
Success condition: you can point at the printed number and explain why it is not a value health should ever be allowed to hold, and why setHealth as written in Task 2 let it happen.
Task 4 - Enforcing the invariant inside setHealth
Open Character.cpp and edit setHealth only. Do not touch takeDamage.
- At the top of
setHealth, add a guard clause. ifhis less than0, sethto0before it is assigned tohealth.
Success condition: rebuilding and rerunning the same main.cpp from Task 2 now prints 0 for carys's health after takeDamage(999), even though takeDamage's own code was never edited. the guard clause inside setHealth protects every path that reaches it, because takeDamage was already written to call setHealth rather than assign to health directly.
#include "Character.h"
Character::Character(std::string name, int health, int strength)
{
this->name = name;
this->health = health;
this->strength = strength;
}
Character::~Character() {}
std::string Character::getName()
{
return name;
}
int Character::getHealth()
{
return health;
}
int Character::getStrength()
{
return strength;
}
void Character::setName(std::string name)
{
this->name = name;
}
void Character::setHealth(int h)
{
// TODO: if h is less than 0, set h to 0 before it reaches the
// assignment below
health = h;
}
void Character::setStrength(int s)
{
this->strength = s;
}
void Character::takeDamage(int damage)
{
setHealth(getHealth() - damage);
}
Solution
Try the tasks above before revealing the solution.