By leaving the data members of a class public, you are effectively allowing any part of your program to reach into the class and modify them. Sometimes these modifications will not cause any problems, but other times you may leave the object in a state that no longer makes sense. Encapsulation means that you close these openings and let the object itself take responsibility for its data.
Consider the class that represents a bank account. All it needs to know is the balance, and all we really need to be able to do is deposit a positive value into the balance, or withdraw a value that does not exceed the balance. Without encapsulation, you simply make the members public.
#include <iostream>
// Define the members that belong to this type
class Account
{
public:
double balance{};
};
int main()
{
Account a{};
a.balance = 100.0;
a.balance = -50.0;
std::cout << a.balance << '\n';
return 0;
}
Here, we create an object a with balance zero. We can easily add some reasonable value, then store a negative value in the object. Although language mechanisms exist that would prevent this second write, this is essentially a bank account that owes the customer money, which does not make sense. We then print the invalid result to simply illustrate that the class offers no real protection when its members are public.
Now, consider the encapsulated version of the same class. By making the data private and the operations public, we get better protection.
#include <iostream>
class Account
{
private:
double balance{};
public:
void deposit(double amount)
{
if (amount > 0.0)
{
balance += amount;
}
}
void withdraw(double amount)
{
// Require both conditions joined by logical AND to be true
if (amount > 0.0 && amount <= balance)
{
balance -= amount;
}
}
double getBalance()
{
return balance;
}
};
int main()
{
Account a{};
a.deposit(100.0);
a.withdraw(30.0);
a.withdraw(100.0);
std::cout << a.getBalance() << '\n';
return 0;
}
Here, the balance is private, so the lines that we were writing to directly are now illegal. They have been replaced by public member functions that take on responsibilities for writing to the data. The private member functions deposit and withdraw first examine the requested deposit or withdrawal.
A negative deposit is rejected, and a withdrawal is rejected if it overdraws the account.
The public member function getBalance returns a copy of the balance so other code can look at it without being given a mutable reference to the private data. After the first deposit, the first withdrawal, the second deposit, and the rejection of the second withdrawal, the program outputs 70.
Because the only members allowed to access the private data are member functions, these are the gatekeepers for the object’s state. Any property of the object that must always be true, e.g. the balance is not negative, the denominator of a fraction is not zero, the current day of the month is valid, can be enforced within the member functions, and no other code will be allowed to break the rule by modifying the data members directly.
Encapsulation also means that the hidden data members can be changed without the user of the class needing to be aware. The account's balance could later be changed to be stored as an integer value that represents cents instead of dollars, for example:
#include <iostream>
class Account
{
private:
int balanceCents{};
public:
void deposit(double amount)
{
if (amount > 0.0)
{
// Perform this conversion explicitly
balanceCents += static_cast<int>(amount * 100.0);
}
}
void withdraw(double amount)
{
int cents = static_cast<int>(amount * 100.0);
if (amount > 0.0 && cents <= balanceCents)
{
balanceCents -= cents;
}
}
double getBalance()
{
return balanceCents / 100.0;
}
};
int main()
{
Account a{};
a.deposit(100.0);
a.withdraw(30.0);
std::cout << a.getBalance() << '\n';
return 0;
}
Now, the public member functions can still work with dollars, but use the internal integer value to hold the data. Code that used the previous version of the class will still compile and function, but only the secret bits inside the class will have changed. If the balance was public, however, code that uses it would have to be modified in every case it was referenced.
Exercise: EncapsulationStarter code, hints and solutionOpenClose
Practice: Encapsulation
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Create a separate Account b, deposit 40, and withdraw 10. Print both account balances and explain why the rejected withdrawal on a does not affect b.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
class Account
{
private:
double balance{};
public:
void deposit(double amount)
{
if (amount > 0.0)
{
balance += amount;
}
}
void withdraw(double amount)
{
// Require both conditions joined by logical AND to be true
if (amount > 0.0 && amount <= balance)
{
balance -= amount;
}
}
double getBalance()
{
return balance;
}
};
int main()
{
Account a{};
a.deposit(100.0);
a.withdraw(30.0);
a.withdraw(100.0);
std::cout << a.getBalance() << '\n';
return 0;
}Hints
Account starts with balance zero. deposit accepts only a positive amount, while withdraw also checks that the amount does not exceed the current balance.
Construct b independently, then call b.deposit(40) and b.withdraw(10). Use a.getBalance() and b.getBalance() to inspect their separate private balances.
Worked solution
A separate Account b receives a deposit of 40 and a withdrawal of 10, leaving 30. a still has 70 because its attempted overdraw was rejected. All changes go through the public operations.
#include <iostream>
class Account
{
private:
double balance{};
public:
void deposit(double amount)
{
if (amount > 0.0)
{
balance += amount;
}
}
void withdraw(double amount)
{
// Require both conditions joined by logical AND to be true
if (amount > 0.0 && amount <= balance)
{
balance -= amount;
}
}
double getBalance()
{
return balance;
}
};
int main()
{
Account a{};
a.deposit(100.0);
a.withdraw(30.0);
a.withdraw(100.0);
std::cout << a.getBalance() << '\n';
Account b{};
b.deposit(40.0);
b.withdraw(10.0);
std::cout << "Balances: " << a.getBalance() << ' ' << b.getBalance() << '\n';
return 0;
}
Example output
70
Balances: 70 30
0 comments
No comments yet. Be the first.