C++ course menu
C++ Encapsulation: Private Data, Getters, Setters and Class Invariants
Learn encapsulation in C++: hiding data with private members, keeping objects valid with class invariants, getters and setters done right, validation in constructors and methods, static members, and friend functions.
On this page
Encapsulation means keeping an object's data private and allowing changes only through methods that keep the object valid. Code outside the class sees what the object can do, not how it stores its data. That makes objects harder to misuse, and lets you change the inside without breaking the code that uses it.
Class invariants#
An invariant is a rule that must always be true for an object. For example: a bank balance is never negative; a date's month is between 1 and 12; a Fraction never has a zero denominator. Encapsulation is how a class guarantees its invariants:
- The constructor establishes the invariant.
- Every public method keeps it true.
- Private data stops outside code from breaking it.
#include <iostream>
#include <stdexcept>
class Percentage {
public:
explicit Percentage(double value) { set(value); }
void set(double value) {
if (value < 0 || value > 100) {
throw std::out_of_range("percentage must be between 0 and 100");
}
value_ = value;
}
double value() const { return value_; }
private:
double value_ = 0; // invariant: always between 0 and 100
};
int main() {
Percentage p(45);
p.set(80);
std::cout << p.value() << '\n';
try {
p.set(140);
} catch (const std::out_of_range& e) {
std::cout << "Rejected: " << e.what() << '\n';
}
std::cout << p.value() << '\n';
}80
Rejected: percentage must be between 0 and 100
80The bad value is rejected and the object stays valid.
Getters and setters, used well#
A getter returns a value; a setter changes it. Simple getters are fine, but blindly adding a setter for every field defeats encapsulation, because it lets callers put the object into any state. Prefer methods that describe actions in the problem's own words:
#include <iostream>
#include <string>
class Order {
public:
explicit Order(std::string id) : id_(std::move(id)) {}
void add_item(double price, int quantity) {
if (shipped_ || price < 0 || quantity <= 0) return;
total_ += price * quantity;
items_ += quantity;
}
void ship() {
if (items_ > 0) shipped_ = true;
}
double total() const { return total_; }
int items() const { return items_; }
bool shipped() const { return shipped_; }
private:
std::string id_;
double total_ = 0;
int items_ = 0;
bool shipped_ = false;
};
int main() {
Order order("A-1001");
order.add_item(250, 2);
order.add_item(99, 1);
order.ship();
order.add_item(500, 1); // ignored: already shipped
std::cout << order.items() << " items, total " << order.total() << '\n';
std::cout << "shipped: " << std::boolalpha << order.shipped() << '\n';
}3 items, total 599
shipped: trueThere is no set_total or set_shipped: the total can only change by adding items, and an order can only be marked shipped through ship(), which checks it is not empty.
Hiding the representation#
Because callers only use methods, you can change how data is stored without changing any calling code. Here, a duration is stored in seconds, but users work in hours and minutes:
#include <iostream>
class Duration {
public:
Duration(int hours, int minutes) : seconds_(hours * 3600 + minutes * 60) {}
int hours() const { return seconds_ / 3600; }
int minutes() const { return seconds_ % 3600 / 60; }
void add_minutes(int m) { seconds_ += m * 60; }
private:
int seconds_; // could change to minutes or a std::chrono type later
};
int main() {
Duration d(1, 50);
d.add_minutes(25);
std::cout << d.hours() << "h " << d.minutes() << "m\n";
}2h 15mstatic members#
A static data member belongs to the class, not to any one object: all objects share it. A static member function can be called without an object:
#include <iostream>
class Ticket {
public:
Ticket() : number_(++issued_) {}
int number() const { return number_; }
static int issued() { return issued_; }
private:
int number_;
static inline int issued_ = 0; // one counter shared by all tickets (C++17 inline)
};
int main() {
Ticket a;
Ticket b;
Ticket c;
std::cout << a.number() << ' ' << c.number() << '\n';
std::cout << "Issued: " << Ticket::issued() << '\n';
}1 3
Issued: 3friend functions#
Occasionally a function outside the class needs access to its private members, most often the << operator for printing. Declaring it a friend grants that access:
#include <iostream>
class Point {
public:
Point(int x, int y) : x_(x), y_(y) {}
friend std::ostream& operator<<(std::ostream& out, const Point& p);
private:
int x_;
int y_;
};
std::ostream& operator<<(std::ostream& out, const Point& p) {
return out << '(' << p.x_ << ", " << p.y_ << ')';
}
int main() {
std::cout << Point(3, 4) << '\n';
}(3, 4)Use friend sparingly: every friend is another piece of code that can break your invariants. Operator Overloading covers operator<< in detail.
Common mistakes#
Practice#
Practice exercise
Write a class Stock for a warehouse item with a private quantity that can never go negative. Provide receive(int n) and bool dispatch(int n) (which fails if there is not enough stock), plus quantity() const. Receive 50, dispatch 30, try to dispatch 40, and print the results.
Show solution
#include <iostream>
class Stock {
public:
void receive(int n) {
if (n > 0) quantity_ += n;
}
bool dispatch(int n) {
if (n <= 0 || n > quantity_) return false;
quantity_ -= n;
return true;
}
int quantity() const { return quantity_; }
private:
int quantity_ = 0; // invariant: never negative
};
int main() {
Stock s;
s.receive(50);
std::cout << std::boolalpha;
std::cout << s.dispatch(30) << ' ' << s.dispatch(40) << '\n';
std::cout << s.quantity() << '\n';
}true false
20Key takeaway#
Finished this lesson?