This lesson is not translated yet – the Russian original is below.
Конструктор гарантирует, что объект родился правильным. Но если поля публичны, испортить его может кто угодно и в любой момент. Модификаторы доступа закрывают этот последний зазор.
class и struct
struct Point {
double x; // public по умолчанию
};
class Widget {
int count_; // private по умолчанию
};
Это единственное различие. Всё остальное – конструкторы, деструкторы, наследование, виртуальные методы – работает одинаково.
Различие поддерживают соглашением: struct – для наборов данных без инварианта (Point,
Config, HttpHeader), class – когда есть что защищать.
Три модификатора
class Account {
public:
Account(int balance);
void Deposit(int amount);
int Balance() const;
protected:
void LogOperation(const char* what);
private:
bool IsValid() const;
int balance_;
};
| Модификатор | Кто видит |
|---|---|
public |
все |
protected |
сам класс и наследники |
private |
только сам класс (и его друзья) |
Секций может быть сколько угодно и в любом порядке; действуют они до следующей секции.
Общепринятый порядок – сначала public, потом остальное: читающий код видит сначала то, чем
пользуется.
Зачем закрывать поля
Дело не в защите от злоумышленника – её здесь и нет. Дело в инварианте: утверждении, которое верно для объекта всегда.
class Account {
public:
Account(int balance) : balance_(balance) {
if (balance < 0) {
throw std::invalid_argument("отрицательный баланс");
}
}
void Withdraw(int amount) {
if (amount > balance_) {
throw std::runtime_error("недостаточно средств");
}
balance_ -= amount;
}
int Balance() const { return balance_; }
private:
int balance_; // инвариант: всегда >= 0
};
Пока balance_ закрыто, инвариант держат конструктор и три метода – и проверять его достаточно
там. Откройте поле – и гарантия исчезает: испортить баланс может любая строчка в проекте, а
искать её придётся везде.
Второе следствие важнее первого: закрытое можно менять. Замена int balance_ на пару
«копейки + валюта» не заденет ни одного пользователя класса, если наружу торчат только методы.
Публичное поле – это обязательство на всю жизнь типа.
Геттер и сеттер на каждое поле – не инкапсуляция, а публичное поле с лишним синтаксисом. Смысл появляется тогда, когда наружу торчит операция, а не поле: account.Withdraw(100) вместо account.SetBalance(account.GetBalance() - 100). Во втором варианте инвариант проверять негде.
Доступ проверяется, но не защищает
Модификаторы – проверка компилятора, а не механизм времени выполнения. В памяти приватное поле лежит ровно там же, где лежало бы публичное, и добраться до него можно арифметикой указателей или приведением типов.
class Cls {
public:
Cls(char c, double d, int i);
private:
char c_;
double d_;
int i_;
};
// Зная раскладку полей, можно получить адрес приватного поля через reinterpret_cast.
// Такой код опирается на неопределённое поведение и в рабочем проекте недопустим,
// но он показывает, что private – это правило языка, а не замок.
Отсюда практический вывод: private защищает от случайного доступа, а не от намеренного.
Секреты в объекте так же доступны, как и в глобальной переменной; шифровать private не
умеет.
Что класть в заголовок
Приватные поля вынуждены находиться в объявлении класса – компилятору нужно знать размер объекта. Значит, они попадают в заголовок и видны всем, кто его включает: не для доступа, но для перекомпиляции.
// widget.hpp – всё это подставится каждому пользователю
class Widget {
public:
void Draw() const;
private:
Renderer renderer_; // и <renderer.hpp> тоже придётся включить
Cache cache_;
};
Добавили приватное поле – пересобрался весь проект. Классический ответ на это – идиома pImpl: в классе остаётся один указатель на скрытую реализацию.
// widget.hpp
class Widget {
public:
Widget();
~Widget();
void Draw() const;
private:
struct Impl; // объявление без определения
Impl* impl_;
};
Теперь заголовок не зависит ни от Renderer, ни от Cache, и правка полей никого не задевает.
Платите за это одним разыменованием на вызов и выделением в куче – цена, которая окупается на
больших проектах и не окупается на маленьких.
Имена приватных полей часто помечают: balance_, m_balance, _balance. Смысл не в красоте: в конструкторе Account(int balance) : balance_(balance) сразу видно, где поле, а где параметр, и не нужен this->. Какой именно вариант – вопрос принятого в команде стиля; в этом курсе используется подчёркивание в конце.
Друзья
Иногда доступ к закрытому нужен коду, который методом класса быть не может, – типичный пример
оператор вывода в поток. Для этого есть friend:
class Point {
public:
Point(double x, double y) : x_(x), y_(y) {}
friend std::ostream& operator<<(std::ostream& out, const Point& p);
private:
double x_;
double y_;
};
std::ostream& operator<<(std::ostream& out, const Point& p) {
return out << '(' << p.x_ << ", " << p.y_ << ')';
}
Дружба объявляется внутри класса – то есть класс сам решает, кого пускать, снаружи навязать
доступ нельзя. Подробнее о friend и о том, почему оператор вывода не может быть методом, – в
модуле про дополнительные возможности.